diff options
Diffstat (limited to 'tests')
383 files changed, 26772 insertions, 3231 deletions
diff --git a/tests/ActivityManagerPerfTests/tests/Android.bp b/tests/ActivityManagerPerfTests/tests/Android.bp index 268715c6d1a6..2ae2cc49bb7a 100644 --- a/tests/ActivityManagerPerfTests/tests/Android.bp +++ b/tests/ActivityManagerPerfTests/tests/Android.bp @@ -16,7 +16,7 @@ android_test { name: "ActivityManagerPerfTests", srcs: ["src/**/*.java"], static_libs: [ - "android-support-test", + "androidx.test.rules", "apct-perftests-utils", "ActivityManagerPerfTestsUtils", ], diff --git a/tests/ActivityManagerPerfTests/tests/AndroidManifest.xml b/tests/ActivityManagerPerfTests/tests/AndroidManifest.xml index a1ab33a96248..04aef47419d0 100644 --- a/tests/ActivityManagerPerfTests/tests/AndroidManifest.xml +++ b/tests/ActivityManagerPerfTests/tests/AndroidManifest.xml @@ -25,6 +25,6 @@ <uses-library android:name="android.test.runner" /> </application> - <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner" + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" android:targetPackage="com.android.frameworks.perftests.amtests"/> </manifest> diff --git a/tests/ActivityManagerPerfTests/tests/AndroidTest.xml b/tests/ActivityManagerPerfTests/tests/AndroidTest.xml index ffb5404d7d94..76c40b2e3dc6 100644 --- a/tests/ActivityManagerPerfTests/tests/AndroidTest.xml +++ b/tests/ActivityManagerPerfTests/tests/AndroidTest.xml @@ -24,6 +24,6 @@ <option name="test-tag" value="ActivityManagerPerfTests"/> <test class="com.android.tradefed.testtype.AndroidJUnitTest"> <option name="package" value="com.android.frameworks.perftests.amtests"/> - <option name="runner" value="android.support.test.runner.AndroidJUnitRunner"/> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner"/> </test> </configuration>
\ No newline at end of file diff --git a/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/tests/BasePerfTest.java b/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/tests/BasePerfTest.java index 58fb136ae9b3..daff76f4f522 100644 --- a/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/tests/BasePerfTest.java +++ b/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/tests/BasePerfTest.java @@ -21,7 +21,8 @@ import android.content.Intent; import android.content.ServiceConnection; import android.perftests.utils.ManualBenchmarkState; import android.perftests.utils.PerfManualStatusReporter; -import android.support.test.InstrumentationRegistry; + +import androidx.test.InstrumentationRegistry; import com.android.frameworks.perftests.am.util.TargetPackageUtils; import com.android.frameworks.perftests.am.util.TimeReceiver; diff --git a/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/tests/BroadcastPerfTest.java b/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/tests/BroadcastPerfTest.java index f7dab03f10ee..bc528d4d4fb7 100644 --- a/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/tests/BroadcastPerfTest.java +++ b/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/tests/BroadcastPerfTest.java @@ -17,8 +17,9 @@ package com.android.frameworks.perftests.am.tests; import android.content.Intent; -import android.support.test.filters.LargeTest; -import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; import com.android.frameworks.perftests.am.util.Constants; diff --git a/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/tests/ContentProviderPerfTest.java b/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/tests/ContentProviderPerfTest.java index 3bf56ce8b085..8e8221954ad6 100644 --- a/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/tests/ContentProviderPerfTest.java +++ b/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/tests/ContentProviderPerfTest.java @@ -17,8 +17,9 @@ package com.android.frameworks.perftests.am.tests; import android.content.ContentProviderClient; -import android.support.test.filters.LargeTest; -import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; import com.android.frameworks.perftests.am.util.TargetPackageUtils; diff --git a/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/tests/ServiceBindPerfTest.java b/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/tests/ServiceBindPerfTest.java index e1263db61b8b..996c5a5c5584 100644 --- a/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/tests/ServiceBindPerfTest.java +++ b/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/tests/ServiceBindPerfTest.java @@ -21,8 +21,9 @@ import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; -import android.support.test.filters.LargeTest; -import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; import com.android.frameworks.perftests.am.util.Constants; import com.android.frameworks.perftests.am.util.TargetPackageUtils; diff --git a/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/tests/ServiceStartPerfTest.java b/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/tests/ServiceStartPerfTest.java index f05f32382e53..ba2064005937 100644 --- a/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/tests/ServiceStartPerfTest.java +++ b/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/tests/ServiceStartPerfTest.java @@ -19,8 +19,9 @@ package com.android.frameworks.perftests.am.tests; import android.content.ComponentName; import android.content.Intent; import android.content.ServiceConnection; -import android.support.test.filters.LargeTest; -import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; import com.android.frameworks.perftests.am.util.Constants; import com.android.frameworks.perftests.am.util.TargetPackageUtils; diff --git a/tests/ActivityManagerPerfTests/utils/Android.bp b/tests/ActivityManagerPerfTests/utils/Android.bp index c052656346cc..300b7ea998fa 100644 --- a/tests/ActivityManagerPerfTests/utils/Android.bp +++ b/tests/ActivityManagerPerfTests/utils/Android.bp @@ -20,7 +20,7 @@ java_test { "src/com/android/frameworks/perftests/am/util/ITimeReceiverCallback.aidl", ], static_libs: [ - "android-support-test", + "androidx.test.rules", "junit", "ub-uiautomator", ], diff --git a/tests/ActivityManagerPerfTests/utils/src/com/android/frameworks/perftests/am/util/Utils.java b/tests/ActivityManagerPerfTests/utils/src/com/android/frameworks/perftests/am/util/Utils.java index 67071d204eff..fc787bafa93a 100644 --- a/tests/ActivityManagerPerfTests/utils/src/com/android/frameworks/perftests/am/util/Utils.java +++ b/tests/ActivityManagerPerfTests/utils/src/com/android/frameworks/perftests/am/util/Utils.java @@ -19,10 +19,11 @@ package com.android.frameworks.perftests.am.util; import android.content.Intent; import android.os.RemoteException; import android.os.ResultReceiver; -import android.support.test.InstrumentationRegistry; import android.support.test.uiautomator.UiDevice; import android.util.Log; +import androidx.test.InstrumentationRegistry; + import java.io.IOException; public class Utils { diff --git a/tests/ActivityTests/AndroidManifest.xml b/tests/ActivityTests/AndroidManifest.xml index b381cbfbd0a9..0b3bd70ba5c9 100644 --- a/tests/ActivityTests/AndroidManifest.xml +++ b/tests/ActivityTests/AndroidManifest.xml @@ -79,6 +79,7 @@ android:singleUser="true" android:exported="true" /> <receiver android:name="TrackTimeReceiver" /> <receiver android:name="AlarmSpamReceiver" /> + <receiver android:name="SlowReceiver" /> <activity android:name="DisableScreenshotsActivity" android:label="DisableScreenshots" android:theme="@style/DisableScreenshots"> diff --git a/tests/ActivityTests/src/com/google/android/test/activity/ActivityTestMain.java b/tests/ActivityTests/src/com/google/android/test/activity/ActivityTestMain.java index 0f4960887a33..07b3a97817a3 100644 --- a/tests/ActivityTests/src/com/google/android/test/activity/ActivityTestMain.java +++ b/tests/ActivityTests/src/com/google/android/test/activity/ActivityTestMain.java @@ -16,21 +16,21 @@ package com.google.android.test.activity; -import java.util.ArrayList; -import java.util.List; - import android.app.Activity; import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.AlarmManager; import android.app.AlertDialog; import android.app.PendingIntent; -import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentProviderClient; +import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; +import android.content.pm.UserInfo; +import android.content.res.Configuration; +import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Bundle; @@ -42,21 +42,18 @@ import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; -import android.graphics.Bitmap; import android.provider.Settings; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; -import android.widget.ScrollView; -import android.widget.Toast; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.content.Context; -import android.content.pm.UserInfo; -import android.content.res.Configuration; -import android.util.Log; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; +import android.widget.Toast; +import java.util.ArrayList; +import java.util.List; public class ActivityTestMain extends Activity { static final String TAG = "ActivityTest"; @@ -73,8 +70,14 @@ public class ActivityTestMain extends Activity { ServiceConnection mIsolatedConnection; + static final String SLOW_RECEIVER_ACTION = "com.google.android.test.activity.SLOW_ACTION"; + static final String SLOW_RECEIVER_EXTRA = "slow_ordinal"; + static final int MSG_SPAM = 1; static final int MSG_SPAM_ALARM = 2; + static final int MSG_SLOW_RECEIVER = 3; + static final int MSG_SLOW_ALARM_RECEIVER = 4; + static final int MSG_REPLACE_BROADCAST = 5; final Handler mHandler = new Handler() { @Override @@ -100,11 +103,72 @@ public class ActivityTestMain extends Activity { mAlarm.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME, when+(30*1000), pi); scheduleSpamAlarm(30*1000); } break; + case MSG_SLOW_RECEIVER: { + // Several back to back, to illustrate dispatch policy + Intent intent = new Intent(ActivityTestMain.this, SlowReceiver.class); + intent.setAction(SLOW_RECEIVER_ACTION); + intent.putExtra(SLOW_RECEIVER_EXTRA, 1); + sendOrderedBroadcast(intent, null, mSlowReceiverCompletion, mHandler, + Activity.RESULT_OK, null, null); + intent.putExtra(SLOW_RECEIVER_EXTRA, 2); + sendOrderedBroadcast(intent, null, mSlowReceiverCompletion, mHandler, + Activity.RESULT_OK, null, null); + intent.putExtra(SLOW_RECEIVER_EXTRA, 3); + sendOrderedBroadcast(intent, null, mSlowReceiverCompletion, mHandler, + Activity.RESULT_OK, null, null); + } break; + case MSG_SLOW_ALARM_RECEIVER: { + // Several back to back, to illustrate dispatch policy + Intent intent = new Intent(ActivityTestMain.this, SlowReceiver.class); + intent.setAction(SLOW_RECEIVER_ACTION); + intent.putExtra(SLOW_RECEIVER_EXTRA, 1); + sendOrderedBroadcast(intent, null, mSlowReceiverCompletion, mHandler, + Activity.RESULT_OK, null, null); + intent.putExtra(SLOW_RECEIVER_EXTRA, 2); + sendOrderedBroadcast(intent, null, mSlowReceiverCompletion, mHandler, + Activity.RESULT_OK, null, null); + intent.putExtra(SLOW_RECEIVER_EXTRA, 3); + sendOrderedBroadcast(intent, null, mSlowReceiverCompletion, mHandler, + Activity.RESULT_OK, null, null); + + // Also send a broadcast alarm to evaluate the alarm fast-forward policy + intent.putExtra(SLOW_RECEIVER_EXTRA, 4); + PendingIntent pi = PendingIntent.getBroadcast(ActivityTestMain.this, 1, intent, 0); + AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + long now = SystemClock.elapsedRealtime(); + Log.i(TAG, "Setting alarm for now + 5 seconds"); + am.setExact(AlarmManager.ELAPSED_REALTIME, now + 5_000, pi); + } break; + case MSG_REPLACE_BROADCAST: { + Intent intent = new Intent(ActivityTestMain.this, SlowReceiver.class); + intent.setAction(SLOW_RECEIVER_ACTION); + intent.putExtra(SLOW_RECEIVER_EXTRA, 1); + sendOrderedBroadcast(intent, null, mSlowReceiverCompletion, mHandler, + Activity.RESULT_OK, null, null); + intent.putExtra(SLOW_RECEIVER_EXTRA, 2); + sendOrderedBroadcast(intent, null, mSlowReceiverCompletion, mHandler, + Activity.RESULT_OK, null, null); + intent.putExtra(SLOW_RECEIVER_EXTRA, 5038); + intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + sendOrderedBroadcast(intent, null, mSlowReceiverCompletion, mHandler, + Activity.RESULT_OK, null, null); + } break; } super.handleMessage(msg); } }; + final BroadcastReceiver mSlowReceiverCompletion = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final int extra = intent.getIntExtra(SLOW_RECEIVER_EXTRA, -1); + final String msg = "Slow receiver " + extra + " completed"; + Toast.makeText(ActivityTestMain.this, msg, Toast.LENGTH_LONG) + .show(); + Log.i(TAG, msg); + } + }; + class BroadcastResultReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { @@ -369,6 +433,20 @@ public class ActivityTestMain extends Activity { return true; } }); + menu.add("Replace broadcast").setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { + @Override public boolean onMenuItemClick(MenuItem item) { + scheduleReplaceBroadcast(); + return true; + } + }); + menu.add("Require unknown permission").setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { + @Override public boolean onMenuItemClick(MenuItem item) { + final Intent intent = new Intent(SLOW_RECEIVER_ACTION); + intent.putExtra(SLOW_RECEIVER_EXTRA, 5038); + sendOrderedBroadcast(intent, "com.google.android.test.activity.permission.UNDEFINED"); + return true; + } + }); menu.add("Stack Doc").setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { ActivityManager.AppTask task = findDocTask(); @@ -387,6 +465,18 @@ public class ActivityTestMain extends Activity { return true; } }); + menu.add("Slow receiver").setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { + @Override public boolean onMenuItemClick(MenuItem item) { + scheduleSlowReceiver(); + return true; + } + }); + menu.add("Slow alarm receiver").setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { + @Override public boolean onMenuItemClick(MenuItem item) { + scheduleSlowAlarmReceiver(); + return true; + } + }); menu.add("Spam!").setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { scheduleSpam(false); @@ -469,6 +559,7 @@ public class ActivityTestMain extends Activity { protected void onStop() { super.onStop(); mHandler.removeMessages(MSG_SPAM_ALARM); + mHandler.removeMessages(MSG_SLOW_RECEIVER); for (ServiceConnection conn : mConnections) { unbindService(conn); } @@ -544,6 +635,21 @@ public class ActivityTestMain extends Activity { mHandler.sendMessageDelayed(msg, delay); } + void scheduleSlowReceiver() { + mHandler.removeMessages(MSG_SLOW_RECEIVER); + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SLOW_RECEIVER), 500); + } + + void scheduleSlowAlarmReceiver() { + mHandler.removeMessages(MSG_SLOW_ALARM_RECEIVER); + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SLOW_ALARM_RECEIVER), 500); + } + + void scheduleReplaceBroadcast() { + mHandler.removeMessages(MSG_REPLACE_BROADCAST); + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_REPLACE_BROADCAST), 500); + } + private View scrollWrap(View view) { ScrollView scroller = new ScrollView(this); scroller.addView(view, new ScrollView.LayoutParams(ScrollView.LayoutParams.MATCH_PARENT, diff --git a/tests/ActivityTests/src/com/google/android/test/activity/SlowReceiver.java b/tests/ActivityTests/src/com/google/android/test/activity/SlowReceiver.java new file mode 100644 index 000000000000..0437a289741c --- /dev/null +++ b/tests/ActivityTests/src/com/google/android/test/activity/SlowReceiver.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.test.activity; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.SystemClock; +import android.util.Log; + +public class SlowReceiver extends BroadcastReceiver { + private static final String TAG = "SlowReceiver"; + private static final long RECEIVER_DELAY = 6_000; + + @Override + public void onReceive(Context context, Intent intent) { + final int extra = intent.getIntExtra(ActivityTestMain.SLOW_RECEIVER_EXTRA, -1); + if (extra == 1) { + Log.i(TAG, "Received broadcast 1; delaying return by " + RECEIVER_DELAY + " ms"); + long now = SystemClock.elapsedRealtime(); + final long end = now + RECEIVER_DELAY; + while (now < end) { + try { + Thread.sleep(end - now); + } catch (InterruptedException e) { } + now = SystemClock.elapsedRealtime(); + } + } else { + Log.i(TAG, "Extra parameter not 1, returning immediately"); + } + Log.i(TAG, "Returning from onReceive()"); + } +} diff --git a/tests/ActivityViewTest/Android.bp b/tests/ActivityViewTest/Android.bp new file mode 100644 index 000000000000..e7b8c8e1d058 --- /dev/null +++ b/tests/ActivityViewTest/Android.bp @@ -0,0 +1,6 @@ +android_test { + name: "ActivityViewTest", + srcs: ["src/**/*.java"], + platform_apis: true, + certificate: "platform", +} diff --git a/tests/ActivityViewTest/AndroidManifest.xml b/tests/ActivityViewTest/AndroidManifest.xml new file mode 100644 index 000000000000..17eb029166f0 --- /dev/null +++ b/tests/ActivityViewTest/AndroidManifest.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.google.android.test.activityview"> + <uses-permission android:name="android.permission.INJECT_EVENTS"/> + <uses-permission android:name="android.permission.MANAGE_ACTIVITY_STACKS"/> + <uses-permission android:name="android.permission.ACTIVITY_EMBEDDING"/> + <uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW"/> + + <uses-sdk android:targetSdkVersion="27"/> + <application android:label="ActivityViewTest"> + <activity android:name=".ActivityViewMainActivity" + android:label="AV Main" + android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + <category android:name="android.intent.category.DEFAULT"/> + </intent-filter> + </activity> + + <activity android:name=".ActivityViewActivity" + android:label="AV" + android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density" + android:windowSoftInputMode="stateHidden|adjustResize"> + </activity> + + <activity android:name=".ActivityViewResizeActivity" + android:label="AV Resize" + android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density" + android:windowSoftInputMode="stateHidden|adjustResize"> + </activity> + + <activity android:name=".ActivityViewScrollActivity" + android:label="AV Scroll" + android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density" + android:windowSoftInputMode="stateHidden"> + </activity> + + <activity android:name=".ActivityViewTestActivity" + android:resizeableActivity="true" + android:theme="@*android:style/Theme.NoTitleBar" + android:exported="true" + android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density"> + </activity> + + <activity android:name=".ActivityViewVisibilityActivity" + android:label="AV Visibility" + android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|colorMode|density"> + </activity> + </application> +</manifest> diff --git a/tests/ActivityViewTest/res/layout/activity_view_activity.xml b/tests/ActivityViewTest/res/layout/activity_view_activity.xml new file mode 100644 index 000000000000..67c01f8c78fe --- /dev/null +++ b/tests/ActivityViewTest/res/layout/activity_view_activity.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="#cfd8dc"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <Button + android:id="@+id/activity_launch_button" + android:layout_width="200dp" + android:layout_height="wrap_content" + android:text="Launch test activity" /> + + <Button + android:id="@+id/activity_pick_launch_button" + android:layout_width="200dp" + android:layout_height="wrap_content" + android:text="Launch from picker" /> + + </LinearLayout> + + <ActivityView + android:id="@+id/activity_view" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + +</LinearLayout>
\ No newline at end of file diff --git a/tests/ActivityViewTest/res/layout/activity_view_main_activity.xml b/tests/ActivityViewTest/res/layout/activity_view_main_activity.xml new file mode 100644 index 000000000000..efcaef679a9c --- /dev/null +++ b/tests/ActivityViewTest/res/layout/activity_view_main_activity.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <Button + android:id="@+id/activity_view_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Test ActivityView" + android:textAllCaps="false"/> + + <Button + android:id="@+id/scroll_activity_view_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Test Scroll ActivityView" + android:textAllCaps="false"/> + + <Button + android:id="@+id/resize_activity_view_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Test Resize ActivityView" + android:textAllCaps="false"/> + + <Button + android:id="@+id/visibility_activity_view_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Test ActivityView Visibility" + android:textAllCaps="false"/> +</LinearLayout> diff --git a/tests/ActivityViewTest/res/layout/activity_view_resize_activity.xml b/tests/ActivityViewTest/res/layout/activity_view_resize_activity.xml new file mode 100644 index 000000000000..18d86e3d5a6f --- /dev/null +++ b/tests/ActivityViewTest/res/layout/activity_view_resize_activity.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="#cfd8dc"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <Button + android:id="@+id/activity_launch_button" + android:layout_width="100dp" + android:layout_height="wrap_content" + android:text="Launch" /> + + <Button + android:id="@+id/activity_resize_button" + android:layout_width="100dp" + android:layout_height="wrap_content" + android:text="Resize" /> + </LinearLayout> + + <SeekBar + android:id="@+id/activity_view_seek_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + <ActivityView + android:id="@+id/activity_view" + android:layout_width="match_parent" + android:layout_height="600dp" /> + +</LinearLayout>
\ No newline at end of file diff --git a/tests/ActivityViewTest/res/layout/activity_view_scroll_activity.xml b/tests/ActivityViewTest/res/layout/activity_view_scroll_activity.xml new file mode 100644 index 000000000000..879c2c20a082 --- /dev/null +++ b/tests/ActivityViewTest/res/layout/activity_view_scroll_activity.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" android:layout_width="match_parent" + android:layout_height="match_parent"> + + <Button + android:id="@+id/activity_launch_button" + android:layout_width="100dp" + android:layout_height="wrap_content" + android:text="Launch" /> + + <ScrollView + android:id="@+id/activity_view_host_scroll_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:color="#cfd8dc"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" > + + <View + android:layout_width="match_parent" + android:layout_height="300dp" + android:layout_gravity="center_horizontal" + android:background="#eeeeee" /> + + <ActivityView + android:id="@+id/activity_view" + android:layout_width="match_parent" + android:layout_height="300dp" + android:background="#fce4ec" /> + + <View + android:layout_width="match_parent" + android:layout_height="300dp" + android:layout_gravity="center_horizontal" + android:background="#eeeeee" /> + </LinearLayout> + </ScrollView> +</LinearLayout>
\ No newline at end of file diff --git a/tests/ActivityViewTest/res/layout/activity_view_test_activity.xml b/tests/ActivityViewTest/res/layout/activity_view_test_activity.xml new file mode 100644 index 000000000000..338d68adfafb --- /dev/null +++ b/tests/ActivityViewTest/res/layout/activity_view_test_activity.xml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/test_activity_root" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="#ffe0b2"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_centerInParent="true" + android:orientation="vertical" + android:background="#00000000" > + <TextView + android:id="@+id/test_activity_title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textColor="@android:color/black" + android:background="#00000000" + android:gravity="center" /> + <TextView + android:id="@+id/test_activity_touch_state" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textColor="@android:color/black" + android:background="#00000000" + android:gravity="center" /> + </LinearLayout> + + <TextView + android:id="@+id/test_activity_width_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textColor="@android:color/black" + android:background="#00000000" + android:gravity="center" /> + + <TextView + android:id="@+id/test_activity_height_text" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_alignParentRight="true" + android:layout_alignParentEnd="true" + android:textColor="@android:color/black" + android:background="#00000000" + android:gravity="center" /> + + <EditText + android:id="@+id/test_activity_edittext" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_margin="16dp" /> +</RelativeLayout>
\ No newline at end of file diff --git a/tests/ActivityViewTest/res/layout/activity_view_visibility_activity.xml b/tests/ActivityViewTest/res/layout/activity_view_visibility_activity.xml new file mode 100644 index 000000000000..d29d4dfc0428 --- /dev/null +++ b/tests/ActivityViewTest/res/layout/activity_view_visibility_activity.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="#cfd8dc"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <Button + android:id="@+id/activity_launch_button" + android:layout_width="200dp" + android:layout_height="wrap_content" + android:text="Launch test activity" /> + + <Spinner + android:id="@+id/visibility_spinner" + android:layout_width="200dp" + android:layout_height="match_parent"/> + + </LinearLayout> + + <ActivityView + android:id="@+id/activity_view" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + +</LinearLayout> diff --git a/tests/ActivityViewTest/src/com/google/android/test/activityview/ActivityViewActivity.java b/tests/ActivityViewTest/src/com/google/android/test/activityview/ActivityViewActivity.java new file mode 100644 index 000000000000..f7c60fc73cb3 --- /dev/null +++ b/tests/ActivityViewTest/src/com/google/android/test/activityview/ActivityViewActivity.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.test.activityview; + +import android.app.Activity; +import android.app.ActivityView; +import android.content.Intent; +import android.os.Bundle; +import android.os.Parcelable; +import android.widget.Button; + +public class ActivityViewActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_view_activity); + + final ActivityView activityView = findViewById(R.id.activity_view); + final Button launchButton = findViewById(R.id.activity_launch_button); + launchButton.setOnClickListener(v -> { + final Intent intent = new Intent(this, ActivityViewTestActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + activityView.startActivity(intent); + }); + final Button pickActivityLaunchButton = findViewById(R.id.activity_pick_launch_button); + pickActivityLaunchButton.setOnClickListener(v -> { + final Intent intent = Intent.makeMainActivity(null); + final Intent chooser = Intent.createChooser(intent, + "Pick an app to launch in ActivityView"); + chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Parcelable[] { + new Intent(Intent.ACTION_MAIN) + .addCategory("com.android.internal.category.PLATLOGO") + }); + if (intent.resolveActivity(getPackageManager()) != null) { + chooser.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + activityView.startActivity(chooser); + } + }); + } +} diff --git a/tests/ActivityViewTest/src/com/google/android/test/activityview/ActivityViewMainActivity.java b/tests/ActivityViewTest/src/com/google/android/test/activityview/ActivityViewMainActivity.java new file mode 100644 index 000000000000..4f09c28fe711 --- /dev/null +++ b/tests/ActivityViewTest/src/com/google/android/test/activityview/ActivityViewMainActivity.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.test.activityview; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +public class ActivityViewMainActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_view_main_activity); + + findViewById(R.id.activity_view_button).setOnClickListener( + v -> startActivity(new Intent(this, ActivityViewActivity.class))); + + findViewById(R.id.scroll_activity_view_button).setOnClickListener( + v -> startActivity(new Intent(this, ActivityViewScrollActivity.class))); + + findViewById(R.id.resize_activity_view_button).setOnClickListener( + v -> startActivity(new Intent(this, ActivityViewResizeActivity.class))); + + findViewById(R.id.visibility_activity_view_button).setOnClickListener( + v -> startActivity(new Intent(this, ActivityViewVisibilityActivity.class))); + } +} diff --git a/tests/ActivityViewTest/src/com/google/android/test/activityview/ActivityViewResizeActivity.java b/tests/ActivityViewTest/src/com/google/android/test/activityview/ActivityViewResizeActivity.java new file mode 100644 index 000000000000..8860a771fd5a --- /dev/null +++ b/tests/ActivityViewTest/src/com/google/android/test/activityview/ActivityViewResizeActivity.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.test.activityview; + +import android.app.Activity; +import android.app.ActivityView; +import android.content.Intent; +import android.os.Bundle; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.SeekBar; + +public class ActivityViewResizeActivity extends Activity { + private static final int SMALL_SIZE = 600; + private static final int LARGE_SIZE = 1200; + + private ActivityView mActivityView; + + private boolean mFlipSize; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_view_resize_activity); + + mActivityView = findViewById(R.id.activity_view); + + final Button launchButton = findViewById(R.id.activity_launch_button); + launchButton.setOnClickListener(v -> { + final Intent intent = new Intent(this, ActivityViewTestActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + mActivityView.startActivity(intent); + }); + final Button resizeButton = findViewById(R.id.activity_resize_button); + if (resizeButton != null) { + resizeButton.setOnClickListener(v -> { + LinearLayout.LayoutParams params = + (LinearLayout.LayoutParams) mActivityView.getLayoutParams(); + params.height = mFlipSize ? SMALL_SIZE : LARGE_SIZE; + mFlipSize = !mFlipSize; + mActivityView.setLayoutParams(params); + }); + } + final SeekBar seekBar = findViewById(R.id.activity_view_seek_bar); + if (seekBar != null) { + seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + final LinearLayout.LayoutParams params = + (LinearLayout.LayoutParams) mActivityView.getLayoutParams(); + params.height = SMALL_SIZE + progress * 10; + mActivityView.setLayoutParams(params); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + } + } +} diff --git a/tests/ActivityViewTest/src/com/google/android/test/activityview/ActivityViewScrollActivity.java b/tests/ActivityViewTest/src/com/google/android/test/activityview/ActivityViewScrollActivity.java new file mode 100644 index 000000000000..56543662675c --- /dev/null +++ b/tests/ActivityViewTest/src/com/google/android/test/activityview/ActivityViewScrollActivity.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.test.activityview; + +import android.app.Activity; +import android.app.ActivityView; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; + +public class ActivityViewScrollActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_view_scroll_activity); + + final ActivityView activityView = findViewById(R.id.activity_view); + final Button launchButton = findViewById(R.id.activity_launch_button); + launchButton.setOnClickListener(v -> { + final Intent intent = new Intent(this, ActivityViewTestActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + activityView.startActivity(intent); + }); + findViewById(R.id.activity_view_host_scroll_view).setOnScrollChangeListener( + (View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) + -> activityView.onLocationChanged()); + } +} diff --git a/tests/ActivityViewTest/src/com/google/android/test/activityview/ActivityViewTestActivity.java b/tests/ActivityViewTest/src/com/google/android/test/activityview/ActivityViewTestActivity.java new file mode 100644 index 000000000000..52aba2b13c42 --- /dev/null +++ b/tests/ActivityViewTest/src/com/google/android/test/activityview/ActivityViewTestActivity.java @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.test.activityview; + +import static android.view.MotionEvent.ACTION_CANCEL; +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_MOVE; +import static android.view.MotionEvent.ACTION_UP; + +import android.app.Activity; +import android.content.res.Configuration; +import android.os.Bundle; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewTreeObserver; +import android.widget.TextView; + +public class ActivityViewTestActivity extends Activity { + private static final String TAG = "ActivityViewTestActivity"; + + private View mRoot; + private TextView mTextView; + private TextView mWidthTextView; + private TextView mHeightTextView; + private TextView mTouchStateTextView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_view_test_activity); + mRoot = findViewById(R.id.test_activity_root); + mTextView = findViewById(R.id.test_activity_title); + mWidthTextView = findViewById(R.id.test_activity_width_text); + mHeightTextView = findViewById(R.id.test_activity_height_text); + mTouchStateTextView = findViewById(R.id.test_activity_touch_state); + ViewTreeObserver viewTreeObserver = mRoot.getViewTreeObserver(); + if (viewTreeObserver.isAlive()) { + viewTreeObserver.addOnGlobalLayoutListener(this::updateDimensionTexts); + } + updateStateText("CREATED"); + } + + @Override + protected void onStart() { + super.onStart(); + updateStateText("STARTED"); + } + + @Override + protected void onResume() { + super.onResume(); + updateStateText("RESUMED"); + } + + @Override + protected void onPause() { + super.onPause(); + updateStateText("PAUSED"); + } + + @Override + protected void onStop() { + super.onStop(); + updateStateText("STOPPED"); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + updateDimensionTexts(); + } + + private void updateStateText(String state) { + Log.d(TAG, state); + mTextView.setText(state); + } + + private void updateDimensionTexts() { + mWidthTextView.setText("" + mRoot.getWidth()); + mHeightTextView.setText("" + mRoot.getHeight()); + } + + private void updateTouchState(MotionEvent event) { + switch (event.getAction()) { + case ACTION_DOWN: + case ACTION_MOVE: + mTouchStateTextView.setText("[" + event.getX() + "," + event.getY() + "]"); + break; + case ACTION_UP: + case ACTION_CANCEL: + mTouchStateTextView.setText(""); + break; + } + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + updateTouchState(event); + return super.dispatchTouchEvent(event); + } +} diff --git a/tests/ActivityViewTest/src/com/google/android/test/activityview/ActivityViewVisibilityActivity.java b/tests/ActivityViewTest/src/com/google/android/test/activityview/ActivityViewVisibilityActivity.java new file mode 100644 index 000000000000..ecd2cf3c578e --- /dev/null +++ b/tests/ActivityViewTest/src/com/google/android/test/activityview/ActivityViewVisibilityActivity.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.test.activityview; + +import static android.view.View.GONE; +import static android.view.View.INVISIBLE; +import static android.view.View.VISIBLE; + +import android.app.Activity; +import android.app.ActivityView; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.Spinner; + +public class ActivityViewVisibilityActivity extends Activity { + private static final String[] sVisibilityOptions = {"VISIBLE", "INVISIBLE", "GONE"}; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_view_visibility_activity); + + final ActivityView activityView = findViewById(R.id.activity_view); + final Button launchButton = findViewById(R.id.activity_launch_button); + launchButton.setOnClickListener(v -> { + final Intent intent = new Intent(this, ActivityViewTestActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + activityView.startActivity(intent); + }); + + final Spinner visibilitySpinner = findViewById(R.id.visibility_spinner); + final ArrayAdapter<String> adapter = new ArrayAdapter<>(this, + android.R.layout.simple_spinner_item, sVisibilityOptions); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + visibilitySpinner.setAdapter(adapter); + visibilitySpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + switch (position) { + case 0: + activityView.setVisibility(VISIBLE); + break; + case 1: + activityView.setVisibility(INVISIBLE); + break; + case 2: + activityView.setVisibility(GONE); + break; + } + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + } + }); + } +} diff --git a/tests/AppLaunch/Android.bp b/tests/AppLaunch/Android.bp index 0f07da61bc91..f90f26f00e6d 100644 --- a/tests/AppLaunch/Android.bp +++ b/tests/AppLaunch/Android.bp @@ -8,6 +8,6 @@ android_test { "android.test.base", "android.test.runner", ], - static_libs: ["android-support-test"], + static_libs: ["androidx.test.rules"], test_suites: ["device-tests"], } diff --git a/tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java b/tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java index 26de0b3f6fcd..95b8f6700c76 100644 --- a/tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java +++ b/tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java @@ -30,16 +30,16 @@ import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.UserHandle; -import android.support.test.rule.logging.AtraceLogger; import android.test.InstrumentationTestCase; import android.test.InstrumentationTestRunner; import android.util.Log; +import androidx.test.rule.logging.AtraceLogger; + import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -52,6 +52,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; + /** * This test is intended to measure the time it takes for the apps to start. * Names of the applications are passed in command line, and the @@ -77,6 +78,7 @@ public class AppLaunch extends InstrumentationTestCase { private static final String KEY_SIMPLEPERF_CMD = "simpleperf_cmd"; private static final String KEY_SIMPLEPERF_APP = "simpleperf_app"; private static final String KEY_CYCLE_CLEAN = "cycle_clean"; + private static final String KEY_TRACE_ALL = "trace_all"; private static final String KEY_TRACE_ITERATIONS = "trace_iterations"; private static final String KEY_LAUNCH_DIRECTORY = "launch_directory"; private static final String KEY_TRACE_DIRECTORY = "trace_directory"; @@ -110,7 +112,7 @@ public class AppLaunch extends InstrumentationTestCase { private static final String SUCCESS_MESSAGE = "Status: ok"; private static final String TOTAL_TIME_MESSAGE = "TotalTime:"; private static final String COMPILE_SUCCESS = "Success"; - private static final String LAUNCH_ITERATION = "LAUNCH_ITERATION - %d"; + private static final String LAUNCH_ITERATION = "LAUNCH_ITERATION-%d"; private static final String TRACE_ITERATION = "TRACE_ITERATION-%d"; private static final String LAUNCH_ITERATION_PREFIX = "LAUNCH_ITERATION"; private static final String TRACE_ITERATION_PREFIX = "TRACE_ITERATION"; @@ -141,6 +143,7 @@ public class AppLaunch extends InstrumentationTestCase { private String[] mCompilerFilters = null; private String mLastAppName = ""; private boolean mCycleCleanUp = false; + private boolean mTraceAll = false; private boolean mIterationCycle = false; private long mCycleTime = 0; private StringBuilder mCycleTimes = new StringBuilder(); @@ -185,7 +188,8 @@ public class AppLaunch extends InstrumentationTestCase { if (null != launchDirectory && !launchDirectory.isEmpty()) { launchRootDir = new File(launchDirectory); if (!launchRootDir.exists() && !launchRootDir.mkdirs()) { - throw new IOException("Unable to create the destination directory"); + throw new IOException("Unable to create the destination directory " + + launchRootDir + ". Try disabling selinux."); } } @@ -193,7 +197,8 @@ public class AppLaunch extends InstrumentationTestCase { File launchSubDir = new File(launchRootDir, LAUNCH_SUB_DIRECTORY); if (!launchSubDir.exists() && !launchSubDir.mkdirs()) { - throw new IOException("Unable to create the lauch file sub directory"); + throw new IOException("Unable to create the lauch file sub directory " + + launchSubDir + ". Try disabling selinux."); } File file = new File(launchSubDir, LAUNCH_FILE); FileOutputStream outputStream = new FileOutputStream(file); @@ -293,18 +298,40 @@ public class AppLaunch extends InstrumentationTestCase { // skip if the app has failures while launched first continue; } - // In the "applaunch.txt" file app launches are referenced using - // "LAUNCH_ITERATION - ITERATION NUM" - launchResults = startApp(launch.getApp(), launch.getLaunchReason()); - if (launchResults.mLaunchTime < 0) { - addLaunchResult(launch, new AppLaunchResult()); - // if it fails once, skip the rest of the launches - continue; - } else { - mCycleTime += launchResults.mLaunchTime; - addLaunchResult(launch, launchResults); + AtraceLogger atraceLogger = null; + if (mTraceAll) { + Log.i(TAG, "Started tracing " + launch.getApp()); + atraceLogger = AtraceLogger + .getAtraceLoggerInstance(getInstrumentation()); } - sleep(POST_LAUNCH_IDLE_TIMEOUT); + try { + // Start the trace + if (atraceLogger != null) { + atraceLogger.atraceStart(traceCategoriesSet, traceBufferSize, + traceDumpInterval, rootTraceSubDir, + String.format("%s-%s-%s", launch.getApp(), + launch.getCompilerFilter(), launch.getLaunchReason())); + } + // In the "applaunch.txt" file app launches are referenced using + // "LAUNCH_ITERATION - ITERATION NUM" + launchResults = startApp(launch.getApp(), launch.getLaunchReason()); + if (launchResults.mLaunchTime < 0) { + addLaunchResult(launch, new AppLaunchResult()); + // if it fails once, skip the rest of the launches + continue; + } else { + mCycleTime += launchResults.mLaunchTime; + addLaunchResult(launch, launchResults); + } + sleep(POST_LAUNCH_IDLE_TIMEOUT); + } finally { + // Stop the trace + if (atraceLogger != null) { + Log.i(TAG, "Stopped tracing " + launch.getApp()); + atraceLogger.atraceStop(); + } + } + } // App launch times for trace launch will not be used for final @@ -531,6 +558,7 @@ public class AppLaunch extends InstrumentationTestCase { mLaunchOrder = args.getString(KEY_LAUNCH_ORDER, LAUNCH_ORDER_CYCLIC); mSimplePerfAppOnly = Boolean.parseBoolean(args.getString(KEY_SIMPLEPERF_APP)); mCycleCleanUp = Boolean.parseBoolean(args.getString(KEY_CYCLE_CLEAN)); + mTraceAll = Boolean.parseBoolean(args.getString(KEY_TRACE_ALL)); mTrialLaunch = mTrialLaunch || Boolean.parseBoolean(args.getString(KEY_TRIAL_LAUNCH)); if (mSimplePerfCmd != null && mSimplePerfAppOnly) { @@ -795,6 +823,8 @@ public class AppLaunch extends InstrumentationTestCase { BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stream))) { String cmd = String.format(SIMPLEPERF_APP_CMD, packageName, launchCmd); + // In the file, we need to escape any "$". + cmd = cmd.replace("$", "\\$"); writer.write(cmd); } launchCmd = launchFile.getAbsolutePath(); @@ -836,6 +866,7 @@ public class AppLaunch extends InstrumentationTestCase { /* SAMPLE OUTPUT : Cold launch Starting: Intent { cmp=com.google.android.calculator/com.android.calculator2.Calculator } Status: ok + LaunchState: COLD Activity: com.google.android.calculator/com.android.calculator2.Calculator TotalTime: 357 WaitTime: 377 @@ -844,6 +875,7 @@ public class AppLaunch extends InstrumentationTestCase { Starting: Intent { cmp=com.google.android.calculator/com.android.calculator2.Calculator } Warning: Activity not started, its current task has been brought to the front Status: ok + LaunchState: HOT Activity: com.google.android.calculator/com.android.calculator2.CalculatorGoogle TotalTime: 60 WaitTime: 67 diff --git a/tests/AppLaunchWear/Android.bp b/tests/AppLaunchWear/Android.bp index 6a8b38200700..8d34b6eb9c0f 100644 --- a/tests/AppLaunchWear/Android.bp +++ b/tests/AppLaunchWear/Android.bp @@ -8,6 +8,6 @@ android_test { "android.test.base", "android.test.runner", ], - static_libs: ["android-support-test"], + static_libs: ["androidx.test.rules"], test_suites: ["device-tests"], } diff --git a/tests/AppLaunchWear/src/com/android/tests/applaunch/AppLaunch.java b/tests/AppLaunchWear/src/com/android/tests/applaunch/AppLaunch.java index d36d84e8f51d..97701c61011e 100644 --- a/tests/AppLaunchWear/src/com/android/tests/applaunch/AppLaunch.java +++ b/tests/AppLaunchWear/src/com/android/tests/applaunch/AppLaunch.java @@ -30,16 +30,16 @@ import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.UserHandle; -import android.support.test.rule.logging.AtraceLogger; import android.test.InstrumentationTestCase; import android.test.InstrumentationTestRunner; import android.util.Log; +import androidx.test.rule.logging.AtraceLogger; + import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -52,6 +52,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; + /** * This test is intended to measure the time it takes for the apps to start. * Names of the applications are passed in command line, and the diff --git a/tests/Camera2Tests/CameraToo/tests/Android.mk b/tests/Camera2Tests/CameraToo/tests/Android.mk index eb8f6c306862..fe4dc42aa7d9 100644 --- a/tests/Camera2Tests/CameraToo/tests/Android.mk +++ b/tests/Camera2Tests/CameraToo/tests/Android.mk @@ -20,6 +20,6 @@ LOCAL_PACKAGE_NAME := CameraTooTests LOCAL_INSTRUMENTATION_FOR := CameraToo LOCAL_SDK_VERSION := current LOCAL_SRC_FILES := $(call all-java-files-under,src) -LOCAL_STATIC_JAVA_LIBRARIES := android-support-test mockito-target-minus-junit4 +LOCAL_STATIC_JAVA_LIBRARIES := androidx.test.rules mockito-target-minus-junit4 include $(BUILD_PACKAGE) diff --git a/tests/Camera2Tests/CameraToo/tests/AndroidManifest.xml b/tests/Camera2Tests/CameraToo/tests/AndroidManifest.xml index 30210bae5cd1..8d3574929ca8 100644 --- a/tests/Camera2Tests/CameraToo/tests/AndroidManifest.xml +++ b/tests/Camera2Tests/CameraToo/tests/AndroidManifest.xml @@ -23,7 +23,7 @@ <application android:label="CameraToo"> <uses-library android:name="android.test.runner" /> </application> - <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner" + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" android:targetPackage="com.example.android.camera2.cameratoo" android:label="CameraToo tests" /> </manifest> diff --git a/tests/Compatibility/Android.bp b/tests/Compatibility/Android.bp index a0c37619841f..4ca406eba3cf 100644 --- a/tests/Compatibility/Android.bp +++ b/tests/Compatibility/Android.bp @@ -14,7 +14,7 @@ android_test { name: "AppCompatibilityTest", - static_libs: ["android-support-test"], + static_libs: ["androidx.test.rules"], // Include all test java files. srcs: ["src/**/*.java"], platform_apis: true, diff --git a/tests/Compatibility/src/com/android/compatibilitytest/AppCompatibility.java b/tests/Compatibility/src/com/android/compatibilitytest/AppCompatibility.java index 95eb5c9e7770..d683ec746a61 100644 --- a/tests/Compatibility/src/com/android/compatibilitytest/AppCompatibility.java +++ b/tests/Compatibility/src/com/android/compatibilitytest/AppCompatibility.java @@ -33,10 +33,11 @@ import android.os.Bundle; import android.os.DropBoxManager; import android.os.RemoteException; import android.os.ServiceManager; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; import android.util.Log; +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + import org.junit.After; import org.junit.Assert; import org.junit.Before; diff --git a/tests/Compatibility/src/com/android/compatibilitytest/AppCompatibilityRunner.java b/tests/Compatibility/src/com/android/compatibilitytest/AppCompatibilityRunner.java index b61ec346f041..960ea4966958 100644 --- a/tests/Compatibility/src/com/android/compatibilitytest/AppCompatibilityRunner.java +++ b/tests/Compatibility/src/com/android/compatibilitytest/AppCompatibilityRunner.java @@ -16,7 +16,7 @@ package com.android.compatibilitytest; -import android.support.test.runner.AndroidJUnitRunner; +import androidx.test.runner.AndroidJUnitRunner; // empty subclass to maintain backwards compatibility on host-side harness public class AppCompatibilityRunner extends AndroidJUnitRunner {} diff --git a/tests/DexLoggerIntegrationTests/Android.mk b/tests/DexLoggerIntegrationTests/Android.mk deleted file mode 100644 index ee2ec0a80b03..000000000000 --- a/tests/DexLoggerIntegrationTests/Android.mk +++ /dev/null @@ -1,50 +0,0 @@ -# -# Copyright 2017 The Android Open Source Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -LOCAL_PATH:= $(call my-dir) - -# Build a tiny library that the test app can dynamically load - -include $(CLEAR_VARS) - -LOCAL_MODULE_TAGS := tests -LOCAL_MODULE := DexLoggerTestLibrary -LOCAL_SRC_FILES := $(call all-java-files-under, src/com/android/dcl) - -include $(BUILD_JAVA_LIBRARY) - -dexloggertest_jar := $(LOCAL_BUILT_MODULE) - - -# Build the test app itself - -include $(CLEAR_VARS) - -LOCAL_MODULE_TAGS := tests -LOCAL_PACKAGE_NAME := DexLoggerIntegrationTests -LOCAL_SDK_VERSION := current -LOCAL_COMPATIBILITY_SUITE := device-tests -LOCAL_CERTIFICATE := platform -LOCAL_SRC_FILES := $(call all-java-files-under, src/com/android/server/pm) - -LOCAL_STATIC_JAVA_LIBRARIES := \ - android-support-test \ - truth-prebuilt \ - -# This gets us the javalib.jar built by DexLoggerTestLibrary above. -LOCAL_JAVA_RESOURCE_FILES := $(dexloggertest_jar) - -include $(BUILD_PACKAGE) diff --git a/tests/DexLoggerIntegrationTests/src/com/android/server/pm/dex/DexLoggerIntegrationTests.java b/tests/DexLoggerIntegrationTests/src/com/android/server/pm/dex/DexLoggerIntegrationTests.java deleted file mode 100644 index 460022e2c83b..000000000000 --- a/tests/DexLoggerIntegrationTests/src/com/android/server/pm/dex/DexLoggerIntegrationTests.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.pm.dex; - -import static com.google.common.truth.Truth.assertThat; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.filters.LargeTest; -import android.util.EventLog; -import dalvik.system.DexClassLoader; - -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.OutputStream; -import java.security.MessageDigest; -import java.util.ArrayList; -import java.util.Formatter; -import java.util.List; - -/** - * Integration tests for {@link com.android.server.pm.dex.DexLogger}. - * - * The setup for the test dynamically loads code in a jar extracted - * from our assets (a secondary dex file). - * - * We then use adb to trigger secondary dex file reconcilation (and - * wait for it to complete). As a side-effect of this DexLogger should - * be notified of the file and should log the hash of the file's name - * and content. We verify that this message appears in the event log. - * - * Run with "atest DexLoggerIntegrationTests". - */ -@LargeTest -@RunWith(JUnit4.class) -public final class DexLoggerIntegrationTests { - - private static final String PACKAGE_NAME = "com.android.frameworks.dexloggertest"; - - // Event log tag used for SNET related events - private static final int SNET_TAG = 0x534e4554; - // Subtag used to distinguish dynamic code loading events - private static final String DCL_SUBTAG = "dcl"; - - // Obtained via "echo -n copied.jar | sha256sum" - private static final String EXPECTED_NAME_HASH = - "1B6C71DB26F36582867432CCA12FB6A517470C9F9AABE9198DD4C5C030D6DC0C"; - - private static String expectedContentHash; - - @BeforeClass - public static void setUpAll() throws Exception { - Context context = InstrumentationRegistry.getTargetContext(); - MessageDigest hasher = MessageDigest.getInstance("SHA-256"); - - // Copy the jar from our Java resources to a private data directory - File privateCopy = new File(context.getDir("jars", Context.MODE_PRIVATE), "copied.jar"); - Class<?> thisClass = DexLoggerIntegrationTests.class; - try (InputStream input = thisClass.getResourceAsStream("/javalib.jar"); - OutputStream output = new FileOutputStream(privateCopy)) { - byte[] buffer = new byte[1024]; - while (true) { - int numRead = input.read(buffer); - if (numRead < 0) { - break; - } - output.write(buffer, 0, numRead); - hasher.update(buffer, 0, numRead); - } - } - - // Remember the SHA-256 of the file content to check that it is the same as - // the value we see logged. - Formatter formatter = new Formatter(); - for (byte b : hasher.digest()) { - formatter.format("%02X", b); - } - expectedContentHash = formatter.toString(); - - // Feed the jar to a class loader and make sure it contains what we expect. - ClassLoader loader = - new DexClassLoader( - privateCopy.toString(), null, null, context.getClass().getClassLoader()); - loader.loadClass("com.android.dcl.Simple"); - } - - @Test - @Ignore // Should invoke shell command via UiAutomation: b/137574238 - public void testDexLoggerReconcileGeneratesEvents() throws Exception { - int[] tagList = new int[] { SNET_TAG }; - List<EventLog.Event> events = new ArrayList<>(); - - // There may already be events in the event log - figure out the most recent one - EventLog.readEvents(tagList, events); - long previousEventNanos = - events.isEmpty() ? 0 : events.get(events.size() - 1).getTimeNanos(); - events.clear(); - - Process process = Runtime.getRuntime().exec( - "cmd package reconcile-secondary-dex-files " + PACKAGE_NAME); - int exitCode = process.waitFor(); - assertThat(exitCode).isEqualTo(0); - - int myUid = android.os.Process.myUid(); - String expectedMessage = EXPECTED_NAME_HASH + " " + expectedContentHash; - - EventLog.readEvents(tagList, events); - boolean found = false; - for (EventLog.Event event : events) { - if (event.getTimeNanos() <= previousEventNanos) { - continue; - } - Object[] data = (Object[]) event.getData(); - - // We only care about DCL events that we generated. - String subTag = (String) data[0]; - if (!DCL_SUBTAG.equals(subTag)) { - continue; - } - int uid = (int) data[1]; - if (uid != myUid) { - continue; - } - - String message = (String) data[2]; - assertThat(message).isEqualTo(expectedMessage); - found = true; - } - - assertThat(found).isTrue(); - } -} diff --git a/tests/DynamicCodeLoggerIntegrationTests/Android.mk b/tests/DynamicCodeLoggerIntegrationTests/Android.mk new file mode 100644 index 000000000000..62c1ba89653c --- /dev/null +++ b/tests/DynamicCodeLoggerIntegrationTests/Android.mk @@ -0,0 +1,84 @@ +# +# Copyright 2017 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +LOCAL_PATH:= $(call my-dir) + +# Build a tiny library that the test app can dynamically load + +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests +LOCAL_MODULE := DynamicCodeLoggerTestLibrary +LOCAL_SRC_FILES := $(call all-java-files-under, src/com/android/dcl) + +include $(BUILD_JAVA_LIBRARY) + +dynamiccodeloggertest_jar := $(LOCAL_BUILT_MODULE) + + +# Also build a native library that the test app can dynamically load + +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests +LOCAL_MODULE := DynamicCodeLoggerNativeTestLibrary +LOCAL_SRC_FILES := src/cpp/com_android_dcl_Jni.cpp +LOCAL_C_INCLUDES += \ + $(JNI_H_INCLUDE) +LOCAL_SDK_VERSION := 28 +LOCAL_NDK_STL_VARIANT := c++_static + +include $(BUILD_SHARED_LIBRARY) + +# And a standalone native executable that we can exec. + +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests +LOCAL_MODULE := DynamicCodeLoggerNativeExecutable +LOCAL_SRC_FILES := src/cpp/test_executable.cpp + +include $(BUILD_EXECUTABLE) + +dynamiccodeloggertest_executable := $(LOCAL_BUILT_MODULE) + +# Build the test app itself + +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests +LOCAL_PACKAGE_NAME := DynamicCodeLoggerIntegrationTests +LOCAL_SDK_VERSION := current +LOCAL_COMPATIBILITY_SUITE := device-tests +LOCAL_CERTIFICATE := shared +LOCAL_SRC_FILES := $(call all-java-files-under, src/com/android/server/pm) + +LOCAL_STATIC_JAVA_LIBRARIES := \ + androidx.test.rules \ + truth-prebuilt \ + +# Include both versions of the .so if we have 2 arch +LOCAL_MULTILIB := both +LOCAL_JNI_SHARED_LIBRARIES := \ + DynamicCodeLoggerNativeTestLibrary \ + +# This gets us the javalib.jar built by DynamicCodeLoggerTestLibrary above as well as the various +# native binaries. +LOCAL_JAVA_RESOURCE_FILES := \ + $(dynamiccodeloggertest_jar) \ + $(dynamiccodeloggertest_executable) \ + +include $(BUILD_PACKAGE) diff --git a/tests/DexLoggerIntegrationTests/AndroidManifest.xml b/tests/DynamicCodeLoggerIntegrationTests/AndroidManifest.xml index a9f01edbdc41..08fac300512b 100644 --- a/tests/DexLoggerIntegrationTests/AndroidManifest.xml +++ b/tests/DynamicCodeLoggerIntegrationTests/AndroidManifest.xml @@ -15,7 +15,7 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.frameworks.dexloggertest"> + package="com.android.frameworks.dynamiccodeloggertest"> <!-- Tests feature introduced in P (28) --> <uses-sdk @@ -29,7 +29,7 @@ </application> <instrumentation - android:name="android.support.test.runner.AndroidJUnitRunner" - android:targetPackage="com.android.frameworks.dexloggertest" - android:label="Integration test for DexLogger" /> + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.frameworks.dynamiccodeloggertest" + android:label="Integration test for DynamicCodeLogger" /> </manifest> diff --git a/tests/DexLoggerIntegrationTests/AndroidTest.xml b/tests/DynamicCodeLoggerIntegrationTests/AndroidTest.xml index fb1bef6da08a..f8a1ec90a78a 100644 --- a/tests/DexLoggerIntegrationTests/AndroidTest.xml +++ b/tests/DynamicCodeLoggerIntegrationTests/AndroidTest.xml @@ -13,18 +13,18 @@ See the License for the specific language governing permissions and limitations under the License. --> -<configuration description="Runs DexLogger Integration Tests"> +<configuration description="Runs DynamicLogger Integration Tests"> <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> - <option name="test-file-name" value="DexLoggerIntegrationTests.apk"/> + <option name="test-file-name" value="DynamicCodeLoggerIntegrationTests.apk"/> <option name="cleanup-apks" value="true"/> </target_preparer> <option name="test-suite-tag" value="apct"/> - <option name="test-tag" value="DexLoggerIntegrationTests"/> + <option name="test-tag" value="DynamicCodeLoggerIntegrationTests"/> <test class="com.android.tradefed.testtype.AndroidJUnitTest"> - <option name="package" value="com.android.frameworks.dexloggertest"/> - <option name="runner" value="android.support.test.runner.AndroidJUnitRunner"/> + <option name="package" value="com.android.frameworks.dynamiccodeloggertest"/> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner"/> <option name="hidden-api-checks" value="false"/> </test> </configuration> diff --git a/tests/DexLoggerIntegrationTests/src/com/android/dcl/Simple.java b/tests/DynamicCodeLoggerIntegrationTests/src/com/android/dcl/Simple.java index e995a26ea5c9..e995a26ea5c9 100644 --- a/tests/DexLoggerIntegrationTests/src/com/android/dcl/Simple.java +++ b/tests/DynamicCodeLoggerIntegrationTests/src/com/android/dcl/Simple.java diff --git a/tests/DynamicCodeLoggerIntegrationTests/src/com/android/server/pm/dex/DynamicCodeLoggerIntegrationTests.java b/tests/DynamicCodeLoggerIntegrationTests/src/com/android/server/pm/dex/DynamicCodeLoggerIntegrationTests.java new file mode 100644 index 000000000000..db2f659c655b --- /dev/null +++ b/tests/DynamicCodeLoggerIntegrationTests/src/com/android/server/pm/dex/DynamicCodeLoggerIntegrationTests.java @@ -0,0 +1,526 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm.dex; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import android.app.UiAutomation; +import android.content.Context; +import android.os.Build; +import android.os.ParcelFileDescriptor; +import android.os.SystemClock; +import android.util.EventLog; +import android.util.EventLog.Event; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.LargeTest; + +import dalvik.system.DexClassLoader; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Formatter; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Integration tests for {@link DynamicCodeLogger}. + * + * The setup for the test dynamically loads code in a jar extracted + * from our assets (a secondary dex file). + * + * We then use shell commands to trigger dynamic code logging (and wait + * for it to complete). This causes DynamicCodeLogger to log the hash of the + * file's name and content. We verify that this message appears in + * the event log. + * + * Run with "atest DynamicCodeLoggerIntegrationTests". + */ +@LargeTest +@RunWith(JUnit4.class) +public final class DynamicCodeLoggerIntegrationTests { + + private static final String SHA_256 = "SHA-256"; + + // Event log tag used for SNET related events + private static final int SNET_TAG = 0x534e4554; + + // Subtags used to distinguish dynamic code loading events + private static final String DCL_DEX_SUBTAG = "dcl"; + private static final String DCL_NATIVE_SUBTAG = "dcln"; + + // These are job IDs from DynamicCodeLoggingService + private static final int IDLE_LOGGING_JOB_ID = 2030028; + private static final int AUDIT_WATCHING_JOB_ID = 203142925; + + // For tests that rely on parsing audit logs, how often to retry. (There are many reasons why + // we might not see the audit logs, including throttling and delays in log generation, so to + // avoid flakiness we run these tests multiple times, allowing progressively longer between + // code loading and checking the logs on each try.) + private static final int AUDIT_LOG_RETRIES = 10; + private static final int RETRY_DELAY_MS = 2_000; + + private static Context sContext; + private static int sMyUid; + + @BeforeClass + public static void setUpAll() { + sContext = InstrumentationRegistry.getTargetContext(); + sMyUid = android.os.Process.myUid(); + } + + @Before + public void primeEventLog() { + // Force a round trip to logd to make sure everything is up to date. + // Without this the first test passes and others don't - we don't see new events in the + // log. The exact reason is unclear. + EventLog.writeEvent(SNET_TAG, "Dummy event"); + + // Audit log messages are throttled by the kernel (at the request of logd) to 5 per + // second, so running the tests too quickly in sequence means we lose some and get + // spurious failures. Sigh. + SystemClock.sleep(1000); + } + + @Test + public void testGeneratesEvents_standardClassLoader() throws Exception { + File privateCopyFile = privateFile("copied.jar"); + // Obtained via "echo -n copied.jar | sha256sum" + String expectedNameHash = + "1B6C71DB26F36582867432CCA12FB6A517470C9F9AABE9198DD4C5C030D6DC0C"; + String expectedContentHash = copyAndHashResource("/javalib.jar", privateCopyFile); + + // Feed the jar to a class loader and make sure it contains what we expect. + ClassLoader parentClassLoader = sContext.getClass().getClassLoader(); + ClassLoader loader = + new DexClassLoader(privateCopyFile.toString(), null, null, parentClassLoader); + loader.loadClass("com.android.dcl.Simple"); + + // And make sure we log events about it + long previousEventNanos = mostRecentEventTimeNanos(); + runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID); + + assertDclLoggedSince(previousEventNanos, DCL_DEX_SUBTAG, + expectedNameHash, expectedContentHash); + } + + @Test + public void testGeneratesEvents_unknownClassLoader() throws Exception { + File privateCopyFile = privateFile("copied2.jar"); + String expectedNameHash = + "202158B6A3169D78F1722487205A6B036B3F2F5653FDCFB4E74710611AC7EB93"; + String expectedContentHash = copyAndHashResource("/javalib.jar", privateCopyFile); + + // This time make sure an unknown class loader is an ancestor of the class loader we use. + ClassLoader knownClassLoader = sContext.getClass().getClassLoader(); + ClassLoader unknownClassLoader = new UnknownClassLoader(knownClassLoader); + ClassLoader loader = + new DexClassLoader(privateCopyFile.toString(), null, null, unknownClassLoader); + loader.loadClass("com.android.dcl.Simple"); + + // And make sure we log events about it + long previousEventNanos = mostRecentEventTimeNanos(); + runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID); + + assertDclLoggedSince(previousEventNanos, DCL_DEX_SUBTAG, + expectedNameHash, expectedContentHash); + } + + @Test + public void testGeneratesEvents_nativeLibrary() throws Exception { + new TestNativeCodeWithRetries() { + @Override + protected void loadNativeCode(int tryNumber) throws Exception { + // We need to use a different file name for each retry, because once a file is + // loaded, re-loading it has no effect. + String privateCopyName = "copied" + tryNumber + ".so"; + File privateCopyFile = privateFile(privateCopyName); + mExpectedNameHash = hashOf(privateCopyName); + mExpectedContentHash = copyAndHashResource( + libraryPath("DynamicCodeLoggerNativeTestLibrary.so"), privateCopyFile); + + System.load(privateCopyFile.toString()); + } + }.runTest(); + } + + @Test + public void testGeneratesEvents_nativeLibrary_escapedName() throws Exception { + new TestNativeCodeWithRetries() { + @Override + protected void loadNativeCode(int tryNumber) throws Exception { + // A file name with a space will be escaped in the audit log; verify we un-escape it + // correctly. + String privateCopyName = "second copy " + tryNumber + ".so"; + File privateCopyFile = privateFile(privateCopyName); + mExpectedNameHash = hashOf(privateCopyName); + mExpectedContentHash = copyAndHashResource( + libraryPath("DynamicCodeLoggerNativeTestLibrary.so"), privateCopyFile); + + System.load(privateCopyFile.toString()); + } + }.runTest(); + } + + @Test + public void testGeneratesEvents_nativeExecutable() throws Exception { + new TestNativeCodeWithRetries() { + @Override + protected void loadNativeCode(int tryNumber) throws Exception { + String privateCopyName = "test_executable" + tryNumber; + File privateCopyFile = privateFile(privateCopyName); + mExpectedNameHash = hashOf(privateCopyName); + mExpectedContentHash = copyAndHashResource( + "/DynamicCodeLoggerNativeExecutable", privateCopyFile); + assertThat(privateCopyFile.setExecutable(true)).isTrue(); + + Process process = Runtime.getRuntime().exec(privateCopyFile.toString()); + int exitCode = process.waitFor(); + assertThat(exitCode).isEqualTo(0); + } + }.runTest(); + } + + @Test + public void testGeneratesEvents_spoofed_validFile() throws Exception { + File privateCopyFile = privateFile("spoofed"); + + String expectedContentHash = copyAndHashResource( + "/DynamicCodeLoggerNativeExecutable", privateCopyFile); + + EventLog.writeEvent(EventLog.getTagCode("auditd"), + "type=1400 avc: granted { execute_no_trans } " + + "path=\"" + privateCopyFile + "\" " + + "scontext=u:r:untrusted_app_27: " + + "tcontext=u:object_r:app_data_file: " + + "tclass=file "); + + String expectedNameHash = + "1CF36F503A02877BB775DC23C1C5A47A95F2684B6A1A83B11795B856D88861E3"; + + // Run the job to scan generated audit log entries + runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID); + + // And then make sure we log events about it + long previousEventNanos = mostRecentEventTimeNanos(); + runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID); + + assertDclLoggedSince(previousEventNanos, DCL_NATIVE_SUBTAG, + expectedNameHash, expectedContentHash); + } + + @Test + public void testGeneratesEvents_spoofed_validFile_untrustedApp() throws Exception { + File privateCopyFile = privateFile("spoofed2"); + + String expectedContentHash = copyAndHashResource( + "/DynamicCodeLoggerNativeExecutable", privateCopyFile); + + EventLog.writeEvent(EventLog.getTagCode("auditd"), + "type=1400 avc: granted { execute_no_trans } " + + "path=\"" + privateCopyFile + "\" " + + "scontext=u:r:untrusted_app: " + + "tcontext=u:object_r:app_data_file: " + + "tclass=file "); + + String expectedNameHash = + "3E57AA59249154C391316FDCF07C1D499C26A564E4D305833CCD9A98ED895AC9"; + + // Run the job to scan generated audit log entries + runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID); + + // And then make sure we log events about it + long previousEventNanos = mostRecentEventTimeNanos(); + runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID); + + assertDclLoggedSince(previousEventNanos, DCL_NATIVE_SUBTAG, + expectedNameHash, expectedContentHash); + } + + @Test + public void testGeneratesEvents_spoofed_pathTraversal() throws Exception { + File privateDir = privateFile("x").getParentFile(); + + // Transform /a/b/c -> /a/b/c/../../.. so we get back to the root + File pathTraversalToRoot = privateDir; + File root = new File("/"); + while (!privateDir.equals(root)) { + pathTraversalToRoot = new File(pathTraversalToRoot, ".."); + privateDir = privateDir.getParentFile(); + } + + File spoofedFile = new File(pathTraversalToRoot, "dev/urandom"); + + assertWithMessage("Expected " + spoofedFile + " to be readable") + .that(spoofedFile.canRead()).isTrue(); + + EventLog.writeEvent(EventLog.getTagCode("auditd"), + "type=1400 avc: granted { execute_no_trans } " + + "path=\"" + spoofedFile + "\" " + + "scontext=u:r:untrusted_app_27: " + + "tcontext=u:object_r:app_data_file: " + + "tclass=file "); + + String expectedNameHash = + "65528FE876BD676B0DFCC9A8ACA8988E026766F99EEC1E1FB48F46B2F635E225"; + + // Run the job to scan generated audit log entries + runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID); + + // And then trigger generating DCL events + long previousEventNanos = mostRecentEventTimeNanos(); + runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID); + + assertNoDclLoggedSince(previousEventNanos, DCL_NATIVE_SUBTAG, expectedNameHash); + } + + @Test + public void testGeneratesEvents_spoofed_otherAppFile() throws Exception { + File ourPath = sContext.getDatabasePath("android_pay"); + File targetPath = new File(ourPath.toString() + .replace("com.android.frameworks.dynamiccodeloggertest", "com.google.android.gms")); + + assertWithMessage("Expected " + targetPath + " to not be readable") + .that(targetPath.canRead()).isFalse(); + + EventLog.writeEvent(EventLog.getTagCode("auditd"), + "type=1400 avc: granted { execute_no_trans } " + + "path=\"" + targetPath + "\" " + + "scontext=u:r:untrusted_app_27: " + + "tcontext=u:object_r:app_data_file: " + + "tclass=file "); + + String expectedNameHash = + "CBE04E8AB9E7199FC19CBAAF9C774B88E56B3B19E823F2251693380AD6F515E6"; + + // Run the job to scan generated audit log entries + runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID); + + // And then trigger generating DCL events + long previousEventNanos = mostRecentEventTimeNanos(); + runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID); + + assertNoDclLoggedSince(previousEventNanos, DCL_NATIVE_SUBTAG, expectedNameHash); + } + + // Abstract out the logic for running a native code loading test multiple times if needed and + // leaving time for audit messages to reach the log. + private abstract class TestNativeCodeWithRetries { + String mExpectedContentHash; + String mExpectedNameHash; + + abstract void loadNativeCode(int tryNumber) throws Exception; + + final void runTest() throws Exception { + List<String> messages = null; + + for (int i = 0; i < AUDIT_LOG_RETRIES; i++) { + loadNativeCode(i); + + SystemClock.sleep(i * RETRY_DELAY_MS); + + // Run the job to scan generated audit log entries + runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID); + + // And then make sure we log events about it + long previousEventNanos = mostRecentEventTimeNanos(); + runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID); + + messages = findMatchingEvents( + previousEventNanos, DCL_NATIVE_SUBTAG, mExpectedNameHash); + if (!messages.isEmpty()) { + break; + } + } + + assertHasDclLog(messages, mExpectedContentHash); + } + } + + private static File privateFile(String name) { + return new File(sContext.getDir("dcl", Context.MODE_PRIVATE), name); + } + + private String libraryPath(final String libraryName) { + // This may be deprecated. but it tells us the ABI of this process which is exactly what we + // want. + return "/lib/" + Build.CPU_ABI + "/" + libraryName; + } + + private static String copyAndHashResource(String resourcePath, File copyTo) throws Exception { + MessageDigest hasher = MessageDigest.getInstance(SHA_256); + + // Copy the jar from our Java resources to a private data directory + Class<?> thisClass = DynamicCodeLoggerIntegrationTests.class; + try (InputStream input = thisClass.getResourceAsStream(resourcePath); + OutputStream output = new FileOutputStream(copyTo)) { + byte[] buffer = new byte[1024]; + while (true) { + int numRead = input.read(buffer); + if (numRead < 0) { + break; + } + output.write(buffer, 0, numRead); + hasher.update(buffer, 0, numRead); + } + } + + // Compute the SHA-256 of the file content so we can check that it is the same as the value + // we see logged. + return toHexString(hasher); + } + + private String hashOf(String input) throws Exception { + MessageDigest hasher = MessageDigest.getInstance(SHA_256); + hasher.update(input.getBytes()); + return toHexString(hasher); + } + + private static String toHexString(MessageDigest hasher) { + Formatter formatter = new Formatter(); + for (byte b : hasher.digest()) { + formatter.format("%02X", b); + } + + return formatter.toString(); + } + + private static void runDynamicCodeLoggingJob(int jobId) throws Exception { + // This forces the DynamicCodeLoggingService job to start now. + runCommand("cmd jobscheduler run -f android " + jobId); + // Wait for the job to have run. + long startTime = SystemClock.elapsedRealtime(); + while (true) { + String response = runCommand( + "cmd jobscheduler get-job-state android " + jobId); + if (!response.contains("pending") && !response.contains("active")) { + break; + } + // Don't wait forever - if it's taken > 10s then something is very wrong. + if (SystemClock.elapsedRealtime() - startTime > TimeUnit.SECONDS.toMillis(10)) { + throw new AssertionError("Job has not completed: " + response); + } + SystemClock.sleep(100); + } + } + + private static String runCommand(String command) throws Exception { + ByteArrayOutputStream response = new ByteArrayOutputStream(); + byte[] buffer = new byte[1000]; + UiAutomation ui = InstrumentationRegistry.getInstrumentation().getUiAutomation(); + ParcelFileDescriptor fd = ui.executeShellCommand(command); + try (InputStream input = new ParcelFileDescriptor.AutoCloseInputStream(fd)) { + while (true) { + int count = input.read(buffer); + if (count == -1) { + break; + } + response.write(buffer, 0, count); + } + } + return response.toString("UTF-8"); + } + + private static long mostRecentEventTimeNanos() throws Exception { + List<Event> events = readSnetEvents(); + return events.isEmpty() ? 0 : events.get(events.size() - 1).getTimeNanos(); + } + + private static void assertDclLoggedSince(long previousEventNanos, String expectedSubTag, + String expectedNameHash, String expectedContentHash) throws Exception { + List<String> messages = + findMatchingEvents(previousEventNanos, expectedSubTag, expectedNameHash); + + assertHasDclLog(messages, expectedContentHash); + } + + private static void assertHasDclLog(List<String> messages, String expectedContentHash) { + assertWithMessage("Expected exactly one matching log entry").that(messages).hasSize(1); + assertThat(messages.get(0)).endsWith(expectedContentHash); + } + + private static void assertNoDclLoggedSince(long previousEventNanos, String expectedSubTag, + String expectedNameHash) throws Exception { + List<String> messages = + findMatchingEvents(previousEventNanos, expectedSubTag, expectedNameHash); + + assertWithMessage("Expected no matching log entries").that(messages).isEmpty(); + } + + private static List<String> findMatchingEvents(long previousEventNanos, String expectedSubTag, + String expectedNameHash) throws Exception { + List<String> messages = new ArrayList<>(); + + for (Event event : readSnetEvents()) { + if (event.getTimeNanos() <= previousEventNanos) { + continue; + } + + Object data = event.getData(); + if (!(data instanceof Object[])) { + continue; + } + Object[] fields = (Object[]) data; + + // We only care about DCL events that we generated. + String subTag = (String) fields[0]; + if (!expectedSubTag.equals(subTag)) { + continue; + } + int uid = (int) fields[1]; + if (uid != sMyUid) { + continue; + } + + String message = (String) fields[2]; + if (!message.startsWith(expectedNameHash)) { + continue; + } + + messages.add(message); + //assertThat(message).endsWith(expectedContentHash); + } + return messages; + } + + private static List<Event> readSnetEvents() throws Exception { + List<Event> events = new ArrayList<>(); + EventLog.readEvents(new int[] { SNET_TAG }, events); + return events; + } + + /** + * A class loader that does nothing useful, but importantly doesn't extend BaseDexClassLoader. + */ + private static class UnknownClassLoader extends ClassLoader { + UnknownClassLoader(ClassLoader parent) { + super(parent); + } + } +} diff --git a/tests/ImfTest/res/values/config.xml b/tests/DynamicCodeLoggerIntegrationTests/src/cpp/com_android_dcl_Jni.cpp index 5ae40a381523..060888310b51 100644 --- a/tests/ImfTest/res/values/config.xml +++ b/tests/DynamicCodeLoggerIntegrationTests/src/cpp/com_android_dcl_Jni.cpp @@ -1,21 +1,22 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * Copyright (c) 2009, The Android Open Source Project +/* + * Copyright 2019 The Android Open Source Project * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and * limitations under the License. */ ---> -<resources> - <bool name="def_expect_ime_autopop">false</bool> -</resources> + +#include "jni.h" + +extern "C" jint JNI_OnLoad(JavaVM* /* vm */, void* /* reserved */) +{ + return JNI_VERSION_1_6; +} diff --git a/tests/DynamicCodeLoggerIntegrationTests/src/cpp/test_executable.cpp b/tests/DynamicCodeLoggerIntegrationTests/src/cpp/test_executable.cpp new file mode 100644 index 000000000000..ad025e696dec --- /dev/null +++ b/tests/DynamicCodeLoggerIntegrationTests/src/cpp/test_executable.cpp @@ -0,0 +1,20 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +int main() { + // This program just has to run, it doesn't need to do anything. So we don't. + return 0; +} diff --git a/tests/FlickerTests/Android.bp b/tests/FlickerTests/Android.bp new file mode 100644 index 000000000000..05f0a8e7921d --- /dev/null +++ b/tests/FlickerTests/Android.bp @@ -0,0 +1,30 @@ +// +// Copyright (C) 2018 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +android_test { + name: "FlickerTests", + srcs: ["src/**/*.java"], + platform_apis: true, + certificate: "platform", + test_suites: ["device-tests"], + libs: ["android.test.runner"], + static_libs: [ + "flickertestapplib", + "flickerlib", + "truth-prebuilt", + "app-helpers-core", + ], +} diff --git a/tests/FlickerTests/AndroidManifest.xml b/tests/FlickerTests/AndroidManifest.xml new file mode 100644 index 000000000000..5b1a36b84cc4 --- /dev/null +++ b/tests/FlickerTests/AndroidManifest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.server.wm.flicker"> + + <uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27"/> + <!-- Read and write traces from external storage --> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> + <!-- Capture screen contents --> + <uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER" /> + <!-- Run layers trace --> + <uses-permission android:name="android.permission.HARDWARE_TEST"/> + <application> + <uses-library android:name="android.test.runner"/> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.server.wm.flicker" + android:label="WindowManager Flicker Tests"> + </instrumentation> +</manifest>
\ No newline at end of file diff --git a/tests/FlickerTests/AndroidTest.xml b/tests/FlickerTests/AndroidTest.xml new file mode 100644 index 000000000000..e36f97656f2a --- /dev/null +++ b/tests/FlickerTests/AndroidTest.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + * Copyright 2018 Google Inc. All Rights Reserved. + --> +<configuration description="Runs WindowManager Flicker Tests"> + <option name="test-tag" value="FlickerTests" /> + <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> + <!-- keeps the screen on during tests --> + <option name="screen-always-on" value="on" /> + <!-- prevents the phone from restarting --> + <option name="force-skip-system-props" value="true" /> + </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true"/> + <option name="test-file-name" value="FlickerTests.apk"/> + <option name="test-file-name" value="FlickerTestApp.apk" /> + </target_preparer> + <test class="com.android.tradefed.testtype.AndroidJUnitTest"> + <option name="package" value="com.android.server.wm.flicker"/> + <option name="exclude-annotation" value="org.junit.Ignore" /> + <option name="shell-timeout" value="6600s" /> + <option name="test-timeout" value="6000s" /> + <option name="hidden-api-checks" value="false" /> + </test> + <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> + <option name="directory-keys" value="/sdcard/flicker" /> + <option name="collect-on-run-ended-only" value="true" /> + </metrics_collector> +</configuration> diff --git a/tests/FlickerTests/README.md b/tests/FlickerTests/README.md new file mode 100644 index 000000000000..a7c9e20e0a07 --- /dev/null +++ b/tests/FlickerTests/README.md @@ -0,0 +1,146 @@ +# Flicker Test Library + +## Motivation +Detect *flicker* — any discontinuous, or unpredictable behavior seen during UI transitions that is not due to performance. This is often the result of a logic error in the code and difficult to identify because the issue is transient and at times difficult to reproduce. This library helps create integration tests between `SurfaceFlinger`, `WindowManager` and `SystemUI` to identify flicker. + +## Adding a Test +The library builds and runs UI transitions, captures Winscope traces and exposes common assertions that can be tested against each trace. + +### Building Transitions +Start by defining common or error prone transitions using `TransitionRunner`. +```java +// Example: Build a transition that cold launches an app from launcher +TransitionRunner transition = TransitionRunner.newBuilder() + // Specify a tag to identify the transition (optional) + .withTag("OpenAppCold_" + testApp.getLauncherName()) + + // Specify preconditions to setup the device + // Wake up device and go to home screen + .runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen) + + // Setup transition under test + // Press the home button and close the app to test a cold start + .runBefore(device::pressHome) + .runBefore(testApp::exit) + + // Run the transition under test + // Open the app and wait for UI to be idle + // This is the part of the transition that will be tested. + .run(testApp::open) + .run(device::waitForIdle) + + // Perform any tear downs + // Close the app + .runAfterAll(testApp::exit) + + // Number of times to repeat the transition to catch any flaky issues + .repeat(5); +``` + + +Run the transition to get a list of `TransitionResult` for each time the transition is repeated. +```java + List<TransitionResult> results = transition.run(); +``` +`TransitionResult` contains paths to test artifacts such as Winscope traces and screen recordings. + + +### Checking Assertions +Each `TransitionResult` can be tested using an extension of the Google Truth library, `LayersTraceSubject` and `WmTraceSubject`. They try to balance test principles set out by Google Truth (not supporting nested assertions, keeping assertions simple) with providing support for common assertion use cases. + +Each trace can be represented as a ordered collection of trace entries, with an associated timestamp. Each trace entry has common assertion checks. The trace subjects expose methods to filter the range of entries and test for changing assertions. + +```java + TransitionResult result = results.get(0); + Rect displayBounds = getDisplayBounds(); + + // check all trace entries + assertThat(result).coversRegion(displayBounds).forAllEntries(); + + // check a range of entries + assertThat(result).coversRegion(displayBounds).forRange(startTime, endTime); + + // check first entry + assertThat(result).coversRegion(displayBounds).inTheBeginning(); + + // check last entry + assertThat(result).coversRegion(displayBounds).atTheEnd(); + + // check a change in assertions, e.g. wallpaper window is visible, + // then wallpaper window becomes and stays invisible + assertThat(result) + .showsBelowAppWindow("wallpaper") + .then() + .hidesBelowAppWindow("wallpaper") + .forAllEntries(); +``` + +All assertions return `Result` which contains a `success` flag, `assertionName` string identifier, and `reason` string to provide actionable details to the user. The `reason` string is build along the way with all the details as to why the assertions failed and any hints which might help the user determine the root cause. Failed assertion message will also contain a path to the trace that was tested. Example of a failed test: + +``` + java.lang.AssertionError: Not true that <com.android.server.wm.flicker.LayersTrace@65da4cc> + Layers Trace can be found in: /layers_trace_emptyregion.pb + Timestamp: 2308008331271 + Assertion: coversRegion + Reason: Region to test: Rect(0, 0 - 1440, 2880) + first empty point: 0, 99 + visible regions: + StatusBar#0Rect(0, 0 - 1440, 98) + NavigationBar#0Rect(0, 2712 - 1440, 2880) + ScreenDecorOverlay#0Rect(0, 0 - 1440, 91) + ... + at com.google.common.truth.FailureStrategy.fail(FailureStrategy.java:24) + ... +``` + +--- + +## Running Tests + +The tests can be run as any other Android JUnit tests. `platform_testing/tests/flicker` uses the library to test common UI transitions. Run `atest FlickerTest` to execute these tests. + +--- + +## Other Topics +### Monitors +Monitors capture test artifacts for each transition run. They are started before each iteration of the test transition (after the `runBefore` calls) and stopped after the transition is completed. Each iteration will produce a new test artifact. The following monitors are available: + +#### LayersTraceMonitor +Captures Layers trace. This monitor is started by default. Build a transition with `skipLayersTrace()` to disable this monitor. +#### WindowManagerTraceMonitor +Captures Window Manager trace. This monitor is started by default. Build a transition with `skipWindowManagerTrace()` to disable this monitor. +#### WindowAnimationFrameStatsMonitor +Captures WindowAnimationFrameStats for the transition. This monitor is started by default and is used to eliminate *janky* runs. If an iteration has skipped frames, as determined by WindowAnimationFrameStats, the results for the iteration is skipped. If the list of results is empty after all iterations are completed, then the test should fail. Build a transition with `includeJankyRuns()` to disable this monitor. +#### ScreenRecorder +Captures screen to a video file. This monitor is disabled by default. Build a transition with `recordEachRun()` to capture each transition or build with `recordAllRuns()` to capture every transition including setup and teardown. + +--- + +### Extending Assertions + +To add a new assertion, add a function to one of the trace entry classes, `LayersTrace.Entry` or `WindowManagerTrace.Entry`. + +```java + // Example adds an assertion to the check if layer is hidden by parent. + Result isHiddenByParent(String layerName) { + // Result should contain a details if assertion fails for any reason + // such as if layer is not found or layer is not hidden by parent + // or layer has no parent. + // ... + } +``` +Then add a function to the trace subject `LayersTraceSubject` or `WmTraceSubject` which will add the assertion for testing. When the assertion is evaluated, the trace will first be filtered then the assertion will be applied to the remaining entries. + +```java + public LayersTraceSubject isHiddenByParent(String layerName) { + mChecker.add(entry -> entry.isHiddenByParent(layerName), + "isHiddenByParent(" + layerName + ")"); + return this; + } +``` + +To use the new assertion: +```java + // Check if "Chrome" layer is hidden by parent in the first trace entry. + assertThat(result).isHiddenByParent("Chrome").inTheBeginning(); +```
\ No newline at end of file diff --git a/tests/FlickerTests/TEST_MAPPING b/tests/FlickerTests/TEST_MAPPING new file mode 100644 index 000000000000..55a61471dfb8 --- /dev/null +++ b/tests/FlickerTests/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "postsubmit": [ + { + "name": "FlickerTests" + } + ] +}
\ No newline at end of file diff --git a/tests/FlickerTests/lib/Android.bp b/tests/FlickerTests/lib/Android.bp new file mode 100644 index 000000000000..5d8ed2c205e9 --- /dev/null +++ b/tests/FlickerTests/lib/Android.bp @@ -0,0 +1,44 @@ +// +// Copyright (C) 2018 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +java_test { + name: "flickerlib", + platform_apis: true, + srcs: ["src/**/*.java"], + static_libs: [ + "androidx.test.janktesthelper", + "cts-wm-util", + "platformprotosnano", + "layersprotosnano", + "truth-prebuilt", + "sysui-helper", + "launcher-helper-lib", + ], +} + +java_library { + name: "flickerautomationhelperlib", + sdk_version: "test_current", + srcs: [ + "src/com/android/server/wm/flicker/AutomationUtils.java", + "src/com/android/server/wm/flicker/WindowUtils.java", + ], + static_libs: [ + "sysui-helper", + "launcher-helper-lib", + "compatibility-device-util-axt", + ], +} diff --git a/tests/FlickerTests/lib/src/com/android/server/wm/flicker/Assertions.java b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/Assertions.java new file mode 100644 index 000000000000..84f9f871324c --- /dev/null +++ b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/Assertions.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +/** + * Collection of functional interfaces and classes representing assertions and their associated + * results. Assertions are functions that are applied over a single trace entry and returns a + * result which includes a detailed reason if the assertion fails. + */ +class Assertions { + /** + * Checks assertion on a single trace entry. + * + * @param <T> trace entry type to perform the assertion on. + */ + @FunctionalInterface + interface TraceAssertion<T> extends Function<T, Result> { + /** + * Returns an assertion that represents the logical negation of this assertion. + * + * @return a assertion that represents the logical negation of this assertion + */ + default TraceAssertion<T> negate() { + return (T t) -> apply(t).negate(); + } + } + + /** + * Checks assertion on a single layers trace entry. + */ + @FunctionalInterface + interface LayersTraceAssertion extends TraceAssertion<LayersTrace.Entry> { + + } + + /** + * Utility class to store assertions with an identifier to help generate more useful debug + * data when dealing with multiple assertions. + */ + static class NamedAssertion<T> { + final TraceAssertion<T> assertion; + final String name; + + NamedAssertion(TraceAssertion<T> assertion, String name) { + this.assertion = assertion; + this.name = name; + } + } + + /** + * Contains the result of an assertion including the reason for failed assertions. + */ + static class Result { + static final String NEGATION_PREFIX = "!"; + final boolean success; + final long timestamp; + final String assertionName; + final String reason; + + Result(boolean success, long timestamp, String assertionName, String reason) { + this.success = success; + this.timestamp = timestamp; + this.assertionName = assertionName; + this.reason = reason; + } + + Result(boolean success, String reason) { + this.success = success; + this.reason = reason; + this.assertionName = ""; + this.timestamp = 0; + } + + /** + * Returns the negated {@code Result} and adds a negation prefix to the assertion name. + */ + Result negate() { + String negatedAssertionName; + if (this.assertionName.startsWith(NEGATION_PREFIX)) { + negatedAssertionName = this.assertionName.substring(NEGATION_PREFIX.length() + 1); + } else { + negatedAssertionName = NEGATION_PREFIX + this.assertionName; + } + return new Result(!this.success, this.timestamp, negatedAssertionName, this.reason); + } + + boolean passed() { + return this.success; + } + + boolean failed() { + return !this.success; + } + + @Override + public String toString() { + return "Timestamp: " + prettyTimestamp(timestamp) + + "\nAssertion: " + assertionName + + "\nReason: " + reason; + } + + private String prettyTimestamp(long timestamp_ns) { + StringBuilder prettyTimestamp = new StringBuilder(); + TimeUnit[] timeUnits = {TimeUnit.HOURS, TimeUnit.MINUTES, TimeUnit.SECONDS, TimeUnit + .MILLISECONDS}; + String[] unitSuffixes = {"h", "m", "s", "ms"}; + + for (int i = 0; i < timeUnits.length; i++) { + long convertedTime = timeUnits[i].convert(timestamp_ns, TimeUnit.NANOSECONDS); + timestamp_ns -= TimeUnit.NANOSECONDS.convert(convertedTime, timeUnits[i]); + prettyTimestamp.append(convertedTime).append(unitSuffixes[i]); + } + + return prettyTimestamp.toString(); + } + } +} diff --git a/tests/FlickerTests/lib/src/com/android/server/wm/flicker/AssertionsChecker.java b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/AssertionsChecker.java new file mode 100644 index 000000000000..3c65d3c341b3 --- /dev/null +++ b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/AssertionsChecker.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import com.android.server.wm.flicker.Assertions.NamedAssertion; +import com.android.server.wm.flicker.Assertions.Result; +import com.android.server.wm.flicker.Assertions.TraceAssertion; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Captures some of the common logic in {@link LayersTraceSubject} and {@link WmTraceSubject} + * used to filter trace entries and combine multiple assertions. + * + * @param <T> trace entry type + */ +public class AssertionsChecker<T extends ITraceEntry> { + private boolean mFilterEntriesByRange = false; + private long mFilterStartTime = 0; + private long mFilterEndTime = 0; + private AssertionOption mOption = AssertionOption.NONE; + private List<NamedAssertion<T>> mAssertions = new LinkedList<>(); + + void add(Assertions.TraceAssertion<T> assertion, String name) { + mAssertions.add(new NamedAssertion<>(assertion, name)); + } + + void filterByRange(long startTime, long endTime) { + mFilterEntriesByRange = true; + mFilterStartTime = startTime; + mFilterEndTime = endTime; + } + + private void setOption(AssertionOption option) { + if (mOption != AssertionOption.NONE && option != mOption) { + throw new IllegalArgumentException("Cannot use " + mOption + " option with " + + option + " option."); + } + mOption = option; + } + + public void checkFirstEntry() { + setOption(AssertionOption.CHECK_FIRST_ENTRY); + } + + public void checkLastEntry() { + setOption(AssertionOption.CHECK_LAST_ENTRY); + } + + public void checkChangingAssertions() { + setOption(AssertionOption.CHECK_CHANGING_ASSERTIONS); + } + + + /** + * Filters trace entries then runs assertions returning a list of failures. + * + * @param entries list of entries to perform assertions on + * @return list of failed assertion results + */ + List<Result> test(List<T> entries) { + List<T> filteredEntries; + List<Result> failures; + + if (mFilterEntriesByRange) { + filteredEntries = entries.stream() + .filter(e -> ((e.getTimestamp() >= mFilterStartTime) + && (e.getTimestamp() <= mFilterEndTime))) + .collect(Collectors.toList()); + } else { + filteredEntries = entries; + } + + switch (mOption) { + case CHECK_CHANGING_ASSERTIONS: + return assertChanges(filteredEntries); + case CHECK_FIRST_ENTRY: + return assertEntry(filteredEntries.get(0)); + case CHECK_LAST_ENTRY: + return assertEntry(filteredEntries.get(filteredEntries.size() - 1)); + } + return assertAll(filteredEntries); + } + + /** + * Steps through each trace entry checking if provided assertions are true in the order they + * are added. Each assertion must be true for at least a single trace entry. + * + * This can be used to check for asserting a change in property over a trace. Such as visibility + * for a window changes from true to false or top-most window changes from A to Bb and back to A + * again. + */ + private List<Result> assertChanges(List<T> entries) { + List<Result> failures = new ArrayList<>(); + int entryIndex = 0; + int assertionIndex = 0; + int lastPassedAssertionIndex = -1; + + if (mAssertions.size() == 0) { + return failures; + } + + while (assertionIndex < mAssertions.size() && entryIndex < entries.size()) { + TraceAssertion<T> currentAssertion = mAssertions.get(assertionIndex).assertion; + Result result = currentAssertion.apply(entries.get(entryIndex)); + if (result.passed()) { + lastPassedAssertionIndex = assertionIndex; + entryIndex++; + continue; + } + + if (lastPassedAssertionIndex != assertionIndex) { + failures.add(result); + break; + } + assertionIndex++; + + if (assertionIndex == mAssertions.size()) { + failures.add(result); + break; + } + } + + if (failures.isEmpty()) { + if (assertionIndex != mAssertions.size() - 1) { + String reason = "\nAssertion " + mAssertions.get(assertionIndex).name + + " never became false"; + reason += "\nPassed assertions: " + mAssertions.stream().limit(assertionIndex) + .map(assertion -> assertion.name).collect(Collectors.joining(",")); + reason += "\nUntested assertions: " + mAssertions.stream().skip(assertionIndex + 1) + .map(assertion -> assertion.name).collect(Collectors.joining(",")); + + Result result = new Result(false /* success */, 0 /* timestamp */, + "assertChanges", "Not all assertions passed." + reason); + failures.add(result); + } + } + return failures; + } + + private List<Result> assertEntry(T entry) { + List<Result> failures = new ArrayList<>(); + for (NamedAssertion<T> assertion : mAssertions) { + Result result = assertion.assertion.apply(entry); + if (result.failed()) { + failures.add(result); + } + } + return failures; + } + + private List<Result> assertAll(List<T> entries) { + return mAssertions.stream().flatMap( + assertion -> entries.stream() + .map(assertion.assertion) + .filter(Result::failed)) + .collect(Collectors.toList()); + } + + private enum AssertionOption { + NONE, + CHECK_CHANGING_ASSERTIONS, + CHECK_FIRST_ENTRY, + CHECK_LAST_ENTRY, + } +} diff --git a/tests/FlickerTests/lib/src/com/android/server/wm/flicker/AutomationUtils.java b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/AutomationUtils.java new file mode 100644 index 000000000000..e00a2474556c --- /dev/null +++ b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/AutomationUtils.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import static android.os.SystemClock.sleep; +import static android.system.helpers.OverviewHelper.isRecentsInLauncher; +import static android.view.Surface.ROTATION_0; + +import static com.android.compatibility.common.util.SystemUtil.runShellCommand; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.RemoteException; +import android.support.test.launcherhelper.LauncherStrategyFactory; +import android.support.test.uiautomator.By; +import android.support.test.uiautomator.BySelector; +import android.support.test.uiautomator.Configurator; +import android.support.test.uiautomator.UiDevice; +import android.support.test.uiautomator.UiObject2; +import android.support.test.uiautomator.Until; +import android.util.Log; +import android.util.Rational; +import android.view.View; +import android.view.ViewConfiguration; + +import androidx.test.InstrumentationRegistry; + +/** + * Collection of UI Automation helper functions. + */ +public class AutomationUtils { + private static final String SYSTEMUI_PACKAGE = "com.android.systemui"; + private static final long FIND_TIMEOUT = 10000; + private static final long LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout() * 2L; + private static final String TAG = "FLICKER"; + + public static void wakeUpAndGoToHomeScreen() { + UiDevice device = UiDevice.getInstance(InstrumentationRegistry + .getInstrumentation()); + try { + device.wakeUp(); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + device.pressHome(); + } + + /** + * Sets {@link android.app.UiAutomation#waitForIdle(long, long)} global timeout to 0 causing + * the {@link android.app.UiAutomation#waitForIdle(long, long)} function to timeout instantly. + * This removes some delays when using the UIAutomator library required to create fast UI + * transitions. + */ + static void setFastWait() { + Configurator.getInstance().setWaitForIdleTimeout(0); + } + + /** + * Reverts {@link android.app.UiAutomation#waitForIdle(long, long)} to default behavior. + */ + static void setDefaultWait() { + Configurator.getInstance().setWaitForIdleTimeout(10000); + } + + public static boolean isQuickstepEnabled(UiDevice device) { + return device.findObject(By.res(SYSTEMUI_PACKAGE, "recent_apps")) == null; + } + + public static void openQuickstep(UiDevice device) { + if (isQuickstepEnabled(device)) { + int height = device.getDisplayHeight(); + UiObject2 navBar = device.findObject(By.res(SYSTEMUI_PACKAGE, "navigation_bar_frame")); + + Rect navBarVisibleBounds; + + // TODO(vishnun) investigate why this object cannot be found. + if (navBar != null) { + navBarVisibleBounds = navBar.getVisibleBounds(); + } else { + Log.e(TAG, "Could not find nav bar, infer location"); + navBarVisibleBounds = WindowUtils.getNavigationBarPosition(ROTATION_0); + } + + // Swipe from nav bar to 2/3rd down the screen. + device.swipe( + navBarVisibleBounds.centerX(), navBarVisibleBounds.centerY(), + navBarVisibleBounds.centerX(), height * 2 / 3, + (navBarVisibleBounds.centerY() - height * 2 / 3) / 100); // 100 px/step + } else { + try { + device.pressRecentApps(); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + BySelector RECENTS = By.res(SYSTEMUI_PACKAGE, "recents_view"); + + // use a long timeout to wait until recents populated + if (device.wait( + Until.findObject(isRecentsInLauncher() + ? getLauncherOverviewSelector(device) : RECENTS), + 10000) == null) { + fail("Recents didn't appear"); + } + device.waitForIdle(); + } + + static void clearRecents(UiDevice device) { + if (isQuickstepEnabled(device)) { + openQuickstep(device); + + for (int i = 0; i < 5; i++) { + device.swipe(device.getDisplayWidth() / 2, + device.getDisplayHeight() / 2, device.getDisplayWidth(), + device.getDisplayHeight() / 2, + 5); + + BySelector clearAllSelector = By.res("com.google.android.apps.nexuslauncher", + "clear_all_button"); + UiObject2 clearAllButton = device.wait(Until.findObject(clearAllSelector), 100); + if (clearAllButton != null) { + clearAllButton.click(); + return; + } + } + } + } + + private static BySelector getLauncherOverviewSelector(UiDevice device) { + return By.res(device.getLauncherPackageName(), "overview_panel"); + } + + private static void longPressRecents(UiDevice device) { + BySelector recentsSelector = By.res(SYSTEMUI_PACKAGE, "recent_apps"); + UiObject2 recentsButton = device.wait(Until.findObject(recentsSelector), FIND_TIMEOUT); + assertNotNull("Unable to find recents button", recentsButton); + recentsButton.click(LONG_PRESS_TIMEOUT); + } + + public static void launchSplitScreen(UiDevice device) { + String mLauncherPackage = LauncherStrategyFactory.getInstance(device) + .getLauncherStrategy().getSupportedLauncherPackage(); + + if (isQuickstepEnabled(device)) { + // Quickstep enabled + openQuickstep(device); + + BySelector overviewIconSelector = By.res(mLauncherPackage, "icon") + .clazz(View.class); + UiObject2 overviewIcon = device.wait(Until.findObject(overviewIconSelector), + FIND_TIMEOUT); + assertNotNull("Unable to find app icon in Overview", overviewIcon); + overviewIcon.click(); + + BySelector splitscreenButtonSelector = By.text("Split screen"); + UiObject2 splitscreenButton = device.wait(Until.findObject(splitscreenButtonSelector), + FIND_TIMEOUT); + assertNotNull("Unable to find Split screen button in Overview", splitscreenButton); + splitscreenButton.click(); + } else { + // Classic long press recents + longPressRecents(device); + } + // Wait for animation to complete. + sleep(2000); + } + + public static void exitSplitScreen(UiDevice device) { + if (isQuickstepEnabled(device)) { + // Quickstep enabled + BySelector dividerSelector = By.res(SYSTEMUI_PACKAGE, "docked_divider_handle"); + UiObject2 divider = device.wait(Until.findObject(dividerSelector), FIND_TIMEOUT); + assertNotNull("Unable to find Split screen divider", divider); + + // Drag the split screen divider to the top of the screen + divider.drag(new Point(device.getDisplayWidth() / 2, 0), 400); + } else { + // Classic long press recents + longPressRecents(device); + } + // Wait for animation to complete. + sleep(2000); + } + + static void resizeSplitScreen(UiDevice device, Rational windowHeightRatio) { + BySelector dividerSelector = By.res(SYSTEMUI_PACKAGE, "docked_divider_handle"); + UiObject2 divider = device.wait(Until.findObject(dividerSelector), FIND_TIMEOUT); + assertNotNull("Unable to find Split screen divider", divider); + int destHeight = + (int) (WindowUtils.getDisplayBounds().height() * windowHeightRatio.floatValue()); + // Drag the split screen divider to so that the ratio of top window height and bottom + // window height is windowHeightRatio + device.drag(divider.getVisibleBounds().centerX(), divider.getVisibleBounds().centerY(), + device.getDisplayWidth() / 2, destHeight, 10); + //divider.drag(new Point(device.getDisplayWidth() / 2, destHeight), 400) + divider = device.wait(Until.findObject(dividerSelector), FIND_TIMEOUT); + + // Wait for animation to complete. + sleep(2000); + } + + static void closePipWindow(UiDevice device) { + UiObject2 pipWindow = device.findObject( + By.res(SYSTEMUI_PACKAGE, "background")); + pipWindow.click(); + UiObject2 exitPipObject = device.findObject( + By.res(SYSTEMUI_PACKAGE, "dismiss")); + exitPipObject.click(); + // Wait for animation to complete. + sleep(2000); + } + + static void expandPipWindow(UiDevice device) { + UiObject2 pipWindow = device.findObject( + By.res(SYSTEMUI_PACKAGE, "background")); + pipWindow.click(); + pipWindow.click(); + } + + public static void stopPackage(Context context, String packageName) { + runShellCommand("am force-stop " + packageName); + int packageUid; + try { + packageUid = context.getPackageManager().getPackageUid(packageName, /* flags= */0); + } catch (PackageManager.NameNotFoundException e) { + return; + } + while (targetPackageIsRunning(packageUid)) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + //ignore + } + } + } + + private static boolean targetPackageIsRunning(int uid) { + final String result = runShellCommand( + String.format("cmd activity get-uid-state %d", uid)); + return !result.contains("(NONEXISTENT)"); + } +}
\ No newline at end of file diff --git a/tests/FlickerTests/lib/src/com/android/server/wm/flicker/ITraceEntry.java b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/ITraceEntry.java new file mode 100644 index 000000000000..9525f41b46b2 --- /dev/null +++ b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/ITraceEntry.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +/** + * Common interface for Layer and WindowManager trace entries. + */ +interface ITraceEntry { + /** + * @return timestamp of current entry + */ + long getTimestamp(); +} diff --git a/tests/FlickerTests/lib/src/com/android/server/wm/flicker/LayersTrace.java b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/LayersTrace.java new file mode 100644 index 000000000000..660ec0fe4833 --- /dev/null +++ b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/LayersTrace.java @@ -0,0 +1,412 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import android.annotation.Nullable; +import android.graphics.Rect; +import android.surfaceflinger.nano.Layers.LayerProto; +import android.surfaceflinger.nano.Layers.RectProto; +import android.surfaceflinger.nano.Layers.RegionProto; +import android.surfaceflinger.nano.Layerstrace.LayersTraceFileProto; +import android.surfaceflinger.nano.Layerstrace.LayersTraceProto; +import android.util.SparseArray; + +import com.android.server.wm.flicker.Assertions.Result; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Contains a collection of parsed Layers trace entries and assertions to apply over + * a single entry. + * + * Each entry is parsed into a list of {@link LayersTrace.Entry} objects. + */ +public class LayersTrace { + final private List<Entry> mEntries; + @Nullable + final private Path mSource; + + private LayersTrace(List<Entry> entries, Path source) { + this.mEntries = entries; + this.mSource = source; + } + + /** + * Parses {@code LayersTraceFileProto} from {@code data} and uses the proto to generates a list + * of trace entries, storing the flattened layers into its hierarchical structure. + * + * @param data binary proto data + * @param source Path to source of data for additional debug information + */ + static LayersTrace parseFrom(byte[] data, Path source) { + List<Entry> entries = new ArrayList<>(); + LayersTraceFileProto fileProto; + try { + fileProto = LayersTraceFileProto.parseFrom(data); + } catch (Exception e) { + throw new RuntimeException(e); + } + for (LayersTraceProto traceProto : fileProto.entry) { + Entry entry = Entry.fromFlattenedLayers(traceProto.elapsedRealtimeNanos, + traceProto.layers.layers); + entries.add(entry); + } + return new LayersTrace(entries, source); + } + + /** + * Parses {@code LayersTraceFileProto} from {@code data} and uses the proto to generates a list + * of trace entries, storing the flattened layers into its hierarchical structure. + * + * @param data binary proto data + */ + static LayersTrace parseFrom(byte[] data) { + return parseFrom(data, null); + } + + List<Entry> getEntries() { + return mEntries; + } + + Entry getEntry(long timestamp) { + Optional<Entry> entry = mEntries.stream() + .filter(e -> e.getTimestamp() == timestamp) + .findFirst(); + if (!entry.isPresent()) { + throw new RuntimeException("Entry does not exist for timestamp " + timestamp); + } + return entry.get(); + } + + Optional<Path> getSource() { + return Optional.ofNullable(mSource); + } + + /** + * Represents a single Layer trace entry. + */ + static class Entry implements ITraceEntry { + private long mTimestamp; + private List<Layer> mRootLayers; // hierarchical representation of layers + private List<Layer> mFlattenedLayers = null; + + private Entry(long timestamp, List<Layer> rootLayers) { + this.mTimestamp = timestamp; + this.mRootLayers = rootLayers; + } + + /** + * Constructs the layer hierarchy from a flattened list of layers. + */ + static Entry fromFlattenedLayers(long timestamp, LayerProto[] protos) { + SparseArray<Layer> layerMap = new SparseArray<>(); + ArrayList<Layer> orphans = new ArrayList<>(); + for (LayerProto proto : protos) { + int id = proto.id; + int parentId = proto.parent; + + Layer newLayer = layerMap.get(id); + if (newLayer == null) { + newLayer = new Layer(proto); + layerMap.append(id, newLayer); + } else if (newLayer.mProto != null) { + throw new RuntimeException("Duplicate layer id found:" + id); + } else { + newLayer.mProto = proto; + orphans.remove(newLayer); + } + + // add parent placeholder + if (layerMap.get(parentId) == null) { + Layer orphanLayer = new Layer(null); + layerMap.append(parentId, orphanLayer); + orphans.add(orphanLayer); + } + layerMap.get(parentId).addChild(newLayer); + newLayer.addParent(layerMap.get(parentId)); + } + + // Fail if we find orphan layers. + orphans.remove(layerMap.get(-1)); + orphans.forEach(orphan -> { + String childNodes = orphan.mChildren.stream().map(node -> + Integer.toString(node.getId())).collect(Collectors.joining(", ")); + int orphanId = orphan.mChildren.get(0).mProto.parent; + throw new RuntimeException( + "Failed to parse layers trace. Found orphan layers with parent " + + "layer id:" + orphanId + " : " + childNodes); + }); + + return new Entry(timestamp, layerMap.get(-1).mChildren); + } + + /** + * Extracts {@link Rect} from {@link RectProto}. + */ + private static Rect extract(RectProto proto) { + return new Rect(proto.left, proto.top, proto.right, proto.bottom); + } + + /** + * Extracts {@link Rect} from {@link RegionProto} by returning a rect that encompasses all + * the rects making up the region. + */ + private static Rect extract(RegionProto regionProto) { + Rect region = new Rect(); + for (RectProto proto : regionProto.rect) { + region.union(proto.left, proto.top, proto.right, proto.bottom); + } + return region; + } + + /** + * Checks if a region specified by {@code testRect} is covered by all visible layers. + */ + Result coversRegion(Rect testRect) { + String assertionName = "coversRegion"; + Collection<Layer> layers = asFlattenedLayers(); + + for (int x = testRect.left; x < testRect.right; x++) { + for (int y = testRect.top; y < testRect.bottom; y++) { + boolean emptyRegionFound = true; + for (Layer layer : layers) { + if (layer.isInvisible() || layer.isHiddenByParent()) { + continue; + } + for (RectProto rectProto : layer.mProto.visibleRegion.rect) { + Rect r = extract(rectProto); + if (r.contains(x, y)) { + y = r.bottom; + emptyRegionFound = false; + } + } + } + if (emptyRegionFound) { + String reason = "Region to test: " + testRect + + "\nfirst empty point: " + x + ", " + y; + reason += "\nvisible regions:"; + for (Layer layer : layers) { + if (layer.isInvisible() || layer.isHiddenByParent()) { + continue; + } + Rect r = extract(layer.mProto.visibleRegion); + reason += "\n" + layer.mProto.name + r.toString(); + } + return new Result(false /* success */, this.mTimestamp, assertionName, + reason); + } + } + } + String info = "Region covered: " + testRect; + return new Result(true /* success */, this.mTimestamp, assertionName, info); + } + + /** + * Checks if a layer with name {@code layerName} has a visible region + * {@code expectedVisibleRegion}. + */ + Result hasVisibleRegion(String layerName, Rect expectedVisibleRegion) { + String assertionName = "hasVisibleRegion"; + String reason = "Could not find " + layerName; + for (Layer layer : asFlattenedLayers()) { + if (layer.mProto.name.contains(layerName)) { + if (layer.isHiddenByParent()) { + reason = layer.getHiddenByParentReason(); + continue; + } + if (layer.isInvisible()) { + reason = layer.getVisibilityReason(); + continue; + } + Rect visibleRegion = extract(layer.mProto.visibleRegion); + if (visibleRegion.equals(expectedVisibleRegion)) { + return new Result(true /* success */, this.mTimestamp, assertionName, + layer.mProto.name + "has visible region " + expectedVisibleRegion); + } + reason = layer.mProto.name + " has visible region:" + visibleRegion + " " + + "expected:" + expectedVisibleRegion; + } + } + return new Result(false /* success */, this.mTimestamp, assertionName, reason); + } + + /** + * Checks if a layer with name {@code layerName} is visible. + */ + Result isVisible(String layerName) { + String assertionName = "isVisible"; + String reason = "Could not find " + layerName; + for (Layer layer : asFlattenedLayers()) { + if (layer.mProto.name.contains(layerName)) { + if (layer.isHiddenByParent()) { + reason = layer.getHiddenByParentReason(); + continue; + } + if (layer.isInvisible()) { + reason = layer.getVisibilityReason(); + continue; + } + return new Result(true /* success */, this.mTimestamp, assertionName, + layer.mProto.name + " is visible"); + } + } + return new Result(false /* success */, this.mTimestamp, assertionName, reason); + } + + @Override + public long getTimestamp() { + return mTimestamp; + } + + List<Layer> getRootLayers() { + return mRootLayers; + } + + List<Layer> asFlattenedLayers() { + if (mFlattenedLayers == null) { + mFlattenedLayers = new ArrayList<>(); + ArrayList<Layer> pendingLayers = new ArrayList<>(this.mRootLayers); + while (!pendingLayers.isEmpty()) { + Layer layer = pendingLayers.remove(0); + mFlattenedLayers.add(layer); + pendingLayers.addAll(layer.mChildren); + } + } + return mFlattenedLayers; + } + + Rect getVisibleBounds(String layerName) { + List<Layer> layers = asFlattenedLayers(); + for (Layer layer : layers) { + if (layer.mProto.name.contains(layerName) && layer.isVisible()) { + return extract(layer.mProto.visibleRegion); + } + } + return new Rect(0, 0, 0, 0); + } + } + + /** + * Represents a single layer with links to its parent and child layers. + */ + static class Layer { + @Nullable + LayerProto mProto; + List<Layer> mChildren; + @Nullable + Layer mParent = null; + + private Layer(LayerProto proto) { + this.mProto = proto; + this.mChildren = new ArrayList<>(); + } + + private void addChild(Layer childLayer) { + this.mChildren.add(childLayer); + } + + private void addParent(Layer parentLayer) { + this.mParent = parentLayer; + } + + int getId() { + return mProto.id; + } + + boolean isActiveBufferEmpty() { + return this.mProto.activeBuffer == null || this.mProto.activeBuffer.height == 0 + || this.mProto.activeBuffer.width == 0; + } + + boolean isVisibleRegionEmpty() { + if (this.mProto.visibleRegion == null) { + return true; + } + Rect visibleRect = Entry.extract(this.mProto.visibleRegion); + return visibleRect.height() == 0 || visibleRect.width() == 0; + } + + boolean isHidden() { + return (this.mProto.flags & /* FLAG_HIDDEN */ 0x1) != 0x0; + } + + boolean isVisible() { + return (!isActiveBufferEmpty() || isColorLayer()) && + !isHidden() && this.mProto.color.a > 0 && !isVisibleRegionEmpty(); + } + + boolean isColorLayer() { + return this.mProto.type.equals("ColorLayer"); + } + + boolean isRootLayer() { + return mParent == null || mParent.mProto == null; + } + + boolean isInvisible() { + return !isVisible(); + } + + boolean isHiddenByParent() { + return !isRootLayer() && (mParent.isHidden() || mParent.isHiddenByParent()); + } + + String getHiddenByParentReason() { + String reason = "Layer " + mProto.name; + if (isHiddenByParent()) { + reason += " is hidden by parent: " + mParent.mProto.name; + } else { + reason += " is not hidden by parent: " + mParent.mProto.name; + } + return reason; + } + + String getVisibilityReason() { + String reason = "Layer " + mProto.name; + if (isVisible()) { + reason += " is visible:"; + } else { + reason += " is invisible:"; + if (this.mProto.activeBuffer == null) { + reason += " activeBuffer=null"; + } else if (this.mProto.activeBuffer.height == 0) { + reason += " activeBuffer.height=0"; + } else if (this.mProto.activeBuffer.width == 0) { + reason += " activeBuffer.width=0"; + } + if (!isColorLayer()) { + reason += " type != ColorLayer"; + } + if (isHidden()) { + reason += " flags=" + this.mProto.flags + " (FLAG_HIDDEN set)"; + } + if (this.mProto.color.a == 0) { + reason += " color.a=0"; + } + if (isVisibleRegionEmpty()) { + reason += " visible region is empty"; + } + } + return reason; + } + } +}
\ No newline at end of file diff --git a/tests/FlickerTests/lib/src/com/android/server/wm/flicker/LayersTraceSubject.java b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/LayersTraceSubject.java new file mode 100644 index 000000000000..4085810a213d --- /dev/null +++ b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/LayersTraceSubject.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import static com.google.common.truth.Truth.assertAbout; +import static com.google.common.truth.Truth.assertWithMessage; + +import android.annotation.Nullable; +import android.graphics.Rect; + +import com.android.server.wm.flicker.Assertions.Result; +import com.android.server.wm.flicker.LayersTrace.Entry; +import com.android.server.wm.flicker.TransitionRunner.TransitionResult; + +import com.google.common.truth.FailureMetadata; +import com.google.common.truth.Subject; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Truth subject for {@link LayersTrace} objects. + */ +public class LayersTraceSubject extends Subject<LayersTraceSubject, LayersTrace> { + // Boiler-plate Subject.Factory for LayersTraceSubject + private static final Subject.Factory<LayersTraceSubject, LayersTrace> FACTORY = + new Subject.Factory<LayersTraceSubject, LayersTrace>() { + @Override + public LayersTraceSubject createSubject( + FailureMetadata fm, @Nullable LayersTrace target) { + return new LayersTraceSubject(fm, target); + } + }; + + private AssertionsChecker<Entry> mChecker = new AssertionsChecker<>(); + + private LayersTraceSubject(FailureMetadata fm, @Nullable LayersTrace subject) { + super(fm, subject); + } + + // User-defined entry point + public static LayersTraceSubject assertThat(@Nullable LayersTrace entry) { + return assertAbout(FACTORY).that(entry); + } + + // User-defined entry point + public static LayersTraceSubject assertThat(@Nullable TransitionResult result) { + LayersTrace entries = LayersTrace.parseFrom(result.getLayersTrace(), + result.getLayersTracePath()); + return assertWithMessage(result.toString()).about(FACTORY).that(entries); + } + + // Static method for getting the subject factory (for use with assertAbout()) + public static Subject.Factory<LayersTraceSubject, LayersTrace> entries() { + return FACTORY; + } + + public void forAllEntries() { + test(); + } + + public void forRange(long startTime, long endTime) { + mChecker.filterByRange(startTime, endTime); + test(); + } + + public LayersTraceSubject then() { + mChecker.checkChangingAssertions(); + return this; + } + + public void inTheBeginning() { + if (getSubject().getEntries().isEmpty()) { + fail("No entries found."); + } + mChecker.checkFirstEntry(); + test(); + } + + public void atTheEnd() { + if (getSubject().getEntries().isEmpty()) { + fail("No entries found."); + } + mChecker.checkLastEntry(); + test(); + } + + private void test() { + List<Result> failures = mChecker.test(getSubject().getEntries()); + if (!failures.isEmpty()) { + String failureLogs = failures.stream().map(Result::toString) + .collect(Collectors.joining("\n")); + String tracePath = ""; + if (getSubject().getSource().isPresent()) { + tracePath = "\nLayers Trace can be found in: " + + getSubject().getSource().get().toAbsolutePath() + "\n"; + } + fail(tracePath + failureLogs); + } + } + + public LayersTraceSubject coversRegion(Rect rect) { + mChecker.add(entry -> entry.coversRegion(rect), + "coversRegion(" + rect + ")"); + return this; + } + + public LayersTraceSubject hasVisibleRegion(String layerName, Rect size) { + mChecker.add(entry -> entry.hasVisibleRegion(layerName, size), + "hasVisibleRegion(" + layerName + size + ")"); + return this; + } + + public LayersTraceSubject showsLayer(String layerName) { + mChecker.add(entry -> entry.isVisible(layerName), + "showsLayer(" + layerName + ")"); + return this; + } + + public LayersTraceSubject hidesLayer(String layerName) { + mChecker.add(entry -> entry.isVisible(layerName).negate(), + "hidesLayer(" + layerName + ")"); + return this; + } +} diff --git a/tests/FlickerTests/lib/src/com/android/server/wm/flicker/TransitionRunner.java b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/TransitionRunner.java new file mode 100644 index 000000000000..0a3fe3c00de2 --- /dev/null +++ b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/TransitionRunner.java @@ -0,0 +1,424 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import android.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.util.Log; + +import androidx.test.InstrumentationRegistry; + +import com.android.server.wm.flicker.monitor.ITransitionMonitor; +import com.android.server.wm.flicker.monitor.LayersTraceMonitor; +import com.android.server.wm.flicker.monitor.ScreenRecorder; +import com.android.server.wm.flicker.monitor.WindowAnimationFrameStatsMonitor; +import com.android.server.wm.flicker.monitor.WindowManagerTraceMonitor; + +import com.google.common.io.Files; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +/** + * Builds and runs UI transitions capturing test artifacts. + * + * User can compose a transition from simpler steps, specifying setup and teardown steps. During + * a transition, Layers trace, WindowManager trace, screen recordings and window animation frame + * stats can be captured. + * + * <pre> + * Transition builder options: + * {@link TransitionBuilder#run(Runnable)} run transition under test. Monitors will be started + * before the transition and stopped after the transition is completed. + * {@link TransitionBuilder#repeat(int)} repeat transitions under test multiple times recording + * result for each run. + * {@link TransitionBuilder#withTag(String)} specify a string identifier used to prefix logs and + * artifacts generated. + * {@link TransitionBuilder#runBeforeAll(Runnable)} run setup transitions once before all other + * transition are run to set up an initial state on device. + * {@link TransitionBuilder#runBefore(Runnable)} run setup transitions before each test transition + * run. + * {@link TransitionBuilder#runAfter(Runnable)} run teardown transitions after each test + * transition. + * {@link TransitionBuilder#runAfter(Runnable)} run teardown transitions once after all + * other transition are run. + * {@link TransitionBuilder#includeJankyRuns()} disables {@link WindowAnimationFrameStatsMonitor} + * to monitor janky frames. If janky frames are detected, then the test run is skipped. This + * monitor is enabled by default. + * {@link TransitionBuilder#skipLayersTrace()} disables {@link LayersTraceMonitor} used to + * capture Layers trace during a transition. This monitor is enabled by default. + * {@link TransitionBuilder#skipWindowManagerTrace()} disables {@link WindowManagerTraceMonitor} + * used to capture WindowManager trace during a transition. This monitor is enabled by + * default. + * {@link TransitionBuilder#recordAllRuns()} records the screen contents and saves it to a file. + * All the runs including setup and teardown transitions are included in the recording. This + * monitor is used for debugging purposes. + * {@link TransitionBuilder#recordEachRun()} records the screen contents during test transitions + * and saves it to a file for each run. This monitor is used for debugging purposes. + * + * Example transition to capture WindowManager and Layers trace when opening a test app: + * {@code + * TransitionRunner.newBuilder() + * .withTag("OpenTestAppFast") + * .runBeforeAll(UiAutomationLib::wakeUp) + * .runBeforeAll(UiAutomationLib::UnlockDevice) + * .runBeforeAll(UiAutomationLib::openTestApp) + * .runBefore(UiAutomationLib::closeTestApp) + * .run(UiAutomationLib::openTestApp) + * .runAfterAll(UiAutomationLib::closeTestApp) + * .repeat(5) + * .build() + * .run(); + * } + * </pre> + */ +class TransitionRunner { + private static final String TAG = "FLICKER"; + private final ScreenRecorder mScreenRecorder; + private final WindowManagerTraceMonitor mWmTraceMonitor; + private final LayersTraceMonitor mLayersTraceMonitor; + private final WindowAnimationFrameStatsMonitor mFrameStatsMonitor; + + private final List<ITransitionMonitor> mAllRunsMonitors; + private final List<ITransitionMonitor> mPerRunMonitors; + private final List<Runnable> mBeforeAlls; + private final List<Runnable> mBefores; + private final List<Runnable> mTransitions; + private final List<Runnable> mAfters; + private final List<Runnable> mAfterAlls; + + private final int mIterations; + private final String mTestTag; + + @Nullable + private List<TransitionResult> mResults = null; + + private TransitionRunner(TransitionBuilder builder) { + mScreenRecorder = builder.mScreenRecorder; + mWmTraceMonitor = builder.mWmTraceMonitor; + mLayersTraceMonitor = builder.mLayersTraceMonitor; + mFrameStatsMonitor = builder.mFrameStatsMonitor; + + mAllRunsMonitors = builder.mAllRunsMonitors; + mPerRunMonitors = builder.mPerRunMonitors; + mBeforeAlls = builder.mBeforeAlls; + mBefores = builder.mBefores; + mTransitions = builder.mTransitions; + mAfters = builder.mAfters; + mAfterAlls = builder.mAfterAlls; + + mIterations = builder.mIterations; + mTestTag = builder.mTestTag; + } + + static TransitionBuilder newBuilder() { + return new TransitionBuilder(); + } + + /** + * Runs the composed transition and calls monitors at the appropriate stages. If jank monitor + * is enabled, transitions with jank are skipped. + * + * @return itself + */ + TransitionRunner run() { + mResults = new ArrayList<>(); + mAllRunsMonitors.forEach(ITransitionMonitor::start); + mBeforeAlls.forEach(Runnable::run); + for (int iteration = 0; iteration < mIterations; iteration++) { + mBefores.forEach(Runnable::run); + mPerRunMonitors.forEach(ITransitionMonitor::start); + mTransitions.forEach(Runnable::run); + mPerRunMonitors.forEach(ITransitionMonitor::stop); + mAfters.forEach(Runnable::run); + if (runJankFree() && mFrameStatsMonitor.jankyFramesDetected()) { + String msg = String.format("Skipping iteration %d/%d for test %s due to jank. %s", + iteration, mIterations - 1, mTestTag, mFrameStatsMonitor.toString()); + Log.e(TAG, msg); + continue; + } + mResults.add(saveResult(iteration)); + } + mAfterAlls.forEach(Runnable::run); + mAllRunsMonitors.forEach(monitor -> { + monitor.stop(); + Path path = monitor.save(mTestTag); + Log.e(TAG, "Video saved to " + path.toString()); + }); + return this; + } + + /** + * Returns a list of transition results. + * + * @return list of transition results. + */ + List<TransitionResult> getResults() { + if (mResults == null) { + throw new IllegalStateException("Results do not exist!"); + } + return mResults; + } + + /** + * Deletes all transition results that are not marked for saving. + * + * @return list of transition results. + */ + void deleteResults() { + if (mResults == null) { + return; + } + mResults.stream() + .filter(TransitionResult::canDelete) + .forEach(TransitionResult::delete); + mResults = null; + } + + /** + * Saves monitor results to file. + * + * @return object containing paths to test artifacts + */ + private TransitionResult saveResult(int iteration) { + Path windowTrace = null; + Path layerTrace = null; + Path screenCaptureVideo = null; + + if (mPerRunMonitors.contains(mWmTraceMonitor)) { + windowTrace = mWmTraceMonitor.save(mTestTag, iteration); + } + if (mPerRunMonitors.contains(mLayersTraceMonitor)) { + layerTrace = mLayersTraceMonitor.save(mTestTag, iteration); + } + if (mPerRunMonitors.contains(mScreenRecorder)) { + screenCaptureVideo = mScreenRecorder.save(mTestTag, iteration); + } + return new TransitionResult(layerTrace, windowTrace, screenCaptureVideo); + } + + private boolean runJankFree() { + return mPerRunMonitors.contains(mFrameStatsMonitor); + } + + public String getTestTag() { + return mTestTag; + } + + /** + * Stores paths to all test artifacts. + */ + @VisibleForTesting + public static class TransitionResult { + @Nullable + final Path layersTrace; + @Nullable + final Path windowManagerTrace; + @Nullable + final Path screenCaptureVideo; + private boolean flaggedForSaving; + + TransitionResult(@Nullable Path layersTrace, @Nullable Path windowManagerTrace, + @Nullable Path screenCaptureVideo) { + this.layersTrace = layersTrace; + this.windowManagerTrace = windowManagerTrace; + this.screenCaptureVideo = screenCaptureVideo; + } + + void flagForSaving() { + flaggedForSaving = true; + } + + boolean canDelete() { + return !flaggedForSaving; + } + + boolean layersTraceExists() { + return layersTrace != null && layersTrace.toFile().exists(); + } + + byte[] getLayersTrace() { + try { + return Files.toByteArray(this.layersTrace.toFile()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + Path getLayersTracePath() { + return layersTrace; + } + + boolean windowManagerTraceExists() { + return windowManagerTrace != null && windowManagerTrace.toFile().exists(); + } + + public byte[] getWindowManagerTrace() { + try { + return Files.toByteArray(this.windowManagerTrace.toFile()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + Path getWindowManagerTracePath() { + return windowManagerTrace; + } + + boolean screenCaptureVideoExists() { + return screenCaptureVideo != null && screenCaptureVideo.toFile().exists(); + } + + Path screenCaptureVideoPath() { + return screenCaptureVideo; + } + + void delete() { + if (layersTraceExists()) layersTrace.toFile().delete(); + if (windowManagerTraceExists()) windowManagerTrace.toFile().delete(); + if (screenCaptureVideoExists()) screenCaptureVideo.toFile().delete(); + } + } + + /** + * Builds a {@link TransitionRunner} instance. + */ + static class TransitionBuilder { + private ScreenRecorder mScreenRecorder; + private WindowManagerTraceMonitor mWmTraceMonitor; + private LayersTraceMonitor mLayersTraceMonitor; + private WindowAnimationFrameStatsMonitor mFrameStatsMonitor; + + private List<ITransitionMonitor> mAllRunsMonitors = new LinkedList<>(); + private List<ITransitionMonitor> mPerRunMonitors = new LinkedList<>(); + private List<Runnable> mBeforeAlls = new LinkedList<>(); + private List<Runnable> mBefores = new LinkedList<>(); + private List<Runnable> mTransitions = new LinkedList<>(); + private List<Runnable> mAfters = new LinkedList<>(); + private List<Runnable> mAfterAlls = new LinkedList<>(); + + private boolean mRunJankFree = true; + private boolean mCaptureWindowManagerTrace = true; + private boolean mCaptureLayersTrace = true; + private boolean mRecordEachRun = false; + private int mIterations = 1; + private String mTestTag = ""; + + private boolean mRecordAllRuns = false; + + TransitionBuilder() { + mScreenRecorder = new ScreenRecorder(); + mWmTraceMonitor = new WindowManagerTraceMonitor(); + mLayersTraceMonitor = new LayersTraceMonitor(); + mFrameStatsMonitor = new + WindowAnimationFrameStatsMonitor(InstrumentationRegistry.getInstrumentation()); + } + + TransitionRunner build() { + if (mCaptureWindowManagerTrace) { + mPerRunMonitors.add(mWmTraceMonitor); + } + + if (mCaptureLayersTrace) { + mPerRunMonitors.add(mLayersTraceMonitor); + } + + if (mRunJankFree) { + mPerRunMonitors.add(mFrameStatsMonitor); + } + + if (mRecordAllRuns) { + mAllRunsMonitors.add(mScreenRecorder); + } + + if (mRecordEachRun) { + mPerRunMonitors.add(mScreenRecorder); + } + + return new TransitionRunner(this); + } + + TransitionBuilder runBeforeAll(Runnable runnable) { + mBeforeAlls.add(runnable); + return this; + } + + TransitionBuilder runBefore(Runnable runnable) { + mBefores.add(runnable); + return this; + } + + TransitionBuilder run(Runnable runnable) { + mTransitions.add(runnable); + return this; + } + + TransitionBuilder runAfter(Runnable runnable) { + mAfters.add(runnable); + return this; + } + + TransitionBuilder runAfterAll(Runnable runnable) { + mAfterAlls.add(runnable); + return this; + } + + TransitionBuilder repeat(int iterations) { + mIterations = iterations; + return this; + } + + TransitionBuilder skipWindowManagerTrace() { + mCaptureWindowManagerTrace = false; + return this; + } + + TransitionBuilder skipLayersTrace() { + mCaptureLayersTrace = false; + return this; + } + + TransitionBuilder includeJankyRuns() { + mRunJankFree = false; + return this; + } + + TransitionBuilder recordEachRun() { + if (mRecordAllRuns) { + throw new IllegalArgumentException("Invalid option with recordAllRuns"); + } + mRecordEachRun = true; + return this; + } + + TransitionBuilder recordAllRuns() { + if (mRecordEachRun) { + throw new IllegalArgumentException("Invalid option with recordEachRun"); + } + mRecordAllRuns = true; + return this; + } + + TransitionBuilder withTag(String testTag) { + mTestTag = testTag; + return this; + } + } +} diff --git a/tests/FlickerTests/lib/src/com/android/server/wm/flicker/WindowManagerTrace.java b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/WindowManagerTrace.java new file mode 100644 index 000000000000..e3592eb8cd01 --- /dev/null +++ b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/WindowManagerTrace.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import android.annotation.Nullable; + +import com.android.server.wm.flicker.Assertions.Result; +import com.android.server.wm.nano.AppWindowTokenProto; +import com.android.server.wm.nano.StackProto; +import com.android.server.wm.nano.TaskProto; +import com.android.server.wm.nano.WindowManagerTraceFileProto; +import com.android.server.wm.nano.WindowManagerTraceProto; +import com.android.server.wm.nano.WindowStateProto; +import com.android.server.wm.nano.WindowTokenProto; + +import com.google.protobuf.nano.InvalidProtocolBufferNanoException; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Contains a collection of parsed WindowManager trace entries and assertions to apply over + * a single entry. + * + * Each entry is parsed into a list of {@link WindowManagerTrace.Entry} objects. + */ +public class WindowManagerTrace { + private static final int DEFAULT_DISPLAY = 0; + private final List<Entry> mEntries; + @Nullable + final private Path mSource; + + private WindowManagerTrace(List<Entry> entries, Path source) { + this.mEntries = entries; + this.mSource = source; + } + + /** + * Parses {@code WindowManagerTraceFileProto} from {@code data} and uses the proto to + * generates a list of trace entries. + * + * @param data binary proto data + * @param source Path to source of data for additional debug information + */ + static WindowManagerTrace parseFrom(byte[] data, Path source) { + List<Entry> entries = new ArrayList<>(); + + WindowManagerTraceFileProto fileProto; + try { + fileProto = WindowManagerTraceFileProto.parseFrom(data); + } catch (InvalidProtocolBufferNanoException e) { + throw new RuntimeException(e); + } + for (WindowManagerTraceProto entryProto : fileProto.entry) { + entries.add(new Entry(entryProto)); + } + return new WindowManagerTrace(entries, source); + } + + static WindowManagerTrace parseFrom(byte[] data) { + return parseFrom(data, null); + } + + public List<Entry> getEntries() { + return mEntries; + } + + Entry getEntry(long timestamp) { + Optional<Entry> entry = mEntries.stream() + .filter(e -> e.getTimestamp() == timestamp) + .findFirst(); + if (!entry.isPresent()) { + throw new RuntimeException("Entry does not exist for timestamp " + timestamp); + } + return entry.get(); + } + + Optional<Path> getSource() { + return Optional.ofNullable(mSource); + } + + /** + * Represents a single WindowManager trace entry. + */ + static class Entry implements ITraceEntry { + private final WindowManagerTraceProto mProto; + + Entry(WindowManagerTraceProto proto) { + mProto = proto; + } + + private static Result isWindowVisible(String windowTitle, + WindowTokenProto[] windowTokenProtos) { + boolean titleFound = false; + for (WindowTokenProto windowToken : windowTokenProtos) { + for (WindowStateProto windowState : windowToken.windows) { + if (windowState.identifier.title.contains(windowTitle)) { + titleFound = true; + if (isVisible(windowState)) { + return new Result(true /* success */, + windowState.identifier.title + " is visible"); + } + } + } + } + + String reason; + if (!titleFound) { + reason = windowTitle + " cannot be found"; + } else { + reason = windowTitle + " is invisible"; + } + return new Result(false /* success */, reason); + } + + private static boolean isVisible(WindowStateProto windowState) { + return windowState.windowContainer.visible; + } + + @Override + public long getTimestamp() { + return mProto.elapsedRealtimeNanos; + } + + /** + * Returns window title of the top most visible app window. + */ + private String getTopVisibleAppWindow() { + StackProto[] stacks = mProto.windowManagerService.rootWindowContainer + .displays[DEFAULT_DISPLAY].stacks; + for (StackProto stack : stacks) { + for (TaskProto task : stack.tasks) { + for (AppWindowTokenProto token : task.appWindowTokens) { + for (WindowStateProto windowState : token.windowToken.windows) { + if (windowState.windowContainer.visible) { + return task.appWindowTokens[0].name; + } + } + } + } + } + + return ""; + } + + /** + * Checks if aboveAppWindow with {@code windowTitle} is visible. + */ + Result isAboveAppWindowVisible(String windowTitle) { + WindowTokenProto[] windowTokenProtos = mProto.windowManagerService + .rootWindowContainer + .displays[DEFAULT_DISPLAY].aboveAppWindows; + Result result = isWindowVisible(windowTitle, windowTokenProtos); + return new Result(result.success, getTimestamp(), "showsAboveAppWindow", result.reason); + } + + /** + * Checks if belowAppWindow with {@code windowTitle} is visible. + */ + Result isBelowAppWindowVisible(String windowTitle) { + WindowTokenProto[] windowTokenProtos = mProto.windowManagerService + .rootWindowContainer + .displays[DEFAULT_DISPLAY].belowAppWindows; + Result result = isWindowVisible(windowTitle, windowTokenProtos); + return new Result(result.success, getTimestamp(), "isBelowAppWindowVisible", + result.reason); + } + + /** + * Checks if imeWindow with {@code windowTitle} is visible. + */ + Result isImeWindowVisible(String windowTitle) { + WindowTokenProto[] windowTokenProtos = mProto.windowManagerService + .rootWindowContainer + .displays[DEFAULT_DISPLAY].imeWindows; + Result result = isWindowVisible(windowTitle, windowTokenProtos); + return new Result(result.success, getTimestamp(), "isImeWindowVisible", + result.reason); + } + + /** + * Checks if app window with {@code windowTitle} is on top. + */ + Result isVisibleAppWindowOnTop(String windowTitle) { + String topAppWindow = getTopVisibleAppWindow(); + boolean success = topAppWindow.contains(windowTitle); + String reason = "wanted=" + windowTitle + " found=" + topAppWindow; + return new Result(success, getTimestamp(), "isAppWindowOnTop", reason); + } + + /** + * Checks if app window with {@code windowTitle} is visible. + */ + Result isAppWindowVisible(String windowTitle) { + final String assertionName = "isAppWindowVisible"; + boolean titleFound = false; + StackProto[] stacks = mProto.windowManagerService.rootWindowContainer + .displays[DEFAULT_DISPLAY].stacks; + for (StackProto stack : stacks) { + for (TaskProto task : stack.tasks) { + for (AppWindowTokenProto token : task.appWindowTokens) { + if (token.name.contains(windowTitle)) { + titleFound = true; + for (WindowStateProto windowState : token.windowToken.windows) { + if (windowState.windowContainer.visible) { + return new Result(true /* success */, getTimestamp(), + assertionName, "Window " + token.name + + "is visible"); + } + } + } + } + } + } + String reason; + if (!titleFound) { + reason = "Window " + windowTitle + " cannot be found"; + } else { + reason = "Window " + windowTitle + " is invisible"; + } + return new Result(false /* success */, getTimestamp(), assertionName, reason); + } + } +}
\ No newline at end of file diff --git a/tests/FlickerTests/lib/src/com/android/server/wm/flicker/WindowUtils.java b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/WindowUtils.java new file mode 100644 index 000000000000..c54396f895e4 --- /dev/null +++ b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/WindowUtils.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.Rect; +import android.view.Surface; +import android.view.WindowManager; + +import androidx.test.InstrumentationRegistry; + +/** + * Helper functions to retrieve system window sizes and positions. + */ +class WindowUtils { + + static Rect getDisplayBounds() { + Point display = new Point(); + WindowManager wm = + (WindowManager) InstrumentationRegistry.getContext().getSystemService( + Context.WINDOW_SERVICE); + wm.getDefaultDisplay().getRealSize(display); + return new Rect(0, 0, display.x, display.y); + } + + private static int getCurrentRotation() { + WindowManager wm = + (WindowManager) InstrumentationRegistry.getContext().getSystemService( + Context.WINDOW_SERVICE); + return wm.getDefaultDisplay().getRotation(); + } + + static Rect getDisplayBounds(int requestedRotation) { + Rect displayBounds = getDisplayBounds(); + int currentDisplayRotation = getCurrentRotation(); + + boolean displayIsRotated = (currentDisplayRotation == Surface.ROTATION_90 || + currentDisplayRotation == Surface.ROTATION_270); + + boolean requestedDisplayIsRotated = requestedRotation == Surface.ROTATION_90 || + requestedRotation == Surface.ROTATION_270; + + // if the current orientation changes with the requested rotation, + // flip height and width of display bounds. + if (displayIsRotated != requestedDisplayIsRotated) { + return new Rect(0, 0, displayBounds.height(), displayBounds.width()); + } + + return new Rect(0, 0, displayBounds.width(), displayBounds.height()); + } + + + static Rect getAppPosition(int requestedRotation) { + Rect displayBounds = getDisplayBounds(); + int currentDisplayRotation = getCurrentRotation(); + + boolean displayIsRotated = currentDisplayRotation == Surface.ROTATION_90 || + currentDisplayRotation == Surface.ROTATION_270; + + boolean requestedAppIsRotated = requestedRotation == Surface.ROTATION_90 || + requestedRotation == Surface.ROTATION_270; + + // display size will change if the display is reflected. Flip height and width of app if the + // requested rotation is different from the current rotation. + if (displayIsRotated != requestedAppIsRotated) { + return new Rect(0, 0, displayBounds.height(), displayBounds.width()); + } + + return new Rect(0, 0, displayBounds.width(), displayBounds.height()); + } + + static Rect getStatusBarPosition(int requestedRotation) { + Resources resources = InstrumentationRegistry.getContext().getResources(); + String resourceName; + Rect displayBounds = getDisplayBounds(); + int width; + if (requestedRotation == Surface.ROTATION_0 || requestedRotation == Surface.ROTATION_180) { + resourceName = "status_bar_height_portrait"; + width = Math.min(displayBounds.width(), displayBounds.height()); + } else { + resourceName = "status_bar_height_landscape"; + width = Math.max(displayBounds.width(), displayBounds.height()); + } + + int resourceId = resources.getIdentifier(resourceName, "dimen", "android"); + int height = resources.getDimensionPixelSize(resourceId); + + return new Rect(0, 0, width, height); + } + + static Rect getNavigationBarPosition(int requestedRotation) { + Resources resources = InstrumentationRegistry.getContext().getResources(); + Rect displayBounds = getDisplayBounds(); + int displayWidth = Math.min(displayBounds.width(), displayBounds.height()); + int displayHeight = Math.max(displayBounds.width(), displayBounds.height()); + int resourceId; + if (requestedRotation == Surface.ROTATION_0 || requestedRotation == Surface.ROTATION_180) { + resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android"); + int height = resources.getDimensionPixelSize(resourceId); + return new Rect(0, displayHeight - height, displayWidth, displayHeight); + } else { + resourceId = resources.getIdentifier("navigation_bar_width", "dimen", "android"); + int width = resources.getDimensionPixelSize(resourceId); + // swap display dimensions in landscape or seascape mode + int temp = displayHeight; + displayHeight = displayWidth; + displayWidth = temp; + if (requestedRotation == Surface.ROTATION_90) { + return new Rect(0, 0, width, displayHeight); + } else { + return new Rect(displayWidth - width, 0, displayWidth, displayHeight); + } + } + } + + static int getNavigationBarHeight() { + Resources resources = InstrumentationRegistry.getContext().getResources(); + int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android"); + return resources.getDimensionPixelSize(resourceId); + } + + static int getDockedStackDividerInset() { + Resources resources = InstrumentationRegistry.getContext().getResources(); + int resourceId = resources.getIdentifier("docked_stack_divider_insets", "dimen", + "android"); + return resources.getDimensionPixelSize(resourceId); + } +} diff --git a/tests/FlickerTests/lib/src/com/android/server/wm/flicker/WmTraceSubject.java b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/WmTraceSubject.java new file mode 100644 index 000000000000..e76da6e90834 --- /dev/null +++ b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/WmTraceSubject.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import static com.google.common.truth.Truth.assertAbout; +import static com.google.common.truth.Truth.assertWithMessage; + +import android.annotation.Nullable; + +import com.android.server.wm.flicker.Assertions.Result; +import com.android.server.wm.flicker.TransitionRunner.TransitionResult; + +import com.google.common.truth.FailureMetadata; +import com.google.common.truth.Subject; + +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Truth subject for {@link WindowManagerTrace} objects. + */ +public class WmTraceSubject extends Subject<WmTraceSubject, WindowManagerTrace> { + // Boiler-plate Subject.Factory for WmTraceSubject + private static final Subject.Factory<WmTraceSubject, WindowManagerTrace> FACTORY = + new Subject.Factory<WmTraceSubject, WindowManagerTrace>() { + @Override + public WmTraceSubject createSubject( + FailureMetadata fm, @Nullable WindowManagerTrace target) { + return new WmTraceSubject(fm, target); + } + }; + + private AssertionsChecker<WindowManagerTrace.Entry> mChecker = new AssertionsChecker<>(); + + private WmTraceSubject(FailureMetadata fm, @Nullable WindowManagerTrace subject) { + super(fm, subject); + } + + // User-defined entry point + public static WmTraceSubject assertThat(@Nullable WindowManagerTrace entry) { + return assertAbout(FACTORY).that(entry); + } + + // User-defined entry point + public static WmTraceSubject assertThat(@Nullable TransitionResult result) { + WindowManagerTrace entries = WindowManagerTrace.parseFrom(result.getWindowManagerTrace(), + result.getWindowManagerTracePath()); + return assertWithMessage(result.toString()).about(FACTORY).that(entries); + } + + // Static method for getting the subject factory (for use with assertAbout()) + public static Subject.Factory<WmTraceSubject, WindowManagerTrace> entries() { + return FACTORY; + } + + public void forAllEntries() { + test(); + } + + public void forRange(long startTime, long endTime) { + mChecker.filterByRange(startTime, endTime); + test(); + } + + public WmTraceSubject then() { + mChecker.checkChangingAssertions(); + return this; + } + + public void inTheBeginning() { + if (getSubject().getEntries().isEmpty()) { + fail("No entries found."); + } + mChecker.checkFirstEntry(); + test(); + } + + public void atTheEnd() { + if (getSubject().getEntries().isEmpty()) { + fail("No entries found."); + } + mChecker.checkLastEntry(); + test(); + } + + private void test() { + List<Result> failures = mChecker.test(getSubject().getEntries()); + if (!failures.isEmpty()) { + Optional<Path> failureTracePath = getSubject().getSource(); + String failureLogs = failures.stream().map(Result::toString) + .collect(Collectors.joining("\n")); + String tracePath = ""; + if (failureTracePath.isPresent()) { + tracePath = "\nWindowManager Trace can be found in: " + + failureTracePath.get().toAbsolutePath() + "\n"; + } + fail(tracePath + failureLogs); + } + } + + public WmTraceSubject showsAboveAppWindow(String partialWindowTitle) { + mChecker.add(entry -> entry.isAboveAppWindowVisible(partialWindowTitle), + "showsAboveAppWindow(" + partialWindowTitle + ")"); + return this; + } + + public WmTraceSubject hidesAboveAppWindow(String partialWindowTitle) { + mChecker.add(entry -> entry.isAboveAppWindowVisible(partialWindowTitle).negate(), + "hidesAboveAppWindow" + "(" + partialWindowTitle + ")"); + return this; + } + + public WmTraceSubject showsBelowAppWindow(String partialWindowTitle) { + mChecker.add(entry -> entry.isBelowAppWindowVisible(partialWindowTitle), + "showsBelowAppWindow(" + partialWindowTitle + ")"); + return this; + } + + public WmTraceSubject hidesBelowAppWindow(String partialWindowTitle) { + mChecker.add(entry -> entry.isBelowAppWindowVisible(partialWindowTitle).negate(), + "hidesBelowAppWindow" + "(" + partialWindowTitle + ")"); + return this; + } + + public WmTraceSubject showsImeWindow(String partialWindowTitle) { + mChecker.add(entry -> entry.isImeWindowVisible(partialWindowTitle), + "showsBelowAppWindow(" + partialWindowTitle + ")"); + return this; + } + + public WmTraceSubject hidesImeWindow(String partialWindowTitle) { + mChecker.add(entry -> entry.isImeWindowVisible(partialWindowTitle).negate(), + "hidesImeWindow" + "(" + partialWindowTitle + ")"); + return this; + } + + public WmTraceSubject showsAppWindowOnTop(String partialWindowTitle) { + mChecker.add( + entry -> { + Result result = entry.isAppWindowVisible(partialWindowTitle); + if (result.passed()) { + result = entry.isVisibleAppWindowOnTop(partialWindowTitle); + } + return result; + }, + "showsAppWindowOnTop(" + partialWindowTitle + ")" + ); + return this; + } + + public WmTraceSubject hidesAppWindowOnTop(String partialWindowTitle) { + mChecker.add( + entry -> { + Result result = entry.isAppWindowVisible(partialWindowTitle).negate(); + if (result.failed()) { + result = entry.isVisibleAppWindowOnTop(partialWindowTitle).negate(); + } + return result; + }, + "hidesAppWindowOnTop(" + partialWindowTitle + ")" + ); + return this; + } + + public WmTraceSubject showsAppWindow(String partialWindowTitle) { + mChecker.add(entry -> entry.isAppWindowVisible(partialWindowTitle), + "showsAppWindow(" + partialWindowTitle + ")"); + return this; + } + + public WmTraceSubject hidesAppWindow(String partialWindowTitle) { + mChecker.add(entry -> entry.isAppWindowVisible(partialWindowTitle).negate(), + "hidesAppWindow(" + partialWindowTitle + ")"); + return this; + } +} diff --git a/tests/FlickerTests/lib/src/com/android/server/wm/flicker/monitor/ITransitionMonitor.java b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/monitor/ITransitionMonitor.java new file mode 100644 index 000000000000..67e0ecc1cde7 --- /dev/null +++ b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/monitor/ITransitionMonitor.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.monitor; + +import android.os.Environment; + +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Collects test artifacts during a UI transition. + */ +public interface ITransitionMonitor { + Path OUTPUT_DIR = Paths.get(Environment.getExternalStorageDirectory().toString(), "flicker"); + + /** + * Starts monitor. + */ + void start(); + + /** + * Stops monitor. + */ + void stop(); + + /** + * Saves any monitor artifacts to file adding {@code testTag} and {@code iteration} + * to the file name. + * + * @param testTag suffix added to artifact name + * @param iteration suffix added to artifact name + * + * @return Path to saved artifact + */ + default Path save(String testTag, int iteration) { + return save(testTag + "_" + iteration); + } + + /** + * Saves any monitor artifacts to file adding {@code testTag} to the file name. + * + * @param testTag suffix added to artifact name + * + * @return Path to saved artifact + */ + default Path save(String testTag) { + throw new UnsupportedOperationException("Save not implemented for this monitor"); + } +} diff --git a/tests/FlickerTests/lib/src/com/android/server/wm/flicker/monitor/LayersTraceMonitor.java b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/monitor/LayersTraceMonitor.java new file mode 100644 index 000000000000..c55d068b41b8 --- /dev/null +++ b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/monitor/LayersTraceMonitor.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.monitor; + +import android.os.IBinder; +import android.os.Parcel; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; + +/** + * Captures Layers trace from SurfaceFlinger. + */ +public class LayersTraceMonitor extends TraceMonitor { + private static final String TAG = "LayersTraceMonitor"; + private IBinder mSurfaceFlinger = ServiceManager.getService("SurfaceFlinger"); + + public LayersTraceMonitor() { + traceFileName = "layers_trace.pb"; + } + + @Override + public void start() { + setEnabled(true); + } + + @Override + public void stop() { + setEnabled(false); + } + + @Override + public boolean isEnabled() throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken("android.ui.ISurfaceComposer"); + mSurfaceFlinger.transact(/* LAYER_TRACE_STATUS_CODE */ 1026, + data, reply, 0 /* flags */); + return reply.readBoolean(); + } + + private void setEnabled(boolean isEnabled) { + Parcel data = null; + try { + if (mSurfaceFlinger != null) { + data = Parcel.obtain(); + data.writeInterfaceToken("android.ui.ISurfaceComposer"); + data.writeInt(isEnabled ? 1 : 0); + mSurfaceFlinger.transact( /* LAYER_TRACE_CONTROL_CODE */ 1025, + data, null, 0 /* flags */); + } + } catch (RemoteException e) { + Log.e(TAG, "Could not set layer tracing." + e.toString()); + } finally { + if (data != null) { + data.recycle(); + } + } + } +} diff --git a/tests/FlickerTests/lib/src/com/android/server/wm/flicker/monitor/ScreenRecorder.java b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/monitor/ScreenRecorder.java new file mode 100644 index 000000000000..4787586777ae --- /dev/null +++ b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/monitor/ScreenRecorder.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.monitor; + +import static com.android.compatibility.common.util.SystemUtil.runShellCommand; + +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; + +import android.support.annotation.VisibleForTesting; +import android.util.Log; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Captures screen contents and saves it as a mp4 video file. + */ +public class ScreenRecorder implements ITransitionMonitor { + @VisibleForTesting + static final Path DEFAULT_OUTPUT_PATH = OUTPUT_DIR.resolve("transition.mp4"); + private static final String TAG = "FLICKER"; + private Thread recorderThread; + + @VisibleForTesting + static Path getPath(String testTag) { + return OUTPUT_DIR.resolve(testTag + ".mp4"); + } + + @Override + public void start() { + OUTPUT_DIR.toFile().mkdirs(); + String command = "screenrecord " + DEFAULT_OUTPUT_PATH; + recorderThread = new Thread(() -> { + try { + Runtime.getRuntime().exec(command); + } catch (IOException e) { + Log.e(TAG, "Error executing " + command, e); + } + }); + recorderThread.start(); + } + + @Override + public void stop() { + runShellCommand("killall -s 2 screenrecord"); + try { + recorderThread.join(); + } catch (InterruptedException e) { + // ignore + } + } + + @Override + public Path save(String testTag) { + try { + return Files.move(DEFAULT_OUTPUT_PATH, getPath(testTag), + REPLACE_EXISTING); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/tests/FlickerTests/lib/src/com/android/server/wm/flicker/monitor/TraceMonitor.java b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/monitor/TraceMonitor.java new file mode 100644 index 000000000000..0e154ecd5d4d --- /dev/null +++ b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/monitor/TraceMonitor.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.monitor; + +import static com.android.compatibility.common.util.SystemUtil.runShellCommand; + +import android.os.RemoteException; + +import com.android.internal.annotations.VisibleForTesting; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Locale; + +/** + * Base class for monitors containing common logic to read the trace + * as a byte array and save the trace to another location. + */ +public abstract class TraceMonitor implements ITransitionMonitor { + public static final String TAG = "FLICKER"; + private static final String TRACE_DIR = "/data/misc/wmtrace/"; + + String traceFileName; + + abstract boolean isEnabled() throws RemoteException; + + /** + * Saves trace file to the external storage directory suffixing the name with the testtag + * and iteration. + * + * Moves the trace file from the default location via a shell command since the test app + * does not have security privileges to access /data/misc/wmtrace. + * + * @param testTag suffix added to trace name used to identify trace + * + * @return Path to saved trace file + */ + @Override + public Path save(String testTag) { + OUTPUT_DIR.toFile().mkdirs(); + Path traceFileCopy = getOutputTraceFilePath(testTag); + String copyCommand = String.format(Locale.getDefault(), "mv %s%s %s", TRACE_DIR, + traceFileName, traceFileCopy.toString()); + runShellCommand(copyCommand); + return traceFileCopy; + } + + @VisibleForTesting + Path getOutputTraceFilePath(String testTag) { + return OUTPUT_DIR.resolve(traceFileName + "_" + testTag); + } +} diff --git a/tests/FlickerTests/lib/src/com/android/server/wm/flicker/monitor/WindowAnimationFrameStatsMonitor.java b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/monitor/WindowAnimationFrameStatsMonitor.java new file mode 100644 index 000000000000..3f86f0d001d7 --- /dev/null +++ b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/monitor/WindowAnimationFrameStatsMonitor.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.monitor; + +import static android.view.FrameStats.UNDEFINED_TIME_NANO; + +import android.app.Instrumentation; +import android.util.Log; +import android.view.FrameStats; + +/** + * Monitors {@link android.view.WindowAnimationFrameStats} to detect janky frames. + * + * Adapted from {@link androidx.test.jank.internal.WindowAnimationFrameStatsMonitorImpl} + * using the same threshold to determine jank. + */ +public class WindowAnimationFrameStatsMonitor implements ITransitionMonitor { + + private static final String TAG = "FLICKER"; + // Maximum normalized error in frame duration before the frame is considered janky + private static final double MAX_ERROR = 0.5f; + // Maximum normalized frame duration before the frame is considered a pause + private static final double PAUSE_THRESHOLD = 15.0f; + private Instrumentation mInstrumentation; + private FrameStats stats; + private int numJankyFrames; + private long mLongestFrameNano = 0L; + + + /** + * Constructs a WindowAnimationFrameStatsMonitor instance. + */ + public WindowAnimationFrameStatsMonitor(Instrumentation instrumentation) { + mInstrumentation = instrumentation; + } + + private void analyze() { + int frameCount = stats.getFrameCount(); + long refreshPeriodNano = stats.getRefreshPeriodNano(); + + // Skip first frame + for (int i = 2; i < frameCount; i++) { + // Handle frames that have not been presented. + if (stats.getFramePresentedTimeNano(i) == UNDEFINED_TIME_NANO) { + // The animation must not have completed. Warn and break out of the loop. + Log.w(TAG, "Skipping fenced frame."); + break; + } + long frameDurationNano = stats.getFramePresentedTimeNano(i) - + stats.getFramePresentedTimeNano(i - 1); + double normalized = (double) frameDurationNano / refreshPeriodNano; + if (normalized < PAUSE_THRESHOLD) { + if (normalized > 1.0f + MAX_ERROR) { + numJankyFrames++; + } + mLongestFrameNano = Math.max(mLongestFrameNano, frameDurationNano); + } + } + } + + @Override + public void start() { + // Clear out any previous data + numJankyFrames = 0; + mLongestFrameNano = 0; + mInstrumentation.getUiAutomation().clearWindowAnimationFrameStats(); + } + + @Override + public void stop() { + stats = mInstrumentation.getUiAutomation().getWindowAnimationFrameStats(); + analyze(); + } + + public boolean jankyFramesDetected() { + return stats.getFrameCount() > 0 && numJankyFrames > 0; + } + + @Override + public String toString() { + return stats.toString() + + " RefreshPeriodNano:" + stats.getRefreshPeriodNano() + + " NumJankyFrames:" + numJankyFrames + + " LongestFrameNano:" + mLongestFrameNano; + } +}
\ No newline at end of file diff --git a/tests/FlickerTests/lib/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitor.java b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitor.java new file mode 100644 index 000000000000..ae160b68c976 --- /dev/null +++ b/tests/FlickerTests/lib/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitor.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.monitor; + +import android.os.RemoteException; +import android.view.IWindowManager; +import android.view.WindowManagerGlobal; + +/** + * Captures WindowManager trace from WindowManager. + */ +public class WindowManagerTraceMonitor extends TraceMonitor { + private IWindowManager wm = WindowManagerGlobal.getWindowManagerService(); + + public WindowManagerTraceMonitor() { + traceFileName = "wm_trace.pb"; + } + + @Override + public void start() { + try { + wm.startWindowTrace(); + } catch (RemoteException e) { + throw new RuntimeException("Could not start trace", e); + } + } + + @Override + public void stop() { + try { + wm.stopWindowTrace(); + } catch (RemoteException e) { + throw new RuntimeException("Could not stop trace", e); + } + } + + @Override + public boolean isEnabled() throws RemoteException{ + return wm.isWindowTraceEnabled(); + } +} diff --git a/tests/FlickerTests/lib/test/Android.bp b/tests/FlickerTests/lib/test/Android.bp new file mode 100644 index 000000000000..bfeb75b23469 --- /dev/null +++ b/tests/FlickerTests/lib/test/Android.bp @@ -0,0 +1,33 @@ +// +// Copyright (C) 2018 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +android_test { + name: "FlickerLibTest", + // sign this with platform cert, so this test is allowed to call private platform apis + certificate: "platform", + platform_apis: true, + test_suites: ["tests"], + srcs: ["src/**/*.java"], + libs: ["android.test.runner"], + static_libs: [ + "androidx.test.rules", + "platform-test-annotations", + "truth-prebuilt", + "platformprotosnano", + "layersprotosnano", + "flickerlib", + ], +} diff --git a/tests/FlickerTests/lib/test/AndroidManifest.xml b/tests/FlickerTests/lib/test/AndroidManifest.xml new file mode 100644 index 000000000000..6451a5710821 --- /dev/null +++ b/tests/FlickerTests/lib/test/AndroidManifest.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + * Copyright 2018 Google Inc. All Rights Reserved. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.server.wm.flicker"> + + <uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27"/> + <!-- Read and write traces from external storage --> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> + <!-- Capture screen contents --> + <uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER" /> + <!-- Run layers trace --> + <uses-permission android:name="android.permission.HARDWARE_TEST"/> + <application android:label="FlickerLibTest"> + <uses-library android:name="android.test.runner"/> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.server.wm.flicker" + android:label="WindowManager Flicker Lib Test"> + </instrumentation> + +</manifest>
\ No newline at end of file diff --git a/tests/FlickerTests/lib/test/AndroidTest.xml b/tests/FlickerTests/lib/test/AndroidTest.xml new file mode 100644 index 000000000000..e4cc298a2aa8 --- /dev/null +++ b/tests/FlickerTests/lib/test/AndroidTest.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + * Copyright 2018 Google Inc. All Rights Reserved. + --> +<configuration description="Config for WindowManager Flicker Tests"> + <target_preparer class="com.google.android.tradefed.targetprep.GoogleDeviceSetup"> + <!-- keeps the screen on during tests --> + <option name="screen-always-on" value="on" /> + <!-- prevents the phone from restarting --> + <option name="force-skip-system-props" value="true" /> + </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true"/> + <option name="test-file-name" value="FlickerLibTest.apk"/> + </target_preparer> + <test class="com.android.tradefed.testtype.AndroidJUnitTest"> + <option name="package" value="com.android.server.wm.flicker"/> + <option name="hidden-api-checks" value="false" /> + </test> +</configuration> diff --git a/tests/FlickerTests/lib/test/assets/testdata/layers_trace_emptyregion.pb b/tests/FlickerTests/lib/test/assets/testdata/layers_trace_emptyregion.pb Binary files differnew file mode 100644 index 000000000000..98ee6f3ed269 --- /dev/null +++ b/tests/FlickerTests/lib/test/assets/testdata/layers_trace_emptyregion.pb diff --git a/tests/FlickerTests/lib/test/assets/testdata/layers_trace_invalid_layer_visibility.pb b/tests/FlickerTests/lib/test/assets/testdata/layers_trace_invalid_layer_visibility.pb Binary files differnew file mode 100644 index 000000000000..20572d79d826 --- /dev/null +++ b/tests/FlickerTests/lib/test/assets/testdata/layers_trace_invalid_layer_visibility.pb diff --git a/tests/FlickerTests/lib/test/assets/testdata/layers_trace_orphanlayers.pb b/tests/FlickerTests/lib/test/assets/testdata/layers_trace_orphanlayers.pb Binary files differnew file mode 100644 index 000000000000..af4079707c69 --- /dev/null +++ b/tests/FlickerTests/lib/test/assets/testdata/layers_trace_orphanlayers.pb diff --git a/tests/FlickerTests/lib/test/assets/testdata/wm_trace_openchrome.pb b/tests/FlickerTests/lib/test/assets/testdata/wm_trace_openchrome.pb Binary files differnew file mode 100644 index 000000000000..b3f31706f55c --- /dev/null +++ b/tests/FlickerTests/lib/test/assets/testdata/wm_trace_openchrome.pb diff --git a/tests/FlickerTests/lib/test/assets/testdata/wm_trace_openchrome2.pb b/tests/FlickerTests/lib/test/assets/testdata/wm_trace_openchrome2.pb Binary files differnew file mode 100644 index 000000000000..b3b73ce0518a --- /dev/null +++ b/tests/FlickerTests/lib/test/assets/testdata/wm_trace_openchrome2.pb diff --git a/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/AssertionsCheckerTest.java b/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/AssertionsCheckerTest.java new file mode 100644 index 000000000000..8e7fe1b4f942 --- /dev/null +++ b/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/AssertionsCheckerTest.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import static com.google.common.truth.Truth.assertThat; + +import com.android.server.wm.flicker.Assertions.Result; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +/** + * Contains {@link AssertionsChecker} tests. + * To run this test: {@code atest FlickerLibTest:AssertionsCheckerTest} + */ +public class AssertionsCheckerTest { + + /** + * Returns a list of SimpleEntry objects with {@code data} and incremental timestamps starting + * at 0. + */ + private static List<SimpleEntry> getTestEntries(int... data) { + List<SimpleEntry> entries = new ArrayList<>(); + for (int i = 0; i < data.length; i++) { + entries.add(new SimpleEntry(i, data[i])); + } + return entries; + } + + @Test + public void canCheckAllEntries() { + AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>(); + checker.add(SimpleEntry::isData42, "isData42"); + + List<Result> failures = checker.test(getTestEntries(1, 1, 1, 1, 1)); + + assertThat(failures).hasSize(5); + } + + @Test + public void canCheckFirstEntry() { + AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>(); + checker.checkFirstEntry(); + checker.add(SimpleEntry::isData42, "isData42"); + + List<Result> failures = checker.test(getTestEntries(1, 1, 1, 1, 1)); + + assertThat(failures).hasSize(1); + assertThat(failures.get(0).timestamp).isEqualTo(0); + } + + @Test + public void canCheckLastEntry() { + AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>(); + checker.checkLastEntry(); + checker.add(SimpleEntry::isData42, "isData42"); + + List<Result> failures = checker.test(getTestEntries(1, 1, 1, 1, 1)); + + assertThat(failures).hasSize(1); + assertThat(failures.get(0).timestamp).isEqualTo(4); + } + + @Test + public void canCheckRangeOfEntries() { + AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>(); + checker.filterByRange(1, 2); + checker.add(SimpleEntry::isData42, "isData42"); + + List<Result> failures = checker.test(getTestEntries(1, 42, 42, 1, 1)); + + assertThat(failures).hasSize(0); + } + + @Test + public void emptyRangePasses() { + AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>(); + checker.filterByRange(9, 10); + checker.add(SimpleEntry::isData42, "isData42"); + + List<Result> failures = checker.test(getTestEntries(1, 1, 1, 1, 1)); + + assertThat(failures).isEmpty(); + } + + @Test + public void canCheckChangingAssertions() { + AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>(); + checker.add(SimpleEntry::isData42, "isData42"); + checker.add(SimpleEntry::isData0, "isData0"); + checker.checkChangingAssertions(); + + List<Result> failures = checker.test(getTestEntries(42, 0, 0, 0, 0)); + + assertThat(failures).isEmpty(); + } + + @Test + public void canCheckChangingAssertions_withNoAssertions() { + AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>(); + checker.checkChangingAssertions(); + + List<Result> failures = checker.test(getTestEntries(42, 0, 0, 0, 0)); + + assertThat(failures).isEmpty(); + } + + @Test + public void canCheckChangingAssertions_withSingleAssertion() { + AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>(); + checker.add(SimpleEntry::isData42, "isData42"); + checker.checkChangingAssertions(); + + List<Result> failures = checker.test(getTestEntries(42, 42, 42, 42, 42)); + + assertThat(failures).isEmpty(); + } + + @Test + public void canFailCheckChangingAssertions_ifStartingAssertionFails() { + AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>(); + checker.add(SimpleEntry::isData42, "isData42"); + checker.add(SimpleEntry::isData0, "isData0"); + checker.checkChangingAssertions(); + + List<Result> failures = checker.test(getTestEntries(0, 0, 0, 0, 0)); + + assertThat(failures).hasSize(1); + } + + @Test + public void canFailCheckChangingAssertions_ifStartingAssertionAlwaysPasses() { + AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>(); + checker.add(SimpleEntry::isData42, "isData42"); + checker.add(SimpleEntry::isData0, "isData0"); + checker.checkChangingAssertions(); + + List<Result> failures = checker.test(getTestEntries(0, 0, 0, 0, 0)); + + assertThat(failures).hasSize(1); + } + + static class SimpleEntry implements ITraceEntry { + long timestamp; + int data; + + SimpleEntry(long timestamp, int data) { + this.timestamp = timestamp; + this.data = data; + } + + @Override + public long getTimestamp() { + return timestamp; + } + + Result isData42() { + return new Result(this.data == 42, this.timestamp, "is42", ""); + } + + Result isData0() { + return new Result(this.data == 0, this.timestamp, "is42", ""); + } + } +} diff --git a/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/AssertionsTest.java b/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/AssertionsTest.java new file mode 100644 index 000000000000..7fd178ca6e51 --- /dev/null +++ b/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/AssertionsTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import static com.google.common.truth.Truth.assertThat; + +import com.android.server.wm.flicker.Assertions.Result; + +import org.junit.Test; + +/** + * Contains {@link Assertions} tests. + * To run this test: {@code atest FlickerLibTest:AssertionsTest} + */ +public class AssertionsTest { + @Test + public void traceEntryAssertionCanNegateResult() { + Assertions.TraceAssertion<Integer> assertNumEquals42 = + getIntegerTraceEntryAssertion(); + + assertThat(assertNumEquals42.apply(1).success).isFalse(); + assertThat(assertNumEquals42.negate().apply(1).success).isTrue(); + + assertThat(assertNumEquals42.apply(42).success).isTrue(); + assertThat(assertNumEquals42.negate().apply(42).success).isFalse(); + } + + @Test + public void resultCanBeNegated() { + String reason = "Everything is fine!"; + Result result = new Result(true, 0, "TestAssert", reason); + Result negatedResult = result.negate(); + assertThat(negatedResult.success).isFalse(); + assertThat(negatedResult.reason).isEqualTo(reason); + assertThat(negatedResult.assertionName).isEqualTo("!TestAssert"); + } + + private Assertions.TraceAssertion<Integer> getIntegerTraceEntryAssertion() { + return (num) -> { + if (num == 42) { + return new Result(true, "Num equals 42"); + } + return new Result(false, "Num doesn't equal 42, actual:" + num); + }; + } +}
\ No newline at end of file diff --git a/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/LayersTraceSubjectTest.java b/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/LayersTraceSubjectTest.java new file mode 100644 index 000000000000..d06c5d76552b --- /dev/null +++ b/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/LayersTraceSubjectTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import static com.android.server.wm.flicker.LayersTraceSubject.assertThat; +import static com.android.server.wm.flicker.TestFileUtils.readTestFile; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.junit.Assert.fail; + +import android.graphics.Rect; + +import org.junit.Test; + +import java.nio.file.Paths; + +/** + * Contains {@link LayersTraceSubject} tests. + * To run this test: {@code atest FlickerLibTest:LayersTraceSubjectTest} + */ +public class LayersTraceSubjectTest { + private static final Rect displayRect = new Rect(0, 0, 1440, 2880); + + private static LayersTrace readLayerTraceFromFile(String relativePath) { + try { + return LayersTrace.parseFrom(readTestFile(relativePath), Paths.get(relativePath)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testCanDetectEmptyRegionFromLayerTrace() { + LayersTrace layersTraceEntries = readLayerTraceFromFile("layers_trace_emptyregion.pb"); + try { + assertThat(layersTraceEntries).coversRegion(displayRect).forAllEntries(); + fail("Assertion passed"); + } catch (AssertionError e) { + assertWithMessage("Contains path to trace") + .that(e.getMessage()).contains("layers_trace_emptyregion.pb"); + assertWithMessage("Contains timestamp") + .that(e.getMessage()).contains("0h38m28s8ms"); + assertWithMessage("Contains assertion function") + .that(e.getMessage()).contains("coversRegion"); + assertWithMessage("Contains debug info") + .that(e.getMessage()).contains("Region to test: " + displayRect); + assertWithMessage("Contains debug info") + .that(e.getMessage()).contains("first empty point: 0, 99"); + } + } + + @Test + public void testCanDetectIncorrectVisibilityFromLayerTrace() { + LayersTrace layersTraceEntries = readLayerTraceFromFile( + "layers_trace_invalid_layer_visibility.pb"); + try { + assertThat(layersTraceEntries).showsLayer("com.android.server.wm.flicker.testapp") + .then().hidesLayer("com.android.server.wm.flicker.testapp").forAllEntries(); + fail("Assertion passed"); + } catch (AssertionError e) { + assertWithMessage("Contains path to trace") + .that(e.getMessage()).contains("layers_trace_invalid_layer_visibility.pb"); + assertWithMessage("Contains timestamp") + .that(e.getMessage()).contains("70h13m14s303ms"); + assertWithMessage("Contains assertion function") + .that(e.getMessage()).contains("!isVisible"); + assertWithMessage("Contains debug info") + .that(e.getMessage()).contains( + "com.android.server.wm.flicker.testapp/com.android.server.wm.flicker.testapp" + + ".SimpleActivity#0 is visible"); + } + } +} diff --git a/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/LayersTraceTest.java b/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/LayersTraceTest.java new file mode 100644 index 000000000000..7d77126fd7d4 --- /dev/null +++ b/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/LayersTraceTest.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import static com.android.server.wm.flicker.TestFileUtils.readTestFile; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.junit.Assert.fail; + +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.view.WindowManager; + +import androidx.test.InstrumentationRegistry; + +import org.junit.Test; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Contains {@link LayersTrace} tests. + * To run this test: {@code atest FlickerLibTest:LayersTraceTest} + */ +public class LayersTraceTest { + private static LayersTrace readLayerTraceFromFile(String relativePath) { + try { + return LayersTrace.parseFrom(readTestFile(relativePath)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static Rect getDisplayBounds() { + Point display = new Point(); + WindowManager wm = + (WindowManager) InstrumentationRegistry.getContext().getSystemService( + Context.WINDOW_SERVICE); + wm.getDefaultDisplay().getRealSize(display); + return new Rect(0, 0, display.x, display.y); + } + + @Test + public void canParseAllLayers() { + LayersTrace trace = readLayerTraceFromFile( + "layers_trace_emptyregion.pb"); + assertThat(trace.getEntries()).isNotEmpty(); + assertThat(trace.getEntries().get(0).getTimestamp()).isEqualTo(2307984557311L); + assertThat(trace.getEntries().get(trace.getEntries().size() - 1).getTimestamp()) + .isEqualTo(2308521813510L); + List<LayersTrace.Layer> flattenedLayers = trace.getEntries().get(0).asFlattenedLayers(); + String msg = "Layers:\n" + flattenedLayers.stream().map(layer -> layer.mProto.name) + .collect(Collectors.joining("\n\t")); + assertWithMessage(msg).that(flattenedLayers).hasSize(47); + } + + @Test + public void canParseVisibleLayers() { + LayersTrace trace = readLayerTraceFromFile( + "layers_trace_emptyregion.pb"); + assertThat(trace.getEntries()).isNotEmpty(); + assertThat(trace.getEntries().get(0).getTimestamp()).isEqualTo(2307984557311L); + assertThat(trace.getEntries().get(trace.getEntries().size() - 1).getTimestamp()) + .isEqualTo(2308521813510L); + List<LayersTrace.Layer> flattenedLayers = trace.getEntries().get(0).asFlattenedLayers(); + List<LayersTrace.Layer> visibleLayers = flattenedLayers.stream() + .filter(layer -> layer.isVisible() && !layer.isHiddenByParent()) + .collect(Collectors.toList()); + + String msg = "Visible Layers:\n" + visibleLayers.stream() + .map(layer -> layer.mProto.name) + .collect(Collectors.joining("\n\t")); + + assertWithMessage(msg).that(visibleLayers).hasSize(9); + } + + @Test + public void canParseLayerHierarchy() { + LayersTrace trace = readLayerTraceFromFile( + "layers_trace_emptyregion.pb"); + assertThat(trace.getEntries()).isNotEmpty(); + assertThat(trace.getEntries().get(0).getTimestamp()).isEqualTo(2307984557311L); + assertThat(trace.getEntries().get(trace.getEntries().size() - 1).getTimestamp()) + .isEqualTo(2308521813510L); + List<LayersTrace.Layer> layers = trace.getEntries().get(0).getRootLayers(); + assertThat(layers).hasSize(2); + assertThat(layers.get(0).mChildren).hasSize(layers.get(0).mProto.children.length); + assertThat(layers.get(1).mChildren).hasSize(layers.get(1).mProto.children.length); + } + + // b/76099859 + @Test + public void canDetectOrphanLayers() { + try { + readLayerTraceFromFile( + "layers_trace_orphanlayers.pb"); + fail("Failed to detect orphaned layers."); + } catch (RuntimeException exception) { + assertThat(exception.getMessage()).contains( + "Failed to parse layers trace. Found orphan layers " + + "with parent layer id:1006 : 49"); + } + } + + // b/75276931 + @Test + public void canDetectUncoveredRegion() { + LayersTrace trace = readLayerTraceFromFile( + "layers_trace_emptyregion.pb"); + LayersTrace.Entry entry = trace.getEntry(2308008331271L); + + Assertions.Result result = entry.coversRegion(getDisplayBounds()); + + assertThat(result.failed()).isTrue(); + assertThat(result.reason).contains("Region to test: Rect(0, 0 - 1440, 2880)"); + assertThat(result.reason).contains("first empty point: 0, 99"); + assertThat(result.reason).contains("visible regions:"); + assertWithMessage("Reason contains list of visible regions") + .that(result.reason).contains("StatusBar#0Rect(0, 0 - 1440, 98"); + } + + // Visible region tests + @Test + public void canTestLayerVisibleRegion_layerDoesNotExist() { + LayersTrace trace = readLayerTraceFromFile( + "layers_trace_emptyregion.pb"); + LayersTrace.Entry entry = trace.getEntry(2308008331271L); + + final Rect expectedVisibleRegion = new Rect(0, 0, 1, 1); + Assertions.Result result = entry.hasVisibleRegion("ImaginaryLayer", + expectedVisibleRegion); + + assertThat(result.failed()).isTrue(); + assertThat(result.reason).contains("Could not find ImaginaryLayer"); + } + + @Test + public void canTestLayerVisibleRegion_layerDoesNotHaveExpectedVisibleRegion() { + LayersTrace trace = readLayerTraceFromFile( + "layers_trace_emptyregion.pb"); + LayersTrace.Entry entry = trace.getEntry(2307993020072L); + + final Rect expectedVisibleRegion = new Rect(0, 0, 1, 1); + Assertions.Result result = entry.hasVisibleRegion("NexusLauncherActivity#2", + expectedVisibleRegion); + + assertThat(result.failed()).isTrue(); + assertThat(result.reason).contains( + "Layer com.google.android.apps.nexuslauncher/com.google.android.apps" + + ".nexuslauncher.NexusLauncherActivity#2 is invisible: activeBuffer=null" + + " type != ColorLayer flags=1 (FLAG_HIDDEN set) visible region is empty"); + } + + @Test + public void canTestLayerVisibleRegion_layerIsHiddenByParent() { + LayersTrace trace = readLayerTraceFromFile( + "layers_trace_emptyregion.pb"); + LayersTrace.Entry entry = trace.getEntry(2308455948035L); + + final Rect expectedVisibleRegion = new Rect(0, 0, 1, 1); + Assertions.Result result = entry.hasVisibleRegion( + "SurfaceView - com.android.chrome/com.google.android.apps.chrome.Main", + expectedVisibleRegion); + + assertThat(result.failed()).isTrue(); + assertThat(result.reason).contains( + "Layer SurfaceView - com.android.chrome/com.google.android.apps.chrome.Main#0 is " + + "hidden by parent: com.android.chrome/com.google.android.apps.chrome" + + ".Main#0"); + } + + @Test + public void canTestLayerVisibleRegion_incorrectRegionSize() { + LayersTrace trace = readLayerTraceFromFile( + "layers_trace_emptyregion.pb"); + LayersTrace.Entry entry = trace.getEntry(2308008331271L); + + final Rect expectedVisibleRegion = new Rect(0, 0, 1440, 99); + Assertions.Result result = entry.hasVisibleRegion( + "StatusBar", + expectedVisibleRegion); + + assertThat(result.failed()).isTrue(); + assertThat(result.reason).contains("StatusBar#0 has visible " + + "region:Rect(0, 0 - 1440, 98) expected:Rect(0, 0 - 1440, 99)"); + } + + @Test + public void canTestLayerVisibleRegion() { + LayersTrace trace = readLayerTraceFromFile( + "layers_trace_emptyregion.pb"); + LayersTrace.Entry entry = trace.getEntry(2308008331271L); + + final Rect expectedVisibleRegion = new Rect(0, 0, 1440, 98); + Assertions.Result result = entry.hasVisibleRegion("StatusBar", expectedVisibleRegion); + + assertThat(result.passed()).isTrue(); + } + + @Test + public void canTestLayerVisibleRegion_layerIsNotVisible() { + LayersTrace trace = readLayerTraceFromFile( + "layers_trace_invalid_layer_visibility.pb"); + LayersTrace.Entry entry = trace.getEntry(252794268378458L); + + Assertions.Result result = entry.isVisible("com.android.server.wm.flicker.testapp"); + assertThat(result.failed()).isTrue(); + assertThat(result.reason).contains( + "Layer com.android.server.wm.flicker.testapp/com.android.server.wm.flicker" + + ".testapp.SimpleActivity#0 is invisible: type != ColorLayer visible " + + "region is empty"); + } +} diff --git a/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/TestFileUtils.java b/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/TestFileUtils.java new file mode 100644 index 000000000000..c46175c1a977 --- /dev/null +++ b/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/TestFileUtils.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import android.content.Context; + +import androidx.test.InstrumentationRegistry; + +import com.google.common.io.ByteStreams; + +import java.io.InputStream; + +/** + * Helper functions for test file resources. + */ +class TestFileUtils { + static byte[] readTestFile(String relativePath) throws Exception { + Context context = InstrumentationRegistry.getContext(); + InputStream in = context.getResources().getAssets().open("testdata/" + relativePath); + return ByteStreams.toByteArray(in); + } +} diff --git a/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/TransitionRunnerTest.java b/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/TransitionRunnerTest.java new file mode 100644 index 000000000000..9c5e2059a0e6 --- /dev/null +++ b/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/TransitionRunnerTest.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import android.os.Environment; + +import com.android.server.wm.flicker.TransitionRunner.TransitionBuilder; +import com.android.server.wm.flicker.TransitionRunner.TransitionResult; +import com.android.server.wm.flicker.monitor.LayersTraceMonitor; +import com.android.server.wm.flicker.monitor.ScreenRecorder; +import com.android.server.wm.flicker.monitor.WindowAnimationFrameStatsMonitor; +import com.android.server.wm.flicker.monitor.WindowManagerTraceMonitor; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.IOException; +import java.nio.file.Paths; +import java.util.List; + +/** + * Contains {@link TransitionRunner} tests. + * {@code atest FlickerLibTest:TransitionRunnerTest} + */ +public class TransitionRunnerTest { + @Mock + private SimpleUiTransitions mTransitionsMock; + @Mock + private ScreenRecorder mScreenRecorderMock; + @Mock + private WindowManagerTraceMonitor mWindowManagerTraceMonitorMock; + @Mock + private LayersTraceMonitor mLayersTraceMonitorMock; + @Mock + private WindowAnimationFrameStatsMonitor mWindowAnimationFrameStatsMonitor; + @InjectMocks + private TransitionBuilder mTransitionBuilder; + + @Before + public void init() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void transitionsRunInOrder() { + TransitionRunner.newBuilder() + .runBeforeAll(mTransitionsMock::turnOnDevice) + .runBefore(mTransitionsMock::openApp) + .run(mTransitionsMock::performMagic) + .runAfter(mTransitionsMock::closeApp) + .runAfterAll(mTransitionsMock::cleanUpTracks) + .skipLayersTrace() + .skipWindowManagerTrace() + .build() + .run(); + + InOrder orderVerifier = inOrder(mTransitionsMock); + orderVerifier.verify(mTransitionsMock).turnOnDevice(); + orderVerifier.verify(mTransitionsMock).openApp(); + orderVerifier.verify(mTransitionsMock).performMagic(); + orderVerifier.verify(mTransitionsMock).closeApp(); + orderVerifier.verify(mTransitionsMock).cleanUpTracks(); + } + + @Test + public void canCombineTransitions() { + TransitionRunner.newBuilder() + .runBeforeAll(mTransitionsMock::turnOnDevice) + .runBeforeAll(mTransitionsMock::turnOnDevice) + .runBefore(mTransitionsMock::openApp) + .runBefore(mTransitionsMock::openApp) + .run(mTransitionsMock::performMagic) + .run(mTransitionsMock::performMagic) + .runAfter(mTransitionsMock::closeApp) + .runAfter(mTransitionsMock::closeApp) + .runAfterAll(mTransitionsMock::cleanUpTracks) + .runAfterAll(mTransitionsMock::cleanUpTracks) + .skipLayersTrace() + .skipWindowManagerTrace() + .build() + .run(); + + final int wantedNumberOfInvocations = 2; + verify(mTransitionsMock, times(wantedNumberOfInvocations)).turnOnDevice(); + verify(mTransitionsMock, times(wantedNumberOfInvocations)).openApp(); + verify(mTransitionsMock, times(wantedNumberOfInvocations)).performMagic(); + verify(mTransitionsMock, times(wantedNumberOfInvocations)).closeApp(); + verify(mTransitionsMock, times(wantedNumberOfInvocations)).cleanUpTracks(); + } + + @Test + public void emptyTransitionPasses() { + List<TransitionResult> results = TransitionRunner.newBuilder() + .skipLayersTrace() + .skipWindowManagerTrace() + .build() + .run() + .getResults(); + assertThat(results).hasSize(1); + assertThat(results.get(0).layersTraceExists()).isFalse(); + assertThat(results.get(0).windowManagerTraceExists()).isFalse(); + assertThat(results.get(0).screenCaptureVideoExists()).isFalse(); + } + + @Test + public void canRepeatTransitions() { + final int wantedNumberOfInvocations = 10; + TransitionRunner.newBuilder() + .runBeforeAll(mTransitionsMock::turnOnDevice) + .runBefore(mTransitionsMock::openApp) + .run(mTransitionsMock::performMagic) + .runAfter(mTransitionsMock::closeApp) + .runAfterAll(mTransitionsMock::cleanUpTracks) + .repeat(wantedNumberOfInvocations) + .skipLayersTrace() + .skipWindowManagerTrace() + .build() + .run(); + verify(mTransitionsMock).turnOnDevice(); + verify(mTransitionsMock, times(wantedNumberOfInvocations)).openApp(); + verify(mTransitionsMock, times(wantedNumberOfInvocations)).performMagic(); + verify(mTransitionsMock, times(wantedNumberOfInvocations)).closeApp(); + verify(mTransitionsMock).cleanUpTracks(); + } + + private void emptyTask() { + + } + + @Test + public void canCaptureWindowManagerTrace() { + mTransitionBuilder + .run(this::emptyTask) + .includeJankyRuns() + .skipLayersTrace() + .withTag("mCaptureWmTraceTransitionRunner") + .build().run(); + InOrder orderVerifier = inOrder(mWindowManagerTraceMonitorMock); + orderVerifier.verify(mWindowManagerTraceMonitorMock).start(); + orderVerifier.verify(mWindowManagerTraceMonitorMock).stop(); + orderVerifier.verify(mWindowManagerTraceMonitorMock) + .save("mCaptureWmTraceTransitionRunner", 0); + verifyNoMoreInteractions(mWindowManagerTraceMonitorMock); + } + + @Test + public void canCaptureLayersTrace() { + mTransitionBuilder + .run(this::emptyTask) + .includeJankyRuns() + .skipWindowManagerTrace() + .withTag("mCaptureLayersTraceTransitionRunner") + .build().run(); + InOrder orderVerifier = inOrder(mLayersTraceMonitorMock); + orderVerifier.verify(mLayersTraceMonitorMock).start(); + orderVerifier.verify(mLayersTraceMonitorMock).stop(); + orderVerifier.verify(mLayersTraceMonitorMock) + .save("mCaptureLayersTraceTransitionRunner", 0); + verifyNoMoreInteractions(mLayersTraceMonitorMock); + } + + @Test + public void canRecordEachRun() throws IOException { + mTransitionBuilder + .run(this::emptyTask) + .withTag("mRecordEachRun") + .recordEachRun() + .includeJankyRuns() + .skipLayersTrace() + .skipWindowManagerTrace() + .repeat(2) + .build().run(); + InOrder orderVerifier = inOrder(mScreenRecorderMock); + orderVerifier.verify(mScreenRecorderMock).start(); + orderVerifier.verify(mScreenRecorderMock).stop(); + orderVerifier.verify(mScreenRecorderMock).save("mRecordEachRun", 0); + orderVerifier.verify(mScreenRecorderMock).start(); + orderVerifier.verify(mScreenRecorderMock).stop(); + orderVerifier.verify(mScreenRecorderMock).save("mRecordEachRun", 1); + verifyNoMoreInteractions(mScreenRecorderMock); + } + + @Test + public void canRecordAllRuns() throws IOException { + doReturn(Paths.get(Environment.getExternalStorageDirectory().getAbsolutePath(), + "mRecordAllRuns.mp4")).when(mScreenRecorderMock).save("mRecordAllRuns"); + mTransitionBuilder + .run(this::emptyTask) + .recordAllRuns() + .includeJankyRuns() + .skipLayersTrace() + .skipWindowManagerTrace() + .withTag("mRecordAllRuns") + .repeat(2) + .build().run(); + InOrder orderVerifier = inOrder(mScreenRecorderMock); + orderVerifier.verify(mScreenRecorderMock).start(); + orderVerifier.verify(mScreenRecorderMock).stop(); + orderVerifier.verify(mScreenRecorderMock).save("mRecordAllRuns"); + verifyNoMoreInteractions(mScreenRecorderMock); + } + + @Test + public void canSkipJankyRuns() { + doReturn(false).doReturn(true).doReturn(false) + .when(mWindowAnimationFrameStatsMonitor).jankyFramesDetected(); + List<TransitionResult> results = mTransitionBuilder + .run(this::emptyTask) + .skipLayersTrace() + .skipWindowManagerTrace() + .repeat(3) + .build().run().getResults(); + assertThat(results).hasSize(2); + } + + public static class SimpleUiTransitions { + public void turnOnDevice() { + } + + public void openApp() { + } + + public void performMagic() { + } + + public void closeApp() { + } + + public void cleanUpTracks() { + } + } +} diff --git a/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/WindowManagerTraceTest.java b/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/WindowManagerTraceTest.java new file mode 100644 index 000000000000..49278718932c --- /dev/null +++ b/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/WindowManagerTraceTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import static com.android.server.wm.flicker.TestFileUtils.readTestFile; + +import static com.google.common.truth.Truth.assertThat; + +import com.android.server.wm.flicker.Assertions.Result; + +import org.junit.Before; +import org.junit.Test; + +/** + * Contains {@link WindowManagerTrace} tests. + * To run this test: {@code atest FlickerLibTest:WindowManagerTraceTest} + */ +public class WindowManagerTraceTest { + private WindowManagerTrace mTrace; + + private static WindowManagerTrace readWindowManagerTraceFromFile(String relativePath) { + try { + return WindowManagerTrace.parseFrom(readTestFile(relativePath)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Before + public void setup() { + mTrace = readWindowManagerTraceFromFile("wm_trace_openchrome.pb"); + } + + @Test + public void canParseAllEntries() { + assertThat(mTrace.getEntries().get(0).getTimestamp()).isEqualTo(241777211939236L); + assertThat(mTrace.getEntries().get(mTrace.getEntries().size() - 1).getTimestamp()).isEqualTo + (241779809471942L); + } + + @Test + public void canDetectAboveAppWindowVisibility() { + WindowManagerTrace.Entry entry = mTrace.getEntry(241777211939236L); + Result result = entry.isAboveAppWindowVisible("NavigationBar"); + assertThat(result.passed()).isTrue(); + } + + @Test + public void canDetectBelowAppWindowVisibility() { + WindowManagerTrace.Entry entry = mTrace.getEntry(241777211939236L); + Result result = entry.isBelowAppWindowVisible("wallpaper"); + assertThat(result.passed()).isTrue(); + } + + @Test + public void canDetectAppWindowVisibility() { + WindowManagerTrace.Entry entry = mTrace.getEntry(241777211939236L); + Result result = entry.isAppWindowVisible("com.google.android.apps.nexuslauncher"); + assertThat(result.passed()).isTrue(); + } + + @Test + public void canFailWithReasonForVisibilityChecks_windowNotFound() { + WindowManagerTrace.Entry entry = mTrace.getEntry(241777211939236L); + Result result = entry.isAboveAppWindowVisible("ImaginaryWindow"); + assertThat(result.failed()).isTrue(); + assertThat(result.reason).contains("ImaginaryWindow cannot be found"); + } + + @Test + public void canFailWithReasonForVisibilityChecks_windowNotVisible() { + WindowManagerTrace.Entry entry = mTrace.getEntry(241777211939236L); + Result result = entry.isAboveAppWindowVisible("AssistPreviewPanel"); + assertThat(result.failed()).isTrue(); + assertThat(result.reason).contains("AssistPreviewPanel is invisible"); + } + + @Test + public void canDetectAppZOrder() { + WindowManagerTrace.Entry entry = mTrace.getEntry(241778130296410L); + Result result = entry.isVisibleAppWindowOnTop("com.google.android.apps.chrome"); + assertThat(result.passed()).isTrue(); + } + + @Test + public void canFailWithReasonForZOrderChecks_windowNotOnTop() { + WindowManagerTrace.Entry entry = mTrace.getEntry(241778130296410L); + Result result = entry.isVisibleAppWindowOnTop("com.google.android.apps.nexuslauncher"); + assertThat(result.failed()).isTrue(); + assertThat(result.reason).contains("wanted=com.google.android.apps.nexuslauncher"); + assertThat(result.reason).contains("found=com.android.chrome/" + + "com.google.android.apps.chrome.Main"); + } +} diff --git a/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/WmTraceSubjectTest.java b/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/WmTraceSubjectTest.java new file mode 100644 index 000000000000..d547a188a663 --- /dev/null +++ b/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/WmTraceSubjectTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import static com.android.server.wm.flicker.TestFileUtils.readTestFile; +import static com.android.server.wm.flicker.WmTraceSubject.assertThat; + +import org.junit.Test; + +/** + * Contains {@link WmTraceSubject} tests. + * To run this test: {@code atest FlickerLibTest:WmTraceSubjectTest} + */ +public class WmTraceSubjectTest { + private static WindowManagerTrace readWmTraceFromFile(String relativePath) { + try { + return WindowManagerTrace.parseFrom(readTestFile(relativePath)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testCanTransitionInAppWindow() { + WindowManagerTrace trace = readWmTraceFromFile("wm_trace_openchrome2.pb"); + + assertThat(trace).showsAppWindowOnTop("com.google.android.apps.nexuslauncher/" + + ".NexusLauncherActivity").forRange(174684850717208L, 174685957511016L); + assertThat(trace).showsAppWindowOnTop( + "com.google.android.apps.nexuslauncher/.NexusLauncherActivity") + .then() + .showsAppWindowOnTop("com.android.chrome") + .forAllEntries(); + } +} diff --git a/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/monitor/LayersTraceMonitorTest.java b/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/monitor/LayersTraceMonitorTest.java new file mode 100644 index 000000000000..dbd6761a05b0 --- /dev/null +++ b/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/monitor/LayersTraceMonitorTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.monitor; + +import static android.surfaceflinger.nano.Layerstrace.LayersTraceFileProto.MAGIC_NUMBER_H; +import static android.surfaceflinger.nano.Layerstrace.LayersTraceFileProto.MAGIC_NUMBER_L; + +import static com.google.common.truth.Truth.assertThat; + +import android.surfaceflinger.nano.Layerstrace.LayersTraceFileProto; + +import com.google.common.io.Files; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; + +/** + * Contains {@link LayersTraceMonitor} tests. + * To run this test: {@code atest FlickerLibTest:LayersTraceMonitorTest} + */ +public class LayersTraceMonitorTest { + private LayersTraceMonitor mLayersTraceMonitor; + + @Before + public void setup() { + mLayersTraceMonitor = new LayersTraceMonitor(); + } + + @After + public void teardown() { + mLayersTraceMonitor.stop(); + mLayersTraceMonitor.getOutputTraceFilePath("captureLayersTrace").toFile().delete(); + } + + @Test + public void canStartLayersTrace() throws Exception { + mLayersTraceMonitor.start(); + assertThat(mLayersTraceMonitor.isEnabled()).isTrue(); + } + + @Test + public void canStopLayersTrace() throws Exception { + mLayersTraceMonitor.start(); + assertThat(mLayersTraceMonitor.isEnabled()).isTrue(); + mLayersTraceMonitor.stop(); + assertThat(mLayersTraceMonitor.isEnabled()).isFalse(); + } + + @Test + public void captureLayersTrace() throws Exception { + mLayersTraceMonitor.start(); + mLayersTraceMonitor.stop(); + File testFile = mLayersTraceMonitor.save("captureLayersTrace").toFile(); + assertThat(testFile.exists()).isTrue(); + byte[] trace = Files.toByteArray(testFile); + assertThat(trace.length).isGreaterThan(0); + LayersTraceFileProto mLayerTraceFileProto = LayersTraceFileProto.parseFrom(trace); + assertThat(mLayerTraceFileProto.magicNumber).isEqualTo( + (long) MAGIC_NUMBER_H << 32 | MAGIC_NUMBER_L); + } +} diff --git a/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.java b/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.java new file mode 100644 index 000000000000..e73eecc348f0 --- /dev/null +++ b/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.monitor; + +import static android.os.SystemClock.sleep; + +import static com.android.server.wm.flicker.monitor.ScreenRecorder.DEFAULT_OUTPUT_PATH; +import static com.android.server.wm.flicker.monitor.ScreenRecorder.getPath; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; + +/** + * Contains {@link ScreenRecorder} tests. + * To run this test: {@code atest FlickerLibTest:ScreenRecorderTest} + */ +public class ScreenRecorderTest { + private static final String TEST_VIDEO_FILENAME = "test.mp4"; + private ScreenRecorder mScreenRecorder; + + @Before + public void setup() { + mScreenRecorder = new ScreenRecorder(); + } + + @After + public void teardown() { + DEFAULT_OUTPUT_PATH.toFile().delete(); + getPath(TEST_VIDEO_FILENAME).toFile().delete(); + } + + @Test + public void videoIsRecorded() { + mScreenRecorder.start(); + sleep(100); + mScreenRecorder.stop(); + File file = DEFAULT_OUTPUT_PATH.toFile(); + assertThat(file.exists()).isTrue(); + } + + @Test + public void videoCanBeSaved() { + mScreenRecorder.start(); + sleep(100); + mScreenRecorder.stop(); + mScreenRecorder.save(TEST_VIDEO_FILENAME); + File file = getPath(TEST_VIDEO_FILENAME).toFile(); + assertThat(file.exists()).isTrue(); + } +} diff --git a/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/monitor/WindowAnimationFrameStatsMonitorTest.java b/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/monitor/WindowAnimationFrameStatsMonitorTest.java new file mode 100644 index 000000000000..dd6fed04d3e6 --- /dev/null +++ b/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/monitor/WindowAnimationFrameStatsMonitorTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.monitor; + +import static com.android.server.wm.flicker.AutomationUtils.wakeUpAndGoToHomeScreen; + +import androidx.test.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +/** + * Contains {@link WindowAnimationFrameStatsMonitor} tests. + * To run this test: {@code atest FlickerLibTest:WindowAnimationFrameStatsMonitorTest} + */ +public class WindowAnimationFrameStatsMonitorTest { + private WindowAnimationFrameStatsMonitor mWindowAnimationFrameStatsMonitor; + + @Before + public void setup() { + mWindowAnimationFrameStatsMonitor = new WindowAnimationFrameStatsMonitor( + InstrumentationRegistry.getInstrumentation()); + wakeUpAndGoToHomeScreen(); + } + + // TODO(vishnun) + @Ignore("Disabled until app-helper libraries are available.") + @Test + public void captureWindowAnimationFrameStats() throws Exception { + mWindowAnimationFrameStatsMonitor.start(); + //AppHelperWrapper.getInstance().getHelper(CHROME).open(); + //AppHelperWrapper.getInstance().getHelper(CHROME).exit(); + mWindowAnimationFrameStatsMonitor.stop(); + } +} diff --git a/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitorTest.java b/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitorTest.java new file mode 100644 index 000000000000..56284d7d516a --- /dev/null +++ b/tests/FlickerTests/lib/test/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitorTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.monitor; + +import static com.android.server.wm.nano.WindowManagerTraceFileProto.MAGIC_NUMBER_H; +import static com.android.server.wm.nano.WindowManagerTraceFileProto.MAGIC_NUMBER_L; + +import static com.google.common.truth.Truth.assertThat; + +import com.android.server.wm.nano.WindowManagerTraceFileProto; + +import com.google.common.io.Files; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; + +/** + * Contains {@link WindowManagerTraceMonitor} tests. + * To run this test: {@code atest FlickerLibTest:WindowManagerTraceMonitorTest} + */ +public class WindowManagerTraceMonitorTest { + private WindowManagerTraceMonitor mWindowManagerTraceMonitor; + + @Before + public void setup() { + mWindowManagerTraceMonitor = new WindowManagerTraceMonitor(); + } + + @After + public void teardown() { + mWindowManagerTraceMonitor.stop(); + mWindowManagerTraceMonitor.getOutputTraceFilePath("captureWindowTrace").toFile().delete(); + } + + @Test + public void canStartWindowTrace() throws Exception { + mWindowManagerTraceMonitor.start(); + assertThat(mWindowManagerTraceMonitor.isEnabled()).isTrue(); + } + + @Test + public void canStopWindowTrace() throws Exception { + mWindowManagerTraceMonitor.start(); + assertThat(mWindowManagerTraceMonitor.isEnabled()).isTrue(); + mWindowManagerTraceMonitor.stop(); + assertThat(mWindowManagerTraceMonitor.isEnabled()).isFalse(); + } + + @Test + public void captureWindowTrace() throws Exception { + mWindowManagerTraceMonitor.start(); + mWindowManagerTraceMonitor.stop(); + File testFile = mWindowManagerTraceMonitor.save("captureWindowTrace").toFile(); + assertThat(testFile.exists()).isTrue(); + byte[] trace = Files.toByteArray(testFile); + assertThat(trace.length).isGreaterThan(0); + WindowManagerTraceFileProto mWindowTraceFileProto = WindowManagerTraceFileProto.parseFrom( + trace); + assertThat(mWindowTraceFileProto.magicNumber).isEqualTo( + (long) MAGIC_NUMBER_H << 32 | MAGIC_NUMBER_L); + } +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ChangeAppRotationTest.java b/tests/FlickerTests/src/com/android/server/wm/flicker/ChangeAppRotationTest.java new file mode 100644 index 000000000000..b6860cbd8d96 --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ChangeAppRotationTest.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import static android.view.Surface.rotationToString; + +import static com.android.server.wm.flicker.CommonTransitions.changeAppRotation; +import static com.android.server.wm.flicker.WindowUtils.getAppPosition; +import static com.android.server.wm.flicker.WindowUtils.getNavigationBarPosition; +import static com.android.server.wm.flicker.WindowUtils.getStatusBarPosition; +import static com.android.server.wm.flicker.WmTraceSubject.assertThat; + +import android.graphics.Rect; +import android.util.Log; +import android.view.Surface; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.LargeTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * Cycle through supported app rotations. + * To run this test: {@code atest FlickerTest:ChangeAppRotationTest} + */ +@RunWith(Parameterized.class) +@LargeTest +public class ChangeAppRotationTest extends FlickerTestBase { + private int beginRotation; + private int endRotation; + + public ChangeAppRotationTest(String beginRotationName, String endRotationName, + int beginRotation, int endRotation) { + this.testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(), + "com.android.server.wm.flicker.testapp", "SimpleApp"); + this.beginRotation = beginRotation; + this.endRotation = endRotation; + } + + @Parameters(name = "{0}-{1}") + public static Collection<Object[]> getParams() { + int[] supportedRotations = + {Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_270}; + Collection<Object[]> params = new ArrayList<>(); + for (int begin : supportedRotations) { + for (int end : supportedRotations) { + if (begin != end) { + params.add(new Object[]{rotationToString(begin), rotationToString(end), begin, + end}); + } + } + } + return params; + } + + @Before + public void runTransition() { + super.runTransition( + changeAppRotation(testApp, uiDevice, beginRotation, endRotation).build()); + } + + @Test + public void checkVisibility_navBarWindowIsAlwaysVisible() { + checkResults(result -> assertThat(result) + .showsAboveAppWindow(NAVIGATION_BAR_WINDOW_TITLE).forAllEntries()); + } + + @Test + public void checkVisibility_statusBarWindowIsAlwaysVisible() { + checkResults(result -> assertThat(result) + .showsAboveAppWindow(STATUS_BAR_WINDOW_TITLE).forAllEntries()); + } + + @Test + public void checkPosition_navBarLayerRotatesAndScales() { + Rect startingPos = getNavigationBarPosition(beginRotation); + Rect endingPos = getNavigationBarPosition(endRotation); + checkResults(result -> { + LayersTraceSubject.assertThat(result) + .hasVisibleRegion(NAVIGATION_BAR_WINDOW_TITLE, startingPos) + .inTheBeginning(); + LayersTraceSubject.assertThat(result) + .hasVisibleRegion(NAVIGATION_BAR_WINDOW_TITLE, endingPos).atTheEnd(); + } + ); + } + + @Test + public void checkPosition_appLayerRotates() { + Rect startingPos = getAppPosition(beginRotation); + Rect endingPos = getAppPosition(endRotation); + Log.e(TAG, "startingPos=" + startingPos + " endingPos=" + endingPos); + checkResults(result -> { + LayersTraceSubject.assertThat(result) + .hasVisibleRegion(testApp.getPackage(), startingPos).inTheBeginning(); + LayersTraceSubject.assertThat(result) + .hasVisibleRegion(testApp.getPackage(), endingPos).atTheEnd(); + } + ); + } + + @Test + public void checkPosition_statusBarLayerScales() { + Rect startingPos = getStatusBarPosition(beginRotation); + Rect endingPos = getStatusBarPosition(endRotation); + checkResults(result -> { + LayersTraceSubject.assertThat(result) + .hasVisibleRegion(STATUS_BAR_WINDOW_TITLE, startingPos) + .inTheBeginning(); + LayersTraceSubject.assertThat(result) + .hasVisibleRegion(STATUS_BAR_WINDOW_TITLE, endingPos).atTheEnd(); + } + ); + } + + @Test + public void checkVisibility_navBarLayerIsAlwaysVisible() { + checkResults(result -> LayersTraceSubject.assertThat(result) + .showsLayer(NAVIGATION_BAR_WINDOW_TITLE).forAllEntries()); + } + + @Test + public void checkVisibility_statusBarLayerIsAlwaysVisible() { + checkResults(result -> LayersTraceSubject.assertThat(result) + .showsLayer(STATUS_BAR_WINDOW_TITLE).forAllEntries()); + } +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/CloseImeWindowToAppTest.java b/tests/FlickerTests/src/com/android/server/wm/flicker/CloseImeWindowToAppTest.java new file mode 100644 index 000000000000..6590b86f1499 --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/CloseImeWindowToAppTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import static com.android.server.wm.flicker.CommonTransitions.editTextLoseFocusToApp; +import static com.android.server.wm.flicker.WindowUtils.getDisplayBounds; + +import android.platform.helpers.IAppHelper; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Test IME window closing back to app window transitions. + * To run this test: {@code atest FlickerTests:CloseImeWindowToAppTest} + */ +@LargeTest +@RunWith(AndroidJUnit4.class) +public class CloseImeWindowToAppTest extends FlickerTestBase { + + private static final String IME_WINDOW_TITLE = "InputMethod"; + private IAppHelper mImeTestApp = new StandardAppHelper( + InstrumentationRegistry.getInstrumentation(), + "com.android.server.wm.flicker.testapp", "ImeApp"); + + @Before + public void runTransition() { + super.runTransition(editTextLoseFocusToApp(uiDevice) + .includeJankyRuns().build()); + } + + @Test + public void checkVisibility_imeLayerBecomesInvisible() { + checkResults(result -> LayersTraceSubject.assertThat(result) + .showsLayer(IME_WINDOW_TITLE) + .then() + .hidesLayer(IME_WINDOW_TITLE) + .forAllEntries()); + } + + @Test + public void checkVisibility_imeAppLayerIsAlwaysVisible() { + checkResults(result -> LayersTraceSubject.assertThat(result) + .showsLayer(mImeTestApp.getPackage()) + .forAllEntries()); + } + + @Test + public void checkVisibility_imeAppWindowIsAlwaysVisible() { + checkResults(result -> WmTraceSubject.assertThat(result) + .showsAppWindowOnTop(mImeTestApp.getPackage()) + .forAllEntries()); + } + + @Test + public void checkCoveredRegion_noUncoveredRegions() { + checkResults(result -> LayersTraceSubject.assertThat(result).coversRegion( + getDisplayBounds()).forAllEntries()); + } +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/CloseImeWindowToHomeTest.java b/tests/FlickerTests/src/com/android/server/wm/flicker/CloseImeWindowToHomeTest.java new file mode 100644 index 000000000000..4771b02000c0 --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/CloseImeWindowToHomeTest.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import static com.android.server.wm.flicker.CommonTransitions.editTextLoseFocusToHome; +import static com.android.server.wm.flicker.WindowUtils.getDisplayBounds; + +import android.platform.helpers.IAppHelper; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Test IME window closing to home transitions. + * To run this test: {@code atest FlickerTests:CloseImeWindowToHomeTest} + */ +@LargeTest +@RunWith(AndroidJUnit4.class) +public class CloseImeWindowToHomeTest extends FlickerTestBase { + + private static final String IME_WINDOW_TITLE = "InputMethod"; + private IAppHelper mImeTestApp = new StandardAppHelper( + InstrumentationRegistry.getInstrumentation(), + "com.android.server.wm.flicker.testapp", "ImeApp"); + + @Before + public void runTransition() { + super.runTransition(editTextLoseFocusToHome(uiDevice) + .includeJankyRuns().build()); + } + + @Test + public void checkVisibility_imeWindowBecomesInvisible() { + checkResults(result -> WmTraceSubject.assertThat(result) + .showsImeWindow(IME_WINDOW_TITLE) + .then() + .hidesImeWindow(IME_WINDOW_TITLE) + .forAllEntries()); + } + + @Test + public void checkVisibility_imeLayerBecomesInvisible() { + checkResults(result -> LayersTraceSubject.assertThat(result) + .showsLayer(IME_WINDOW_TITLE) + .then() + .hidesLayer(IME_WINDOW_TITLE) + .forAllEntries()); + } + + @Test + public void checkVisibility_imeAppLayerBecomesInvisible() { + checkResults(result -> LayersTraceSubject.assertThat(result) + .showsLayer(mImeTestApp.getPackage()) + .then() + .hidesLayer(mImeTestApp.getPackage()) + .forAllEntries()); + } + + @Test + public void checkVisibility_imeAppWindowBecomesInvisible() { + checkResults(result -> WmTraceSubject.assertThat(result) + .showsAppWindowOnTop(mImeTestApp.getPackage()) + .then() + .hidesAppWindowOnTop(mImeTestApp.getPackage()) + .forAllEntries()); + } + + @Test + public void checkCoveredRegion_noUncoveredRegions() { + checkResults(result -> LayersTraceSubject.assertThat(result).coversRegion( + getDisplayBounds()).forAllEntries()); + } +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonTransitions.java b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonTransitions.java new file mode 100644 index 000000000000..65888acc184b --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonTransitions.java @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import static android.os.SystemClock.sleep; +import static android.view.Surface.rotationToString; + +import static com.android.server.wm.flicker.AutomationUtils.clearRecents; +import static com.android.server.wm.flicker.AutomationUtils.closePipWindow; +import static com.android.server.wm.flicker.AutomationUtils.exitSplitScreen; +import static com.android.server.wm.flicker.AutomationUtils.expandPipWindow; +import static com.android.server.wm.flicker.AutomationUtils.launchSplitScreen; +import static com.android.server.wm.flicker.AutomationUtils.stopPackage; + +import android.content.Context; +import android.content.Intent; +import android.os.RemoteException; +import android.platform.helpers.IAppHelper; +import android.support.test.uiautomator.By; +import android.support.test.uiautomator.UiDevice; +import android.support.test.uiautomator.UiObject2; +import android.support.test.uiautomator.Until; +import android.util.Rational; +import android.view.Surface; + +import androidx.test.InstrumentationRegistry; + +import com.android.server.wm.flicker.TransitionRunner.TransitionBuilder; + +/** + * Collection of common transitions which can be used to test different apps or scenarios. + */ +class CommonTransitions { + + public static final int ITERATIONS = 1; + private static final String TAG = "FLICKER"; + private static final long APP_LAUNCH_TIMEOUT = 10000; + + private static void setRotation(UiDevice device, int rotation) { + try { + switch (rotation) { + case Surface.ROTATION_270: + device.setOrientationLeft(); + break; + + case Surface.ROTATION_90: + device.setOrientationRight(); + break; + + case Surface.ROTATION_0: + default: + device.setOrientationNatural(); + } + // Wait for animation to complete + sleep(3000); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + private static void clickEditTextWidget(UiDevice device, IAppHelper testApp) { + UiObject2 editText = device.findObject(By.res(testApp.getPackage(), "plain_text_input")); + editText.click(); + sleep(500); + } + + private static void clickEnterPipButton(UiDevice device, IAppHelper testApp) { + UiObject2 enterPipButton = device.findObject(By.res(testApp.getPackage(), "enter_pip")); + enterPipButton.click(); + sleep(500); + } + + static TransitionBuilder openAppWarm(IAppHelper testApp, UiDevice + device) { + return TransitionRunner.newBuilder() + .withTag("OpenAppWarm_" + testApp.getLauncherName()) + .runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen) + .runBeforeAll(testApp::open) + .runBefore(device::pressHome) + .runBefore(device::waitForIdle) + .run(testApp::open) + .runAfterAll(testApp::exit) + .runAfterAll(AutomationUtils::setDefaultWait) + .repeat(ITERATIONS); + } + + static TransitionBuilder closeAppWithBackKey(IAppHelper testApp, UiDevice + device) { + return TransitionRunner.newBuilder() + .withTag("closeAppWithBackKey_" + testApp.getLauncherName()) + .runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen) + .runBefore(testApp::open) + .runBefore(device::waitForIdle) + .run(device::pressBack) + .run(device::waitForIdle) + .runAfterAll(testApp::exit) + .runAfterAll(AutomationUtils::setDefaultWait) + .repeat(ITERATIONS); + } + + static TransitionBuilder closeAppWithHomeKey(IAppHelper testApp, UiDevice + device) { + return TransitionRunner.newBuilder() + .withTag("closeAppWithHomeKey_" + testApp.getLauncherName()) + .runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen) + .runBefore(testApp::open) + .runBefore(device::waitForIdle) + .run(device::pressHome) + .run(device::waitForIdle) + .runAfterAll(testApp::exit) + .runAfterAll(AutomationUtils::setDefaultWait) + .repeat(ITERATIONS); + } + + static TransitionBuilder getOpenAppCold(IAppHelper testApp, + UiDevice device) { + return TransitionRunner.newBuilder() + .withTag("OpenAppCold_" + testApp.getLauncherName()) + .runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen) + .runBefore(device::pressHome) + .runBefore(testApp::exit) + .runBefore(device::waitForIdle) + .run(testApp::open) + .runAfterAll(testApp::exit) + .repeat(ITERATIONS); + } + + static TransitionBuilder changeAppRotation(IAppHelper testApp, UiDevice + device, int beginRotation, int endRotation) { + return TransitionRunner.newBuilder() + .withTag("changeAppRotation_" + testApp.getLauncherName() + + rotationToString(beginRotation) + "_" + + rotationToString(endRotation)) + .runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen) + .runBeforeAll(testApp::open) + .runBefore(() -> setRotation(device, beginRotation)) + .run(() -> setRotation(device, endRotation)) + .runAfterAll(testApp::exit) + .runAfterAll(() -> setRotation(device, Surface.ROTATION_0)) + .repeat(ITERATIONS); + } + + static TransitionBuilder changeAppRotation(Intent intent, String intentId, Context context, + UiDevice + device, int beginRotation, int endRotation) { + final String testTag = "changeAppRotation_" + intentId + "_" + + rotationToString(beginRotation) + "_" + rotationToString(endRotation); + return TransitionRunner.newBuilder() + .withTag(testTag) + .runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen) + .runBeforeAll(() -> { + context.startActivity(intent); + device.wait(Until.hasObject(By.pkg(intent.getComponent() + .getPackageName()).depth(0)), APP_LAUNCH_TIMEOUT); + } + ) + .runBefore(() -> setRotation(device, beginRotation)) + .run(() -> setRotation(device, endRotation)) + .runAfterAll(() -> stopPackage(context, intent.getComponent().getPackageName())) + .runAfterAll(() -> setRotation(device, Surface.ROTATION_0)) + .repeat(ITERATIONS); + } + + static TransitionBuilder appToSplitScreen(IAppHelper testApp, UiDevice device) { + return TransitionRunner.newBuilder() + .withTag("appToSplitScreen_" + testApp.getLauncherName()) + .runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen) + .runBefore(testApp::open) + .runBefore(device::waitForIdle) + .runBefore(() -> sleep(500)) + .run(() -> launchSplitScreen(device)) + .runAfter(() -> exitSplitScreen(device)) + .runAfterAll(testApp::exit) + .repeat(ITERATIONS); + } + + static TransitionBuilder splitScreenToLauncher(IAppHelper testApp, UiDevice device) { + return TransitionRunner.newBuilder() + .withTag("splitScreenToLauncher_" + testApp.getLauncherName()) + .runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen) + .runBefore(testApp::open) + .runBefore(device::waitForIdle) + .runBefore(() -> launchSplitScreen(device)) + .run(() -> exitSplitScreen(device)) + .runAfterAll(testApp::exit) + .repeat(ITERATIONS); + } + + static TransitionBuilder editTextSetFocus(UiDevice device) { + IAppHelper testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(), + "com.android.server.wm.flicker.testapp", "ImeApp"); + return TransitionRunner.newBuilder() + .withTag("editTextSetFocus_" + testApp.getLauncherName()) + .runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen) + .runBefore(device::pressHome) + .runBefore(testApp::open) + .run(() -> clickEditTextWidget(device, testApp)) + .runAfterAll(testApp::exit) + .repeat(ITERATIONS); + } + + static TransitionBuilder resizeSplitScreen(IAppHelper testAppTop, IAppHelper testAppBottom, + UiDevice device, Rational startRatio, Rational stopRatio) { + String testTag = "resizeSplitScreen_" + testAppTop.getLauncherName() + "_" + + testAppBottom.getLauncherName() + "_" + + startRatio.toString().replace("/", ":") + "_to_" + + stopRatio.toString().replace("/", ":"); + return TransitionRunner.newBuilder() + .withTag(testTag) + .runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen) + .runBeforeAll(() -> clearRecents(device)) + .runBefore(testAppBottom::open) + .runBefore(device::pressHome) + .runBefore(testAppTop::open) + .runBefore(device::waitForIdle) + .runBefore(() -> launchSplitScreen(device)) + .runBefore(() -> { + UiObject2 snapshot = device.findObject( + By.res("com.google.android.apps.nexuslauncher", "snapshot")); + snapshot.click(); + }) + .runBefore(() -> AutomationUtils.resizeSplitScreen(device, startRatio)) + .run(() -> AutomationUtils.resizeSplitScreen(device, stopRatio)) + .runAfter(() -> exitSplitScreen(device)) + .runAfter(device::pressHome) + .runAfterAll(testAppTop::exit) + .runAfterAll(testAppBottom::exit) + .repeat(ITERATIONS); + } + + static TransitionBuilder editTextLoseFocusToHome(UiDevice device) { + IAppHelper testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(), + "com.android.server.wm.flicker.testapp", "ImeApp"); + return TransitionRunner.newBuilder() + .withTag("editTextLoseFocusToHome_" + testApp.getLauncherName()) + .runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen) + .runBefore(device::pressHome) + .runBefore(testApp::open) + .runBefore(() -> clickEditTextWidget(device, testApp)) + .run(device::pressHome) + .run(device::waitForIdle) + .runAfterAll(testApp::exit) + .repeat(ITERATIONS); + } + + static TransitionBuilder editTextLoseFocusToApp(UiDevice device) { + IAppHelper testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(), + "com.android.server.wm.flicker.testapp", "ImeApp"); + return TransitionRunner.newBuilder() + .withTag("editTextLoseFocusToApp_" + testApp.getLauncherName()) + .runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen) + .runBefore(device::pressHome) + .runBefore(testApp::open) + .runBefore(() -> clickEditTextWidget(device, testApp)) + .run(device::pressBack) + .run(device::waitForIdle) + .runAfterAll(testApp::exit) + .repeat(ITERATIONS); + } + + static TransitionBuilder enterPipMode(UiDevice device) { + IAppHelper testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(), + "com.android.server.wm.flicker.testapp", "PipApp"); + return TransitionRunner.newBuilder() + .withTag("enterPipMode_" + testApp.getLauncherName()) + .runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen) + .runBefore(device::pressHome) + .runBefore(testApp::open) + .run(() -> clickEnterPipButton(device, testApp)) + .runAfter(() -> closePipWindow(device)) + .runAfterAll(testApp::exit) + .repeat(ITERATIONS); + } + + static TransitionBuilder exitPipModeToHome(UiDevice device) { + IAppHelper testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(), + "com.android.server.wm.flicker.testapp", "PipApp"); + return TransitionRunner.newBuilder() + .withTag("exitPipModeToHome_" + testApp.getLauncherName()) + .runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen) + .runBefore(device::pressHome) + .runBefore(testApp::open) + .runBefore(() -> clickEnterPipButton(device, testApp)) + .run(() -> closePipWindow(device)) + .run(device::waitForIdle) + .runAfterAll(testApp::exit) + .repeat(ITERATIONS); + } + + static TransitionBuilder exitPipModeToApp(UiDevice device) { + IAppHelper testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(), + "com.android.server.wm.flicker.testapp", "PipApp"); + return TransitionRunner.newBuilder() + .withTag("exitPipModeToApp_" + testApp.getLauncherName()) + .runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen) + .runBefore(device::pressHome) + .runBefore(testApp::open) + .runBefore(() -> clickEnterPipButton(device, testApp)) + .run(() -> expandPipWindow(device)) + .run(device::waitForIdle) + .runAfterAll(testApp::exit) + .repeat(ITERATIONS); + } +}
\ No newline at end of file diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/DebugTest.java b/tests/FlickerTests/src/com/android/server/wm/flicker/DebugTest.java new file mode 100644 index 000000000000..61cca0d6b53f --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/DebugTest.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import android.platform.helpers.IAppHelper; +import android.support.test.uiautomator.UiDevice; +import android.util.Rational; +import android.view.Surface; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests to help debug individual transitions, capture video recordings and create test cases. + */ +@Ignore("Used for debugging transitions used in FlickerTests.") +@RunWith(AndroidJUnit4.class) +public class DebugTest { + private IAppHelper testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(), + "com.android.server.wm.flicker.testapp", "SimpleApp"); + private UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + + /** + * atest FlickerTest:DebugTests#openAppCold + */ + @Test + public void openAppCold() { + CommonTransitions.getOpenAppCold(testApp, uiDevice).recordAllRuns().build().run(); + } + + /** + * atest FlickerTest:DebugTests#openAppWarm + */ + @Test + public void openAppWarm() { + CommonTransitions.openAppWarm(testApp, uiDevice).recordAllRuns().build().run(); + } + + /** + * atest FlickerTest:DebugTests#changeOrientationFromNaturalToLeft + */ + @Test + public void changeOrientationFromNaturalToLeft() { + CommonTransitions.changeAppRotation(testApp, uiDevice, Surface.ROTATION_0, + Surface.ROTATION_270).recordAllRuns().build().run(); + } + + /** + * atest FlickerTest:DebugTests#closeAppWithBackKey + */ + @Test + public void closeAppWithBackKey() { + CommonTransitions.closeAppWithBackKey(testApp, uiDevice).recordAllRuns().build().run(); + } + + /** + * atest FlickerTest:DebugTests#closeAppWithHomeKey + */ + @Test + public void closeAppWithHomeKey() { + CommonTransitions.closeAppWithHomeKey(testApp, uiDevice).recordAllRuns().build().run(); + } + + /** + * atest FlickerTest:DebugTests#openAppToSplitScreen + */ + @Test + public void openAppToSplitScreen() { + CommonTransitions.appToSplitScreen(testApp, uiDevice).includeJankyRuns().recordAllRuns() + .build().run(); + } + + /** + * atest FlickerTest:DebugTests#splitScreenToLauncher + */ + @Test + public void splitScreenToLauncher() { + CommonTransitions.splitScreenToLauncher(testApp, + uiDevice).includeJankyRuns().recordAllRuns() + .build().run(); + } + + /** + * atest FlickerTest:DebugTests#resizeSplitScreen + */ + @Test + public void resizeSplitScreen() { + IAppHelper bottomApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(), + "com.android.server.wm.flicker.testapp", "ImeApp"); + CommonTransitions.resizeSplitScreen(testApp, bottomApp, uiDevice, new Rational(1, 3), + new Rational(2, 3)).includeJankyRuns().recordEachRun().build().run(); + } + + // IME tests + + /** + * atest FlickerTest:DebugTests#editTextSetFocus + */ + @Test + public void editTextSetFocus() { + CommonTransitions.editTextSetFocus(uiDevice).includeJankyRuns().recordEachRun() + .build().run(); + } + + /** + * atest FlickerTest:DebugTests#editTextLoseFocusToHome + */ + @Test + public void editTextLoseFocusToHome() { + CommonTransitions.editTextLoseFocusToHome(uiDevice).includeJankyRuns().recordEachRun() + .build().run(); + } + + /** + * atest FlickerTest:DebugTests#editTextLoseFocusToApp + */ + @Test + public void editTextLoseFocusToApp() { + CommonTransitions.editTextLoseFocusToHome(uiDevice).includeJankyRuns().recordEachRun() + .build().run(); + } + + // PIP tests + + /** + * atest FlickerTest:DebugTests#enterPipMode + */ + @Test + public void enterPipMode() { + CommonTransitions.enterPipMode(uiDevice).includeJankyRuns().recordEachRun().build().run(); + } + + /** + * atest FlickerTest:DebugTests#exitPipModeToHome + */ + @Test + public void exitPipModeToHome() { + CommonTransitions.exitPipModeToHome(uiDevice).includeJankyRuns().recordEachRun() + .build().run(); + } + + /** + * atest FlickerTest:DebugTests#exitPipModeToApp + */ + @Test + public void exitPipModeToApp() { + CommonTransitions.exitPipModeToApp(uiDevice).includeJankyRuns().recordEachRun() + .build().run(); + } +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/FlickerTestBase.java b/tests/FlickerTests/src/com/android/server/wm/flicker/FlickerTestBase.java new file mode 100644 index 000000000000..00e11c0cef41 --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/FlickerTestBase.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import static com.android.server.wm.flicker.AutomationUtils.setDefaultWait; + +import static com.google.common.truth.Truth.assertWithMessage; + +import android.platform.helpers.IAppHelper; +import android.support.test.uiautomator.UiDevice; +import android.util.Log; + +import androidx.test.InstrumentationRegistry; + +import com.android.server.wm.flicker.TransitionRunner.TransitionResult; + +import org.junit.After; +import org.junit.AfterClass; + +import java.util.HashMap; +import java.util.List; +import java.util.function.Consumer; + +/** + * Base class of all Flicker test that performs common functions for all flicker tests: + * <p> + * - Caches transitions so that a transition is run once and the transition results are used by + * tests multiple times. This is needed for parameterized tests which call the BeforeClass methods + * multiple times. + * - Keeps track of all test artifacts and deletes ones which do not need to be reviewed. + * - Fails tests if results are not available for any test due to jank. + */ +public class FlickerTestBase { + public static final String TAG = "FLICKER"; + static final String NAVIGATION_BAR_WINDOW_TITLE = "NavigationBar"; + static final String STATUS_BAR_WINDOW_TITLE = "StatusBar"; + static final String DOCKED_STACK_DIVIDER = "DockedStackDivider"; + private static HashMap<String, List<TransitionResult>> transitionResults = + new HashMap<>(); + IAppHelper testApp; + UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + private List<TransitionResult> results; + private TransitionResult lastResult = null; + + /** + * Teardown any system settings and clean up test artifacts from the file system. + * + * Note: test artifacts for failed tests will remain on the device. + */ + @AfterClass + public static void teardown() { + setDefaultWait(); + transitionResults.values().stream() + .flatMap(List::stream) + .forEach(result -> { + if (result.canDelete()) { + result.delete(); + } else { + if (result.layersTraceExists()) { + Log.e(TAG, "Layers trace saved to " + result.getLayersTracePath()); + } + if (result.windowManagerTraceExists()) { + Log.e(TAG, "WindowManager trace saved to " + result + .getWindowManagerTracePath + ()); + } + if (result.screenCaptureVideoExists()) { + Log.e(TAG, "Screen capture video saved to " + result + .screenCaptureVideo.toString()); + } + } + }); + } + + /** + * Runs a transition, returns a cached result if the transition has run before. + */ + void runTransition(TransitionRunner transition) { + if (transitionResults.containsKey(transition.getTestTag())) { + results = transitionResults.get(transition.getTestTag()); + return; + } + results = transition.run().getResults(); + /* Fail if we don't have any results due to jank */ + assertWithMessage("No results to test because all transition runs were invalid because " + + "of Jank").that(results).isNotEmpty(); + transitionResults.put(transition.getTestTag(), results); + } + + /** + * Goes through a list of transition results and checks assertions on each result. + */ + void checkResults(Consumer<TransitionResult> assertion) { + + for (TransitionResult result : results) { + lastResult = result; + assertion.accept(result); + } + lastResult = null; + } + + /** + * Kludge to mark a file for saving. If {@code checkResults} fails, the last result is not + * cleared. This indicates the assertion failed for the result, so mark it for saving. + */ + @After + public void markArtifactsForSaving() { + if (lastResult != null) { + lastResult.flagForSaving(); + } + } +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/OpenAppColdTest.java b/tests/FlickerTests/src/com/android/server/wm/flicker/OpenAppColdTest.java new file mode 100644 index 000000000000..7818c4e4ba50 --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/OpenAppColdTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import static com.android.server.wm.flicker.CommonTransitions.getOpenAppCold; +import static com.android.server.wm.flicker.WindowUtils.getDisplayBounds; +import static com.android.server.wm.flicker.WmTraceSubject.assertThat; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Test cold launch app from launcher. + * To run this test: {@code atest FlickerTests:OpenAppColdTest} + */ +@LargeTest +@RunWith(AndroidJUnit4.class) +public class OpenAppColdTest extends FlickerTestBase { + + public OpenAppColdTest() { + this.testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(), + "com.android.server.wm.flicker.testapp", "SimpleApp"); + } + + @Before + public void runTransition() { + super.runTransition(getOpenAppCold(testApp, uiDevice).build()); + } + + @Test + public void checkVisibility_navBarWindowIsAlwaysVisible() { + checkResults(result -> assertThat(result) + .showsAboveAppWindow(NAVIGATION_BAR_WINDOW_TITLE).forAllEntries()); + } + + @Test + public void checkVisibility_statusBarWindowIsAlwaysVisible() { + checkResults(result -> assertThat(result) + .showsAboveAppWindow(STATUS_BAR_WINDOW_TITLE).forAllEntries()); + } + + @Test + public void checkVisibility_wallpaperWindowBecomesInvisible() { + checkResults(result -> assertThat(result) + .showsBelowAppWindow("wallpaper") + .then() + .hidesBelowAppWindow("wallpaper") + .forAllEntries()); + } + + @Test + public void checkZOrder_appWindowReplacesLauncherAsTopWindow() { + checkResults(result -> assertThat(result) + .showsAppWindowOnTop( + "com.google.android.apps.nexuslauncher/.NexusLauncherActivity") + .then() + .showsAppWindowOnTop(testApp.getPackage()) + .forAllEntries()); + } + + @Test + public void checkCoveredRegion_noUncoveredRegions() { + checkResults(result -> LayersTraceSubject.assertThat(result).coversRegion( + getDisplayBounds()).forAllEntries()); + } + + @Test + public void checkVisibility_navBarLayerIsAlwaysVisible() { + checkResults(result -> LayersTraceSubject.assertThat(result) + .showsLayer(NAVIGATION_BAR_WINDOW_TITLE).forAllEntries()); + } + + @Test + public void checkVisibility_statusBarLayerIsAlwaysVisible() { + checkResults(result -> LayersTraceSubject.assertThat(result) + .showsLayer(STATUS_BAR_WINDOW_TITLE).forAllEntries()); + } + + @Test + public void checkVisibility_wallpaperLayerBecomesInvisible() { + checkResults(result -> LayersTraceSubject.assertThat(result) + .showsLayer("wallpaper") + .then() + .hidesLayer("wallpaper") + .forAllEntries()); + } +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/OpenAppToSplitScreenTest.java b/tests/FlickerTests/src/com/android/server/wm/flicker/OpenAppToSplitScreenTest.java new file mode 100644 index 000000000000..63018ec1d9e7 --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/OpenAppToSplitScreenTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import static com.android.server.wm.flicker.CommonTransitions.appToSplitScreen; +import static com.android.server.wm.flicker.WindowUtils.getDisplayBounds; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Test open app to split screen. + * To run this test: {@code atest FlickerTests:OpenAppToSplitScreenTest} + */ +@LargeTest +@RunWith(AndroidJUnit4.class) +public class OpenAppToSplitScreenTest extends FlickerTestBase { + + public OpenAppToSplitScreenTest() { + this.testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(), + "com.android.server.wm.flicker.testapp", "SimpleApp"); + } + + @Before + public void runTransition() { + super.runTransition(appToSplitScreen(testApp, uiDevice).includeJankyRuns().build()); + } + + @Test + public void checkVisibility_navBarWindowIsAlwaysVisible() { + checkResults(result -> WmTraceSubject.assertThat(result) + .showsAboveAppWindow(NAVIGATION_BAR_WINDOW_TITLE).forAllEntries()); + } + + @Test + public void checkVisibility_statusBarWindowIsAlwaysVisible() { + checkResults(result -> WmTraceSubject.assertThat(result) + .showsAboveAppWindow(STATUS_BAR_WINDOW_TITLE).forAllEntries()); + } + + @Test + public void checkVisibility_dividerWindowBecomesVisible() { + checkResults(result -> WmTraceSubject.assertThat(result) + .hidesAboveAppWindow(DOCKED_STACK_DIVIDER) + .then() + .showsAboveAppWindow(DOCKED_STACK_DIVIDER) + .forAllEntries()); + } + + @Test + public void checkCoveredRegion_noUncoveredRegions() { + checkResults(result -> + LayersTraceSubject.assertThat(result) + .coversRegion(getDisplayBounds()).forAllEntries()); + } + + @Test + public void checkVisibility_navBarLayerIsAlwaysVisible() { + checkResults(result -> LayersTraceSubject.assertThat(result) + .showsLayer(NAVIGATION_BAR_WINDOW_TITLE).forAllEntries()); + } + + @Test + public void checkVisibility_statusBarLayerIsAlwaysVisible() { + checkResults(result -> LayersTraceSubject.assertThat(result) + .showsLayer(STATUS_BAR_WINDOW_TITLE).forAllEntries()); + } + + @Test + public void checkVisibility_dividerLayerBecomesVisible() { + checkResults(result -> LayersTraceSubject.assertThat(result) + .hidesLayer(DOCKED_STACK_DIVIDER) + .then() + .showsLayer(DOCKED_STACK_DIVIDER) + .forAllEntries()); + } +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/OpenAppWarmTest.java b/tests/FlickerTests/src/com/android/server/wm/flicker/OpenAppWarmTest.java new file mode 100644 index 000000000000..1aba93056c89 --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/OpenAppWarmTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import static com.android.server.wm.flicker.CommonTransitions.openAppWarm; +import static com.android.server.wm.flicker.WindowUtils.getDisplayBounds; +import static com.android.server.wm.flicker.WmTraceSubject.assertThat; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Test warm launch app. + * To run this test: {@code atest FlickerTests:OpenAppWarmTest} + */ +@LargeTest +@RunWith(AndroidJUnit4.class) +public class OpenAppWarmTest extends FlickerTestBase { + + public OpenAppWarmTest() { + this.testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(), + "com.android.server.wm.flicker.testapp", "SimpleApp"); + } + + @Before + public void runTransition() { + super.runTransition(openAppWarm(testApp, uiDevice).build()); + } + + @Test + public void checkVisibility_navBarIsAlwaysVisible() { + checkResults(result -> assertThat(result) + .showsAboveAppWindow(NAVIGATION_BAR_WINDOW_TITLE).forAllEntries()); + } + + @Test + public void checkVisibility_statusBarIsAlwaysVisible() { + checkResults(result -> assertThat(result) + .showsAboveAppWindow(STATUS_BAR_WINDOW_TITLE).forAllEntries()); + } + + @Test + public void checkVisibility_wallpaperBecomesInvisible() { + checkResults(result -> assertThat(result) + .showsBelowAppWindow("wallpaper") + .then() + .hidesBelowAppWindow("wallpaper") + .forAllEntries()); + } + + @Test + public void checkZOrder_appWindowReplacesLauncherAsTopWindow() { + checkResults(result -> assertThat(result) + .showsAppWindowOnTop( + "com.google.android.apps.nexuslauncher/.NexusLauncherActivity") + .then() + .showsAppWindowOnTop(testApp.getPackage()) + .forAllEntries()); + } + + @Test + public void checkCoveredRegion_noUncoveredRegions() { + checkResults(result -> LayersTraceSubject.assertThat(result).coversRegion( + getDisplayBounds()).forAllEntries()); + } + + @Test + public void checkVisibility_navBarLayerIsAlwaysVisible() { + checkResults(result -> LayersTraceSubject.assertThat(result) + .showsLayer(NAVIGATION_BAR_WINDOW_TITLE).forAllEntries()); + } + + @Test + public void checkVisibility_statusBarLayerIsAlwaysVisible() { + checkResults(result -> LayersTraceSubject.assertThat(result) + .showsLayer(STATUS_BAR_WINDOW_TITLE).forAllEntries()); + } + + @Test + public void checkVisibility_wallpaperLayerBecomesInvisible() { + checkResults(result -> LayersTraceSubject.assertThat(result) + .showsLayer("wallpaper") + .then() + .hidesLayer("wallpaper") + .forAllEntries()); + } +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/OpenImeWindowTest.java b/tests/FlickerTests/src/com/android/server/wm/flicker/OpenImeWindowTest.java new file mode 100644 index 000000000000..a81fa8e6d123 --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/OpenImeWindowTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import static com.android.server.wm.flicker.CommonTransitions.editTextSetFocus; +import static com.android.server.wm.flicker.WindowUtils.getDisplayBounds; + +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Test IME window opening transitions. + * To run this test: {@code atest FlickerTests:OpenImeWindowTest} + */ +@LargeTest +@RunWith(AndroidJUnit4.class) +public class OpenImeWindowTest extends FlickerTestBase { + + private static final String IME_WINDOW_TITLE = "InputMethod"; + + @Before + public void runTransition() { + super.runTransition(editTextSetFocus(uiDevice) + .includeJankyRuns().build()); + } + + @Test + public void checkVisibility_imeWindowBecomesVisible() { + checkResults(result -> WmTraceSubject.assertThat(result) + .hidesImeWindow(IME_WINDOW_TITLE) + .then() + .showsImeWindow(IME_WINDOW_TITLE) + .forAllEntries()); + } + + @Test + public void checkVisibility_imeLayerBecomesVisible() { + checkResults(result -> LayersTraceSubject.assertThat(result) + .hidesLayer(IME_WINDOW_TITLE) + .then() + .showsLayer(IME_WINDOW_TITLE) + .forAllEntries()); + } + + @Test + public void checkCoveredRegion_noUncoveredRegions() { + checkResults(result -> LayersTraceSubject.assertThat(result).coversRegion( + getDisplayBounds()).forAllEntries()); + } +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ResizeSplitScreenTest.java b/tests/FlickerTests/src/com/android/server/wm/flicker/ResizeSplitScreenTest.java new file mode 100644 index 000000000000..50dba81e53b7 --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ResizeSplitScreenTest.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import static com.android.server.wm.flicker.CommonTransitions.resizeSplitScreen; +import static com.android.server.wm.flicker.WindowUtils.getDisplayBounds; +import static com.android.server.wm.flicker.WindowUtils.getDockedStackDividerInset; +import static com.android.server.wm.flicker.WindowUtils.getNavigationBarHeight; + +import static com.google.common.truth.Truth.assertThat; + +import android.graphics.Rect; +import android.platform.helpers.IAppHelper; +import android.util.Rational; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Test split screen resizing window transitions. + * To run this test: {@code atest FlickerTests:ResizeSplitScreenTest} + */ +@LargeTest +@RunWith(AndroidJUnit4.class) +public class ResizeSplitScreenTest extends FlickerTestBase { + + public ResizeSplitScreenTest() { + this.testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(), + "com.android.server.wm.flicker.testapp", "SimpleApp"); + } + + @Before + public void runTransition() { + IAppHelper bottomApp = new StandardAppHelper(InstrumentationRegistry + .getInstrumentation(), + "com.android.server.wm.flicker.testapp", "ImeApp"); + super.runTransition(resizeSplitScreen(testApp, bottomApp, uiDevice, new Rational(1, 3), + new Rational(2, 3)).includeJankyRuns().build()); + } + + @Test + public void checkVisibility_navBarLayerIsAlwaysVisible() { + checkResults(result -> LayersTraceSubject.assertThat(result) + .showsLayer(NAVIGATION_BAR_WINDOW_TITLE) + .forAllEntries()); + } + + @Test + public void checkVisibility_statusBarLayerIsAlwaysVisible() { + checkResults(result -> LayersTraceSubject.assertThat(result) + .showsLayer(STATUS_BAR_WINDOW_TITLE) + .forAllEntries()); + } + + @Test + public void checkVisibility_topAppLayerIsAlwaysVisible() { + checkResults(result -> LayersTraceSubject.assertThat(result) + .showsLayer("SimpleActivity") + .forAllEntries()); + } + + @Test + public void checkVisibility_bottomAppLayerIsAlwaysVisible() { + checkResults(result -> LayersTraceSubject.assertThat(result) + .showsLayer("ImeActivity") + .forAllEntries()); + } + + @Test + public void checkVisibility_dividerLayerIsAlwaysVisible() { + checkResults(result -> LayersTraceSubject.assertThat(result) + .showsLayer(DOCKED_STACK_DIVIDER) + .forAllEntries()); + } + + @Test + public void checkPosition_appsStartingBounds() { + Rect displayBounds = getDisplayBounds(); + checkResults(result -> { + LayersTrace entries = LayersTrace.parseFrom(result.getLayersTrace(), + result.getLayersTracePath()); + + assertThat(entries.getEntries()).isNotEmpty(); + Rect startingDividerBounds = entries.getEntries().get(0).getVisibleBounds + (DOCKED_STACK_DIVIDER); + + Rect startingTopAppBounds = new Rect(0, 0, startingDividerBounds.right, + startingDividerBounds.top + getDockedStackDividerInset()); + + Rect startingBottomAppBounds = new Rect(0, + startingDividerBounds.bottom - getDockedStackDividerInset(), + displayBounds.right, + displayBounds.bottom - getNavigationBarHeight()); + + LayersTraceSubject.assertThat(result) + .hasVisibleRegion("SimpleActivity", startingTopAppBounds) + .inTheBeginning(); + + LayersTraceSubject.assertThat(result) + .hasVisibleRegion("ImeActivity", startingBottomAppBounds) + .inTheBeginning(); + }); + } + + @Test + public void checkPosition_appsEndingBounds() { + Rect displayBounds = getDisplayBounds(); + checkResults(result -> { + LayersTrace entries = LayersTrace.parseFrom(result.getLayersTrace(), + result.getLayersTracePath()); + + assertThat(entries.getEntries()).isNotEmpty(); + Rect endingDividerBounds = entries.getEntries().get( + entries.getEntries().size() - 1).getVisibleBounds( + DOCKED_STACK_DIVIDER); + + Rect startingTopAppBounds = new Rect(0, 0, endingDividerBounds.right, + endingDividerBounds.top + getDockedStackDividerInset()); + + Rect startingBottomAppBounds = new Rect(0, + endingDividerBounds.bottom - getDockedStackDividerInset(), + displayBounds.right, + displayBounds.bottom - getNavigationBarHeight()); + + LayersTraceSubject.assertThat(result) + .hasVisibleRegion("SimpleActivity", startingTopAppBounds) + .atTheEnd(); + + LayersTraceSubject.assertThat(result) + .hasVisibleRegion("ImeActivity", startingBottomAppBounds) + .atTheEnd(); + }); + } + + @Test + public void checkVisibility_navBarWindowIsAlwaysVisible() { + checkResults(result -> WmTraceSubject.assertThat(result) + .showsAboveAppWindow(NAVIGATION_BAR_WINDOW_TITLE) + .forAllEntries()); + } + + @Test + public void checkVisibility_statusBarWindowIsAlwaysVisible() { + checkResults(result -> WmTraceSubject.assertThat(result) + .showsAboveAppWindow(STATUS_BAR_WINDOW_TITLE) + .forAllEntries()); + } + + @Test + public void checkVisibility_topAppWindowIsAlwaysVisible() { + checkResults(result -> WmTraceSubject.assertThat(result) + .showsAppWindow("SimpleActivity") + .forAllEntries()); + } + + @Test + public void checkVisibility_bottomAppWindowIsAlwaysVisible() { + checkResults(result -> WmTraceSubject.assertThat(result) + .showsAppWindow("ImeActivity") + .forAllEntries()); + } + + @Test + public void checkVisibility_dividerWindowIsAlwaysVisible() { + checkResults(result -> WmTraceSubject.assertThat(result) + .showsAboveAppWindow(DOCKED_STACK_DIVIDER) + .forAllEntries()); + } +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/SeamlessAppRotationTest.java b/tests/FlickerTests/src/com/android/server/wm/flicker/SeamlessAppRotationTest.java new file mode 100644 index 000000000000..117ac5a8fadf --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/SeamlessAppRotationTest.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import static android.view.Surface.rotationToString; + +import static com.android.server.wm.flicker.CommonTransitions.changeAppRotation; +import static com.android.server.wm.flicker.WindowUtils.getAppPosition; +import static com.android.server.wm.flicker.WindowUtils.getDisplayBounds; +import static com.android.server.wm.flicker.WindowUtils.getNavigationBarPosition; +import static com.android.server.wm.flicker.testapp.ActivityOptions.EXTRA_STARVE_UI_THREAD; +import static com.android.server.wm.flicker.testapp.ActivityOptions.SEAMLESS_ACTIVITY_COMPONENT_NAME; + +import android.content.Intent; +import android.graphics.Rect; +import android.view.Surface; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.LargeTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * Cycle through supported app rotations using seamless rotations. + * To run this test: {@code atest FlickerTests:SeamlessAppRotationTest} + */ +@LargeTest +@RunWith(Parameterized.class) +public class SeamlessAppRotationTest extends FlickerTestBase { + private int mBeginRotation; + private int mEndRotation; + private Intent mIntent; + + public SeamlessAppRotationTest(String testId, Intent intent, int beginRotation, + int endRotation) { + this.mIntent = intent; + this.mBeginRotation = beginRotation; + this.mEndRotation = endRotation; + } + + @Parameters(name = "{0}") + public static Collection<Object[]> getParams() { + int[] supportedRotations = + {Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_270}; + Collection<Object[]> params = new ArrayList<>(); + + ArrayList<Intent> testIntents = new ArrayList<>(); + + // launch test activity that supports seamless rotation + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setComponent(SEAMLESS_ACTIVITY_COMPONENT_NAME); + testIntents.add(intent); + + // launch test activity that supports seamless rotation with a busy UI thread to miss frames + // when the app is asked to redraw + intent = new Intent(intent); + intent.putExtra(EXTRA_STARVE_UI_THREAD, true); + testIntents.add(intent); + + for (Intent testIntent : testIntents) { + for (int begin : supportedRotations) { + for (int end : supportedRotations) { + if (begin != end) { + String testId = rotationToString(begin) + "_" + rotationToString(end); + if (testIntent.getExtras() != null && + testIntent.getExtras().getBoolean(EXTRA_STARVE_UI_THREAD)) { + testId += "_" + "BUSY_UI_THREAD"; + } + params.add(new Object[]{testId, testIntent, begin, end}); + } + } + } + } + return params; + } + + @Before + public void runTransition() { + String intentId = ""; + if (mIntent.getExtras() != null && + mIntent.getExtras().getBoolean(EXTRA_STARVE_UI_THREAD)) { + intentId = "BUSY_UI_THREAD"; + } + + super.runTransition( + changeAppRotation(mIntent, intentId, InstrumentationRegistry.getContext(), + uiDevice, mBeginRotation, mEndRotation).repeat(5).build()); + } + + @Test + public void checkVisibility_navBarWindowIsAlwaysVisible() { + checkResults(result -> WmTraceSubject.assertThat(result) + .showsAboveAppWindow(NAVIGATION_BAR_WINDOW_TITLE).forAllEntries()); + } + + @Test + public void checkPosition_navBarLayerRotatesAndScales() { + Rect startingPos = getNavigationBarPosition(mBeginRotation); + Rect endingPos = getNavigationBarPosition(mEndRotation); + if (startingPos.equals(endingPos)) { + checkResults(result -> LayersTraceSubject.assertThat(result) + .hasVisibleRegion(NAVIGATION_BAR_WINDOW_TITLE, startingPos) + .forAllEntries()); + } else { + checkResults(result -> LayersTraceSubject.assertThat(result) + .hasVisibleRegion(NAVIGATION_BAR_WINDOW_TITLE, startingPos) + .inTheBeginning()); + checkResults(result -> LayersTraceSubject.assertThat(result) + .hasVisibleRegion(NAVIGATION_BAR_WINDOW_TITLE, endingPos) + .atTheEnd()); + } + } + + @Test + public void checkPosition_appLayerRotates() { + Rect startingPos = getAppPosition(mBeginRotation); + Rect endingPos = getAppPosition(mEndRotation); + if (startingPos.equals(endingPos)) { + checkResults(result -> LayersTraceSubject.assertThat(result) + .hasVisibleRegion(mIntent.getComponent().getPackageName(), startingPos) + .forAllEntries()); + } else { + checkResults(result -> LayersTraceSubject.assertThat(result) + .hasVisibleRegion(mIntent.getComponent().getPackageName(), startingPos) + .then() + .hasVisibleRegion(mIntent.getComponent().getPackageName(), endingPos) + .forAllEntries()); + } + } + + @Test + public void checkCoveredRegion_noUncoveredRegions() { + Rect startingBounds = getDisplayBounds(mBeginRotation); + Rect endingBounds = getDisplayBounds(mEndRotation); + if (startingBounds.equals(endingBounds)) { + checkResults(result -> + LayersTraceSubject.assertThat(result) + .coversRegion(startingBounds) + .forAllEntries()); + } else { + checkResults(result -> + LayersTraceSubject.assertThat(result) + .coversRegion(startingBounds) + .then() + .coversRegion(endingBounds) + .forAllEntries()); + } + } +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/SplitScreenToLauncherTest.java b/tests/FlickerTests/src/com/android/server/wm/flicker/SplitScreenToLauncherTest.java new file mode 100644 index 000000000000..1d30df9750b2 --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/SplitScreenToLauncherTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import static com.android.server.wm.flicker.CommonTransitions.splitScreenToLauncher; +import static com.android.server.wm.flicker.WindowUtils.getDisplayBounds; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.FlakyTest; +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Test open app to split screen. + * To run this test: {@code atest FlickerTests:SplitScreenToLauncherTest} + */ +@LargeTest +@RunWith(AndroidJUnit4.class) +public class SplitScreenToLauncherTest extends FlickerTestBase { + + public SplitScreenToLauncherTest() { + this.testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(), + "com.android.server.wm.flicker.testapp", "SimpleApp"); + } + + @Before + public void runTransition() { + super.runTransition(splitScreenToLauncher(testApp, uiDevice).includeJankyRuns().build()); + } + + @Test + public void checkCoveredRegion_noUncoveredRegions() { + checkResults(result -> + LayersTraceSubject.assertThat(result) + .coversRegion(getDisplayBounds()).forAllEntries()); + } + + @Test + public void checkVisibility_dividerLayerBecomesInVisible() { + checkResults(result -> LayersTraceSubject.assertThat(result) + .showsLayer(DOCKED_STACK_DIVIDER) + .then() + .hidesLayer(DOCKED_STACK_DIVIDER) + .forAllEntries()); + } + + @FlakyTest(bugId = 79686616) + @Test + public void checkVisibility_appLayerBecomesInVisible() { + checkResults(result -> LayersTraceSubject.assertThat(result) + .showsLayer(testApp.getPackage()) + .then() + .hidesLayer(testApp.getPackage()) + .forAllEntries()); + } + + @Test + public void checkVisibility_navBarWindowIsAlwaysVisible() { + checkResults(result -> WmTraceSubject.assertThat(result) + .showsAboveAppWindow(NAVIGATION_BAR_WINDOW_TITLE).forAllEntries()); + } + + @Test + public void checkVisibility_statusBarWindowIsAlwaysVisible() { + checkResults(result -> WmTraceSubject.assertThat(result) + .showsAboveAppWindow(STATUS_BAR_WINDOW_TITLE).forAllEntries()); + } + + @Test + public void checkVisibility_dividerWindowBecomesInVisible() { + checkResults(result -> WmTraceSubject.assertThat(result) + .showsAboveAppWindow(DOCKED_STACK_DIVIDER) + .then() + .hidesAboveAppWindow(DOCKED_STACK_DIVIDER) + .forAllEntries()); + } +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/StandardAppHelper.java b/tests/FlickerTests/src/com/android/server/wm/flicker/StandardAppHelper.java new file mode 100644 index 000000000000..79a0220e0e87 --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/StandardAppHelper.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker; + +import android.app.Instrumentation; +import android.platform.helpers.AbstractStandardAppHelper; + +/** + * Class to take advantage of {@code IAppHelper} interface so the same test can be run against + * first party and third party apps. + */ +public class StandardAppHelper extends AbstractStandardAppHelper { + private final String mPackageName; + private final String mLauncherName; + + public StandardAppHelper(Instrumentation instr, String packageName, String launcherName) { + super(instr); + mPackageName = packageName; + mLauncherName = launcherName; + } + + /** + * {@inheritDoc} + */ + @Override + public String getPackage() { + return mPackageName; + } + + /** + * {@inheritDoc} + */ + @Override + public String getLauncherName() { + return mLauncherName; + } + + /** + * {@inheritDoc} + */ + @Override + public void dismissInitialDialogs() { + + } +} diff --git a/tests/FlickerTests/test-apps/Android.bp b/tests/FlickerTests/test-apps/Android.bp new file mode 100644 index 000000000000..e69de29bb2d1 --- /dev/null +++ b/tests/FlickerTests/test-apps/Android.bp diff --git a/tests/FlickerTests/test-apps/flickerapp/Android.bp b/tests/FlickerTests/test-apps/flickerapp/Android.bp new file mode 100644 index 000000000000..0bea209a757a --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/Android.bp @@ -0,0 +1,26 @@ +// Copyright (C) 2018 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +android_test { + name: "FlickerTestApp", + srcs: ["**/*.java"], + sdk_version: "current", + test_suites: ["device-tests"], +} + +java_test { + name: "flickertestapplib", + sdk_version: "current", + srcs: ["src/com/android/server/wm/flicker/testapp/ActivityOptions.java"], +} diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml new file mode 100644 index 000000000000..b694172d60ca --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.server.wm.flicker.testapp"> + + <uses-sdk android:minSdkVersion="17" android:targetSdkVersion="27"/> + <application + android:allowBackup="false" + android:supportsRtl="true"> + <activity android:name=".SimpleActivity" + android:taskAffinity="com.android.server.wm.flicker.testapp.SimpleActivity" + android:label="SimpleApp"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + <activity android:name=".ImeActivity" + android:taskAffinity="com.android.server.wm.flicker.testapp.ImeActivity" + android:label="ImeApp"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + <activity android:name=".PipActivity" + android:resizeableActivity="true" + android:supportsPictureInPicture="true" + android:configChanges= + "screenSize|smallestScreenSize|screenLayout|orientation" + android:taskAffinity="com.android.server.wm.flicker.testapp.PipActivity" + android:label="PipApp"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + <activity android:name=".SeamlessRotationActivity" + android:taskAffinity= + "com.android.server.wm.flicker.testapp.SeamlessRotationActivity" + android:configChanges="orientation|screenSize" + android:label="SeamlessApp"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + </application> +</manifest>
\ No newline at end of file diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml new file mode 100644 index 000000000000..d5eb02330441 --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@android:color/holo_green_light"> + <EditText android:id="@+id/plain_text_input" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:inputType="text"/> +</LinearLayout> diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_pip.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_pip.xml new file mode 100644 index 000000000000..2c58d91e34fe --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_pip.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@android:color/holo_blue_bright"> + <Button android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/enter_pip" + android:text="Enter PIP"/> +</LinearLayout> diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_simple.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_simple.xml new file mode 100644 index 000000000000..5d94e5177dcc --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_simple.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@android:color/holo_orange_light"> + +</LinearLayout> diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java new file mode 100644 index 000000000000..18994111324e --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.testapp; + +import android.content.ComponentName; + +public class ActivityOptions { + public static final String EXTRA_STARVE_UI_THREAD = "StarveUiThread"; + public static final ComponentName SEAMLESS_ACTIVITY_COMPONENT_NAME = + new ComponentName("com.android.server.wm.flicker.testapp", + "com.android.server.wm.flicker.testapp.SeamlessRotationActivity"); +} diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivity.java new file mode 100644 index 000000000000..df60460e7ae3 --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivity.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.testapp; + +import android.app.Activity; +import android.os.Bundle; +import android.view.WindowManager; + +public class ImeActivity extends Activity { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + WindowManager.LayoutParams p = getWindow().getAttributes(); + p.layoutInDisplayCutoutMode = WindowManager.LayoutParams + .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + getWindow().setAttributes(p); + setContentView(R.layout.activity_ime); + } +} diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PipActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PipActivity.java new file mode 100644 index 000000000000..9a8f39907877 --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PipActivity.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.testapp; + +import android.app.Activity; +import android.app.PictureInPictureParams; +import android.graphics.Rect; +import android.os.Bundle; +import android.util.Rational; +import android.view.WindowManager; +import android.widget.Button; + +public class PipActivity extends Activity { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + WindowManager.LayoutParams p = getWindow().getAttributes(); + p.layoutInDisplayCutoutMode = WindowManager.LayoutParams + .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + getWindow().setAttributes(p); + setContentView(R.layout.activity_pip); + Button enterPip = (Button) findViewById(R.id.enter_pip); + + PictureInPictureParams params = new PictureInPictureParams.Builder() + .setAspectRatio(new Rational(1, 1)) + .setSourceRectHint(new Rect(0, 0, 100, 100)) + .build(); + + enterPip.setOnClickListener((v) -> enterPictureInPictureMode(params)); + } +} diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SeamlessRotationActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SeamlessRotationActivity.java new file mode 100644 index 000000000000..3a0c1c9382fe --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SeamlessRotationActivity.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.testapp; + +import static android.os.SystemClock.sleep; +import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + +import static com.android.server.wm.flicker.testapp.ActivityOptions.EXTRA_STARVE_UI_THREAD; + +import android.app.Activity; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.view.Window; +import android.view.WindowManager; + +import java.util.Timer; +import java.util.TimerTask; + +public class SeamlessRotationActivity extends Activity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + enableSeamlessRotation(); + setContentView(R.layout.activity_simple); + boolean starveUiThread = getIntent().getExtras() != null && + getIntent().getExtras().getBoolean(EXTRA_STARVE_UI_THREAD); + if (starveUiThread) { + starveUiThread(); + } + } + + private void starveUiThread() { + Handler handler = new Handler(Looper.getMainLooper(), (Message unused) -> { + sleep(20); + return true; + }); + new Timer().schedule(new TimerTask() { + @Override + public void run() { + handler.sendEmptyMessage(0); + } + }, 0, 21); + } + + private void enableSeamlessRotation() { + WindowManager.LayoutParams p = getWindow().getAttributes(); + p.rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS; + p.layoutInDisplayCutoutMode = WindowManager.LayoutParams + .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + requestWindowFeature(Window.FEATURE_NO_TITLE); + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN); + getWindow().setAttributes(p); + } +} diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SimpleActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SimpleActivity.java new file mode 100644 index 000000000000..699abf87d341 --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SimpleActivity.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.testapp; + +import android.app.Activity; +import android.os.Bundle; +import android.view.WindowManager; + +public class SimpleActivity extends Activity { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + WindowManager.LayoutParams p = getWindow().getAttributes(); + p.layoutInDisplayCutoutMode = WindowManager.LayoutParams + .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + getWindow().setAttributes(p); + setContentView(R.layout.activity_simple); + } +} diff --git a/tests/FrameworkPerf/AndroidManifest.xml b/tests/FrameworkPerf/AndroidManifest.xml index d62ef9ec210c..4fd2043ce938 100644 --- a/tests/FrameworkPerf/AndroidManifest.xml +++ b/tests/FrameworkPerf/AndroidManifest.xml @@ -13,7 +13,8 @@ <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> - <service android:name="SchedulerService"> + <service android:name="SchedulerService" + android:foregroundServiceType="dataSync|mediaPlayback|phoneCall|location|connectedDevice"> </service> <service android:name="TestService" android:process=":test"> </service> @@ -22,7 +23,6 @@ <receiver android:name="Receiver" android:exported="true"> </receiver> </application> - <instrumentation android:name="android.test.InstrumentationTestRunner" android:targetPackage="com.android.frameworkperf" android:label="Framework Perf Runner" diff --git a/tests/FrameworkPerf/src/com/android/frameworkperf/SchedulerService.java b/tests/FrameworkPerf/src/com/android/frameworkperf/SchedulerService.java index fc3f39027348..d4cbbf9c8271 100644 --- a/tests/FrameworkPerf/src/com/android/frameworkperf/SchedulerService.java +++ b/tests/FrameworkPerf/src/com/android/frameworkperf/SchedulerService.java @@ -17,16 +17,22 @@ package com.android.frameworkperf; import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Intent; import android.os.IBinder; public class SchedulerService extends Service { + private static final String NOTIFICATION_CHANNEL_ID = SchedulerService.class.getSimpleName(); @Override public int onStartCommand(Intent intent, int flags, int startId) { - Notification status = new Notification.Builder(this) + getSystemService(NotificationManager.class).createNotificationChannel( + new NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID, + NotificationManager.IMPORTANCE_DEFAULT)); + Notification status = new Notification.Builder(this, NOTIFICATION_CHANNEL_ID) .setSmallIcon(R.drawable.stat_happy) .setWhen(System.currentTimeMillis()) .setContentTitle("Scheduler Test running") diff --git a/tests/GamePerformance/Android.mk b/tests/GamePerformance/Android.mk new file mode 100644 index 000000000000..58654de34029 --- /dev/null +++ b/tests/GamePerformance/Android.mk @@ -0,0 +1,39 @@ +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) + +# Don't include this package in any target +LOCAL_MODULE_TAGS := tests + +LOCAL_DEX_PREOPT := false + +LOCAL_PROGUARD_ENABLED := disabled + +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_STATIC_JAVA_LIBRARIES := android-support-test + +LOCAL_JAVA_LIBRARIES := android.test.base android.test.runner + +LOCAL_PACKAGE_NAME := GamePerformance + +LOCAL_PRIVATE_PLATFORM_APIS := true + +LOCAL_CERTIFICATE := platform + + +include $(BUILD_PACKAGE) diff --git a/tests/GamePerformance/AndroidManifest.xml b/tests/GamePerformance/AndroidManifest.xml new file mode 100644 index 000000000000..b331e2c07e14 --- /dev/null +++ b/tests/GamePerformance/AndroidManifest.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android.gameperformance"> + <uses-sdk android:minSdkVersion="25"/> + <uses-feature android:glEsVersion="0x00020000" android:required="true" /> + + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <application android:theme="@style/noeffects"> + <uses-library android:name="android.test.runner" /> + <activity android:name="android.gameperformance.GamePerformanceActivity" > + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + <uses-library android:name="android.test.runner" /> + </application> + + <!-- self-instrumenting test package. --> + <instrumentation android:name="android.test.InstrumentationTestRunner" + android:targetPackage="android.gameperformance"> + </instrumentation> +</manifest> diff --git a/tests/GamePerformance/res/values/themes.xml b/tests/GamePerformance/res/values/themes.xml new file mode 100644 index 000000000000..63130717fe72 --- /dev/null +++ b/tests/GamePerformance/res/values/themes.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + --> +<resources> + <style name="noeffects" parent="@android:style/Theme.Holo.NoActionBar.Fullscreen"> + <item name="android:windowNoTitle">true</item> + <item name="android:windowFullscreen">true</item> + <item name="android:fadingEdge">none</item> + <item name="android:windowContentTransitions">false</item> + <item name="android:windowAnimationStyle">@null</item> + </style> +</resources> diff --git a/tests/GamePerformance/src/android/gameperformance/ATraceRunner.java b/tests/GamePerformance/src/android/gameperformance/ATraceRunner.java new file mode 100644 index 000000000000..25754fd79a72 --- /dev/null +++ b/tests/GamePerformance/src/android/gameperformance/ATraceRunner.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.gameperformance; + +import java.io.BufferedReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStreamReader; + +import android.app.Instrumentation; +import android.os.AsyncTask; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +/** + * Helper that runs atrace command for required duration and category. Results are read from + * the output of atrace and serialized to the provided file. We cannot use direct atrace to + * file because atrace is executed in UI automator context and analysis is done in test context. + * In last case output file is not accessible from the both contexts. + */ +public class ATraceRunner extends AsyncTask<Void, Integer, Boolean>{ + private final static String TAG = "ATraceRunner"; + + // Report that atrace is done. + public interface Delegate { + public void onProcessed(boolean success); + } + + private final Instrumentation mInstrumentation; + private final String mOutput; + private final int mTimeInSeconds; + private final String mCategory; + private final Delegate mDelegate; + + public ATraceRunner(Instrumentation instrumentation, + String output, + int timeInSeconds, + String category, + Delegate delegate) { + mInstrumentation = instrumentation; + mOutput = output; + mTimeInSeconds = timeInSeconds; + mCategory = category; + mDelegate = delegate; + } + + @Override + protected Boolean doInBackground(Void... params) { + BufferedReader bufferedReader = null; + FileWriter writer = null; + try { + // Run the command. + final String cmd = "atrace -t " + mTimeInSeconds + " " + mCategory; + Log.i(TAG, "Running atrace... " + cmd); + writer = new FileWriter(mOutput); + final ParcelFileDescriptor fd = + mInstrumentation.getUiAutomation().executeShellCommand(cmd); + bufferedReader = new BufferedReader( + new InputStreamReader(new ParcelFileDescriptor.AutoCloseInputStream(fd))); + String line; + while ((line = bufferedReader.readLine()) != null) { + writer.write(line); + writer.write("\n"); + } + Log.i(TAG, "Running atrace... DONE"); + return true; + } catch (IOException e) { + Log.i(TAG, "atrace failed", e); + return false; + } finally { + Utils.closeQuietly(bufferedReader); + Utils.closeQuietly(writer); + } + } + + @Override + protected void onPostExecute(Boolean result) { + mDelegate.onProcessed(result); + } + +} diff --git a/tests/GamePerformance/src/android/gameperformance/CustomOpenGLView.java b/tests/GamePerformance/src/android/gameperformance/CustomOpenGLView.java new file mode 100644 index 000000000000..2b37280ae9b5 --- /dev/null +++ b/tests/GamePerformance/src/android/gameperformance/CustomOpenGLView.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.gameperformance; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +import android.content.Context; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; + +public class CustomOpenGLView extends GLSurfaceView { + private Random mRandom; + private List<Long> mFrameTimes; + + public CustomOpenGLView(Context context) { + super(context); + + mRandom = new Random(); + mFrameTimes = new ArrayList<Long>(); + + setEGLContextClientVersion(2); + + setRenderer(new GLSurfaceView.Renderer() { + @Override + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f); + gl.glClearDepthf(1.0f); + gl.glEnable(GL10.GL_DEPTH_TEST); + gl.glDepthFunc(GL10.GL_LEQUAL); + + gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, + GL10.GL_NICEST); } + + @Override + public void onSurfaceChanged(GL10 gl, int width, int height) { + GLES20.glViewport(0, 0, width, height); + } + + @Override + public void onDrawFrame(GL10 gl) { + GLES20.glClearColor( + mRandom.nextFloat(), mRandom.nextFloat(), mRandom.nextFloat(), 1.0f); + gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); + synchronized (mFrameTimes) { + mFrameTimes.add(System.currentTimeMillis()); + } + } + }); + setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); + } + + /** + * Resets frame times in order to calculate fps for different test pass. + */ + public void resetFrameTimes() { + synchronized (mFrameTimes) { + mFrameTimes.clear(); + } + } + + /** + * Returns current fps based on collected frame times. + */ + public double getFps() { + synchronized (mFrameTimes) { + if (mFrameTimes.size() < 2) { + return 0.0f; + } + return 1000.0 * mFrameTimes.size() / + (mFrameTimes.get(mFrameTimes.size() - 1) - mFrameTimes.get(0)); + } + } +} diff --git a/tests/GamePerformance/src/android/gameperformance/CustomSurfaceView.java b/tests/GamePerformance/src/android/gameperformance/CustomSurfaceView.java new file mode 100644 index 000000000000..a46668dd9e24 --- /dev/null +++ b/tests/GamePerformance/src/android/gameperformance/CustomSurfaceView.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.gameperformance; + +import java.util.ArrayList; +import java.util.List; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Trace; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +/** + * Minimal SurfaceView that sends buffer on request. + */ +public class CustomSurfaceView extends SurfaceView implements SurfaceHolder.Callback { + // Tag for trace when buffer is requested. + public final static String LOCAL_REQUEST_BUFFER = "localRequestBuffer"; + // Tag for trace when buffer is posted. + public final static String LOCAL_POST_BUFFER = "localPostBuffer"; + + private final Object mSurfaceLock = new Object(); + // Keeps frame times. Used to calculate fps. + private List<Long> mFrameTimes; + // Surface to send. + private Surface mSurface; + private Handler mHandler; + + private Runnable mInvalidateSurfaceTask = new Runnable() { + @Override + public void run() { + synchronized (mSurfaceLock) { + if (mSurface == null) { + return; + } + invalidateSurface(true, true); + mHandler.post(this); + } + } + }; + + public CustomSurfaceView(Context context) { + super(context); + mFrameTimes = new ArrayList<Long>(); + getHolder().addCallback(this); + getHolder().setFormat(PixelFormat.OPAQUE); + + HandlerThread thread = new HandlerThread("SurfaceInvalidator"); + thread.start(); + mHandler = new Handler(thread.getLooper()); + } + + /** + * Resets frame times in order to calculate fps for different test pass. + */ + public void resetFrameTimes() { + synchronized (mSurfaceLock) { + mFrameTimes.clear(); + } + } + + /** + * Returns current fps based on collected frame times. + */ + public double getFps() { + synchronized (mSurfaceLock) { + if (mFrameTimes.size() < 2) { + return 0.0f; + } + return 1000.0 * mFrameTimes.size() / + (mFrameTimes.get(mFrameTimes.size() - 1) - mFrameTimes.get(0)); + } + } + + /** + * Invalidates surface. + * @param traceCalls set to true in case we need register trace calls. Not used for warm-up. + * @param drawFps perform drawing current fps on surface to have some payload on surface. + */ + public void invalidateSurface(boolean traceCalls, boolean drawFps) { + synchronized (mSurfaceLock) { + if (mSurface == null) { + throw new IllegalStateException("Surface is not ready"); + } + if (traceCalls) { + Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, LOCAL_REQUEST_BUFFER); + } + Canvas canvas = mSurface.lockHardwareCanvas(); + if (traceCalls) { + Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS); + } + + if (drawFps) { + int textSize = canvas.getHeight() / 24; + Paint paint = new Paint(); + paint.setTextSize(textSize); + paint.setColor(0xFFFF8040); + canvas.drawARGB(92, 255, 255, 255); + canvas.drawText("FPS: " + String.format("%.2f", getFps()), 10, 300, paint); + } + + if (traceCalls) { + Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, LOCAL_POST_BUFFER); + } + mSurface.unlockCanvasAndPost(canvas); + if (traceCalls) { + Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS); + } + + mFrameTimes.add(System.currentTimeMillis()); + } + } + + /** + * Wait until surface is created and ready to use or return immediately if surface + * already exists. + */ + public void waitForSurfaceReady() { + synchronized (mSurfaceLock) { + if (mSurface == null) { + try { + mSurfaceLock.wait(5000); + } catch(InterruptedException e) { + e.printStackTrace(); + } + } + if (mSurface == null) + throw new IllegalStateException("Surface is not ready."); + } + } + + /** + * Waits until surface is destroyed or return immediately if surface does not exist. + */ + public void waitForSurfaceDestroyed() { + synchronized (mSurfaceLock) { + if (mSurface != null) { + try { + mSurfaceLock.wait(5000); + } catch(InterruptedException e) { + } + } + if (mSurface != null) + throw new IllegalStateException("Surface still exists."); + } + } + + + @Override + public void surfaceCreated(SurfaceHolder holder) { + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + // This method is always called at least once, after surfaceCreated. + synchronized (mSurfaceLock) { + mSurface = holder.getSurface(); + mSurfaceLock.notify(); + mHandler.post(mInvalidateSurfaceTask); + } + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + synchronized (mSurfaceLock) { + mHandler.removeCallbacks(mInvalidateSurfaceTask); + mSurface = null; + mSurfaceLock.notify(); + } + } +} diff --git a/tests/GamePerformance/src/android/gameperformance/GamePerformanceActivity.java b/tests/GamePerformance/src/android/gameperformance/GamePerformanceActivity.java new file mode 100644 index 000000000000..b0e6196b53d7 --- /dev/null +++ b/tests/GamePerformance/src/android/gameperformance/GamePerformanceActivity.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.gameperformance; + +import java.util.concurrent.CountDownLatch; + +import android.app.Activity; +import android.graphics.Rect; +import android.os.Bundle; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.RelativeLayout; + +/** + * Minimal activity that holds SurfaceView or GLSurfaceView. + * call attachSurfaceView or attachOpenGLView to switch views. + */ +public class GamePerformanceActivity extends Activity { + private CustomSurfaceView mSurfaceView = null; + private CustomOpenGLView mOpenGLView = null; + private RelativeLayout mRootLayout; + + public void attachSurfaceView() throws InterruptedException { + synchronized (mRootLayout) { + if (mSurfaceView != null) { + return; + } + final CountDownLatch latch = new CountDownLatch(1); + runOnUiThread(new Runnable() { + @Override + public void run() { + if (mOpenGLView != null) { + mRootLayout.removeView(mOpenGLView); + mOpenGLView = null; + } + mSurfaceView = new CustomSurfaceView(GamePerformanceActivity.this); + mRootLayout.addView(mSurfaceView); + latch.countDown(); + } + }); + latch.await(); + mSurfaceView.waitForSurfaceReady(); + } + } + + public void attachOpenGLView() throws InterruptedException { + synchronized (mRootLayout) { + if (mOpenGLView != null) { + return; + } + final CountDownLatch latch = new CountDownLatch(1); + runOnUiThread(new Runnable() { + @Override + public void run() { + if (mSurfaceView != null) { + mRootLayout.removeView(mSurfaceView); + mSurfaceView = null; + } + mOpenGLView = new CustomOpenGLView(GamePerformanceActivity.this); + mRootLayout.addView(mOpenGLView); + latch.countDown(); + } + }); + latch.await(); + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + + // To layouts in parent. First contains list of Surfaces and second + // controls. Controls stay on top. + mRootLayout = new RelativeLayout(this); + mRootLayout.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + Rect rect = new Rect(); + getWindow().getDecorView().getWindowVisibleDisplayFrame(rect); + + mOpenGLView = new CustomOpenGLView(this); + mRootLayout.addView(mOpenGLView); + + setContentView(mRootLayout); + } + + public void resetFrameTimes() { + if (mSurfaceView != null) { + mSurfaceView.resetFrameTimes(); + } else if (mOpenGLView != null) { + mOpenGLView.resetFrameTimes(); + } else { + throw new IllegalStateException("Nothing attached"); + } + } + + public double getFps() { + if (mSurfaceView != null) { + return mSurfaceView.getFps(); + } else if (mOpenGLView != null) { + return mOpenGLView.getFps(); + } else { + throw new IllegalStateException("Nothing attached"); + } + } + + @Override + protected void onResume() { + super.onResume(); + } + + @Override + protected void onPause() { + super.onPause(); + } +}
\ No newline at end of file diff --git a/tests/GamePerformance/src/android/gameperformance/GamePerformanceTest.java b/tests/GamePerformance/src/android/gameperformance/GamePerformanceTest.java new file mode 100644 index 000000000000..e5de7d75886e --- /dev/null +++ b/tests/GamePerformance/src/android/gameperformance/GamePerformanceTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.gameperformance; + +import java.io.File; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.CountDownLatch; + +import android.app.Activity; +import android.content.Context; +import android.graphics.PixelFormat; +import android.os.Build; +import android.os.Bundle; +import android.os.Trace; +import android.test.ActivityInstrumentationTestCase2; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Log; + +public class GamePerformanceTest extends + ActivityInstrumentationTestCase2<GamePerformanceActivity> { + private final static String TAG = "GamePerformanceTest"; + + private final static int GRAPHIC_BUFFER_WARMUP_LOOP_CNT = 60; + + public GamePerformanceTest() { + super(GamePerformanceActivity.class); + } + + @SmallTest + public void testGraphicBufferMetrics() throws IOException, InterruptedException { + Bundle status = new Bundle(); + + for (int i = 0; i < 2; ++i) { + if (i == 0) { + getActivity().attachSurfaceView(); + } else { + getActivity().attachOpenGLView(); + } + + // Perform warm-up. + Thread.sleep(2000); + + // Once atrace is done, this one is triggered. + CountDownLatch latch = new CountDownLatch(1); + + final String passTag = i == 0 ? "surface" : "opengl"; + final String output = (new File(getInstrumentation().getContext().getFilesDir(), + "atrace_" + passTag + ".log")).getAbsolutePath(); + Log.i(TAG, "Collecting traces to " + output); + new ATraceRunner(getInstrumentation(), output, 5, "gfx", new ATraceRunner.Delegate() { + @Override + public void onProcessed(boolean success) { + latch.countDown(); + } + }).execute(); + + // Reset frame times and perform invalidation loop while atrace is running. + getActivity().resetFrameTimes(); + latch.await(); + + // Copy results. + final Map<String, Double> metrics = + GraphicBufferMetrics.processGraphicBufferResult(output, passTag); + for (Map.Entry<String, Double> metric : metrics.entrySet()) { + status.putDouble(metric.getKey(), metric.getValue()); + } + // Also record FPS. + status.putDouble(passTag + "_fps", getActivity().getFps()); + } + + getInstrumentation().sendStatus(Activity.RESULT_OK, status); + } +} diff --git a/tests/GamePerformance/src/android/gameperformance/GraphicBufferMetrics.java b/tests/GamePerformance/src/android/gameperformance/GraphicBufferMetrics.java new file mode 100644 index 000000000000..dffce1acdec3 --- /dev/null +++ b/tests/GamePerformance/src/android/gameperformance/GraphicBufferMetrics.java @@ -0,0 +1,530 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.gameperformance; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.TreeMap; +import java.util.TreeSet; + +/** + * Utility class that performs analysis of atrace logs. This is implemented without Android + * dependencies and therefore can be used in stand-alone mode. + * Idea of this is to track atrace gfx event from graphics buffer producer/consumer. + * We analyze here from surfaceflinger + * queueBuffer - event when buffer was queued. + * acquireBuffer - event when buffer was requested for composition. + * releaseBuffer - even when buffer was released after composition. + * This also track events, issued locally + * localPostBuffer - event when buffer was posted from the local app. + * + * queueBuffer, acquireBuffer, releaseBuffer is accompanied with buffer name so we + * can track life-cycle of particular buffer. + * We don't have such information for localPostBuffer, however we can track next queueBuffer + * from surfaceflinger corresponds to previous localPostBuffer. + * + * Following results are calculated: + * post_time_[min/max/avr]_mcs - time for localPostBuffer duration. + * ready_time_[min/max/avr]_mcs - time from localPostBuffer to when buffer was acquired by + * surfaceflinger. + * latency_[min/max/avr]_mcs - time from localPostBuffer to when buffer was released by + * surfaceflinger. + * missed_frame_percents - percentage of missed frames (frames that do not have right sequence + * of events). + * + * Following is example of atrace logs from different platforms + * <...>-5237 (-----) [000] ...1 228.380392: tracing_mark_write: B|11|SurfaceView - android.gameperformance/android.gameperformance.GamePerformanceActivity#0: 2 + * surfaceflinger-5855 ( 5855) [001] ...1 169.627364: tracing_mark_write: B|24|acquireBuffer + * HwBinder:617_2-652 ( 617) [002] d..1 360262.921756: sde_evtlog: 617|sde_encoder_virt_atomic_check:855|19|0|0|0|0|0|0|0|0|0|0|0|0|0|0 + */ +public class GraphicBufferMetrics { + private final static String TAG = "GraphicBufferMetrics"; + + private final static String KEY_POST_TIME = "post_time"; + private final static String KEY_READY_TIME = "ready_time"; + private final static String KEY_LATENCY = "latency"; + private final static String SUFFIX_MIN = "min"; + private final static String SUFFIX_MAX = "max"; + private final static String SUFFIX_MEDIAN = "median"; + private final static String KEY_MISSED_FRAME_RATE = "missed_frame_percents"; + + private final static int EVENT_POST_BUFFER = 0; + private final static int EVENT_QUEUE_BUFFER = 1; + private final static int EVENT_ACQUIRE_BUFFER = 2; + private final static int EVENT_RELEASE_BUFFER = 3; + + // atrace prints this line. Used as a marker to make sure that we can parse its output. + private final static String ATRACE_HEADER = + "# TASK-PID TGID CPU# |||| TIMESTAMP FUNCTION"; + + private final static String[] KNOWN_PHRASES = new String[] { + "capturing trace... done", "TRACE:"}; + private final static List<String> KNWON_PHRASES_LIST = Arrays.asList(KNOWN_PHRASES); + + // Type of the atrace event we can parse and analyze. + private final static String FUNCTION_TRACING_MARK_WRITE = "tracing_mark_write"; + + // Trace event we can ignore. It contains current timestamp information for the atrace output. + private final static String TRACE_EVENT_CLOCK_SYNC = "trace_event_clock_sync:"; + + // Threshold we consider test passes successfully. If we cannot collect enough amount of frames + // let fail the test. 50 is calculated 10 frames per second running for five seconds. + private final static int MINIMAL_SAMPLE_CNT_TO_PASS = 50; + + /** + * Raw event in atrace. Stored hierarchically. + */ + private static class RawEvent { + // Parent of this event or null for the root holder. + public final RawEvent mParent; + // Time of the event in mcs. + public final long mTime; + // Duration of the event in mcs. + public long mDuration; + // Name/body of the event. + public final String mName; + // Children events. + public final List<RawEvent> mChildren; + + public RawEvent(RawEvent parent, long time, String name) { + mParent = parent; + mTime = time; + mName = name; + mDuration = -1; + mChildren = new ArrayList<>(); + } + + /** + * Recursively finds child events. + * @param path specify path to events. For example a/b. That means to find child with name + * 'a' of the current event and in this child find child with name 'b'. Path + * can consist from only one segment and that means we analyze only children of + * the current event. + * @param collector to collect found events. + */ + public void findEvents(String path, List<RawEvent> collector) { + final int separator = path.indexOf('/'); + final String current = separator > 0 ? path.substring(0, separator) : path; + final String nextPath = separator > 0 ? path.substring(separator + 1) : null; + for (RawEvent child : mChildren) { + if (child.mName.equals(current)) { + if (nextPath != null) { + child.findEvents(nextPath, collector); + } else { + collector.add(child); + } + } + } + } + + public void dump(String prefix) { + System.err.print(prefix); + System.err.println(mTime + "[" + mDuration + "] " + mName); + for (RawEvent e : mChildren) { + e.dump(prefix + " "); + } + } + } + + /** + * Describes graphic buffer event. local post, queued, acquired, released. + */ + private static class BufferEvent { + public final int mType; + public final long mTime; + public final long mDuration; + public final String mBufferId; + + public BufferEvent(int type, long time, long duration, String bufferId) { + mType = type; + mTime = time; + mDuration = duration; + mBufferId = bufferId; + } + + @Override + public String toString() { + return "Type: " + mType + ". Time: " + mTime + + "[" + mDuration + "]. Buffer: " + mBufferId + "."; + } + } + + /** + * Returns true if given char is digit. + */ + private static boolean isDigitChar(char c) { + return (c >= '0') && (c <= '9'); + } + + /** + * Returns true if given char is digit or '.'. + */ + private static boolean isDoubleDigitChar(char c) { + return (c == '.') || isDigitChar(c); + } + + /** + * Convert timestamp string that represents double value in seconds to long time that represents + * timestamp in microseconds. + */ + private static long getTimeStamp(String timeStampStr) { + return (long)(1000000.0 * Double.parseDouble(timeStampStr)); + } + + /** + * Reads atrace log and build event model. Result is a map, where key specifies event for the + * particular thread. Value is the synthetic root RawEvent that holds all events for the + * thread. Events are stored hierarchically. + */ + private static Map<Integer, RawEvent> buildEventModel(String fileName) throws IOException { + Map<Integer, RawEvent> result = new HashMap<>(); + + BufferedReader bufferedReader = null; + String line = ""; + boolean headerDetected = false; + try { + bufferedReader = new BufferedReader(new FileReader(fileName)); + while ((line = bufferedReader.readLine()) != null) { + // Make sure we find comment that describes output format we can with. + headerDetected |= line.equals(ATRACE_HEADER); + // Skip comments. + if (line.startsWith("#")) { + continue; + } + // Skip known service output + if (KNWON_PHRASES_LIST.contains(line)) { + continue; + } + + if (!headerDetected) { + // We don't know the format of this line. + throw new IllegalStateException("Header was not detected"); + } + + // TASK-PID in header exists at position 12. PID position 17 should contains first + // digit of thread id after the '-'. + if (!isDigitChar(line.charAt(17)) || line.charAt(16) != '-') { + throw new IllegalStateException("Failed to parse thread id: " + line); + } + int rightIndex = line.indexOf(' ', 17); + final String threadIdStr = line.substring(17, rightIndex); + final int threadId = Integer.parseInt(threadIdStr); + + // TIMESTAMP in header exists at position 45 + // This position should point in the middle of timestamp which is ended by ':'. + int leftIndex = 45; + while (isDoubleDigitChar(line.charAt(leftIndex))) { + --leftIndex; + } + rightIndex = line.indexOf(':', 45); + + final String timeStampString = line.substring(leftIndex + 1, rightIndex); + final long timeStampMcs = getTimeStamp(timeStampString); + + // Find function name, pointed by FUNCTION. Long timestamp can shift if position + // so use end of timestamp to find the function which is ended by ':'. + leftIndex = rightIndex + 1; + while (Character.isWhitespace(line.charAt(leftIndex))) { + ++leftIndex; + } + rightIndex = line.indexOf(':', leftIndex); + final String function = line.substring(leftIndex, rightIndex); + + if (!function.equals(FUNCTION_TRACING_MARK_WRITE)) { + continue; + } + + // Rest of the line is event body. + leftIndex = rightIndex + 1; + while (Character.isWhitespace(line.charAt(leftIndex))) { + ++leftIndex; + } + + final String event = line.substring(leftIndex); + if (event.startsWith(TRACE_EVENT_CLOCK_SYNC)) { + continue; + } + + // Parse event, for example: + // B|615|SurfaceView - android.gameperformance.GamePerformanceActivity#0: 1 + // E|615 + // C|11253|operation id|2 + StringTokenizer eventTokenizer = new StringTokenizer(event, "|"); + final String eventType = eventTokenizer.nextToken(); + + // Attach root on demand. + if (!result.containsKey(threadId)) { + result.put(threadId, new RawEvent(null /* parent */, + timeStampMcs, + "#ROOT_" + threadId)); + } + + switch (eventType) { + case "B": { + // Log entry starts. + eventTokenizer.nextToken(); // PID + String eventText = eventTokenizer.nextToken(); + while (eventTokenizer.hasMoreTokens()) { + eventText += " "; + eventText += eventTokenizer.nextToken(); + } + RawEvent parent = result.get(threadId); + RawEvent current = new RawEvent(parent, timeStampMcs, eventText); + parent.mChildren.add(current); + result.put(threadId, current); + } + break; + case "E": { + // Log entry ends. + RawEvent current = result.get(threadId); + current.mDuration = timeStampMcs - current.mTime; + if (current.mParent == null) { + // Detect a tail of the previous call. Remove last child element if it + // exists once it does not belong to the root. + if (!current.mChildren.isEmpty()) { + current.mChildren.remove(current.mChildren.size() -1); + } + } else { + result.put(threadId, current.mParent); + } + } + break; + case "C": + // Counter, ignore + break; + default: + throw new IllegalStateException( + "Unrecognized trace: " + line + " # " + eventType + " # " + event); + } + } + + // Detect incomplete events and detach from the root. + Set<Integer> threadIds = new TreeSet<>(); + threadIds.addAll(result.keySet()); + for (int threadId : threadIds) { + RawEvent root = result.get(threadId); + if (root.mParent == null) { + // Last trace was closed. + continue; + } + // Find the root. + while (root.mParent != null) { + root = root.mParent; + } + // Discard latest incomplete element. + root.mChildren.remove(root.mChildren.size() - 1); + result.put(threadId, root); + } + } catch (Exception e) { + throw new IOException("Failed to process " + line, e); + } finally { + Utils.closeQuietly(bufferedReader); + } + + return result; + } + + /** + * Processes provided atrace log and calculates graphics buffer metrics. + * @param fileName name of atrace log file. + * @param testTag tag to separate results for the different passes. + */ + public static Map<String, Double> processGraphicBufferResult( + String fileName, String testTag) throws IOException { + final Map<Integer, RawEvent> model = buildEventModel(fileName); + + List<RawEvent> collectorPostBuffer = new ArrayList<>(); + List<RawEvent> collectorQueueBuffer = new ArrayList<>(); + List<RawEvent> collectorReleaseBuffer = new ArrayList<>(); + List<RawEvent> collectorAcquireBuffer = new ArrayList<>(); + + // Collect required events. + for (RawEvent root : model.values()) { + // Surface view + root.findEvents("localPostBuffer", collectorPostBuffer); + // OpengGL view + root.findEvents("eglSwapBuffersWithDamageKHR", collectorPostBuffer); + + root.findEvents("queueBuffer", collectorQueueBuffer); + root.findEvents("onMessageReceived/handleMessageInvalidate/latchBuffer/" + + "updateTexImage/acquireBuffer", + collectorAcquireBuffer); + // PI stack + root.findEvents( + "onMessageReceived/handleMessageRefresh/postComposition/releaseBuffer", + collectorReleaseBuffer); + // NYC stack + root.findEvents( + "onMessageReceived/handleMessageRefresh/releaseBuffer", + collectorReleaseBuffer); + } + + // Convert raw event to buffer events. + List<BufferEvent> bufferEvents = new ArrayList<>(); + for (RawEvent event : collectorPostBuffer) { + bufferEvents.add( + new BufferEvent(EVENT_POST_BUFFER, event.mTime, event.mDuration, null)); + } + toBufferEvents(EVENT_QUEUE_BUFFER, collectorQueueBuffer, bufferEvents); + toBufferEvents(EVENT_ACQUIRE_BUFFER, collectorAcquireBuffer, bufferEvents); + toBufferEvents(EVENT_RELEASE_BUFFER, collectorReleaseBuffer, bufferEvents); + + // Sort events based on time. These events are originally taken from different threads so + // order is not always preserved. + Collections.sort(bufferEvents, new Comparator<BufferEvent>() { + @Override + public int compare(BufferEvent o1, BufferEvent o2) { + if (o1.mTime < o2.mTime) { + return -1; + } if (o1.mTime > o2.mTime) { + return +1; + } else { + return 0; + } + } + }); + + // Collect samples. + List<Long> postTimes = new ArrayList<>(); + List<Long> readyTimes = new ArrayList<>(); + List<Long> latencyTimes = new ArrayList<>(); + long missedCnt = 0; + + for (int i = 0; i < bufferEvents.size(); ++i) { + if (bufferEvents.get(i).mType != EVENT_POST_BUFFER) { + continue; + } + final int queueIndex = findNextOfType(bufferEvents, i + 1, EVENT_QUEUE_BUFFER); + if (queueIndex < 0) { + break; + } + final int acquireIndex = findNextOfBufferId(bufferEvents, queueIndex + 1, + bufferEvents.get(queueIndex).mBufferId); + if (acquireIndex < 0) { + break; + } + if (bufferEvents.get(acquireIndex).mType != EVENT_ACQUIRE_BUFFER) { + // Was not actually presented. + ++missedCnt; + continue; + } + final int releaseIndex = findNextOfBufferId(bufferEvents, acquireIndex + 1, + bufferEvents.get(queueIndex).mBufferId); + if (releaseIndex < 0) { + break; + } + if (bufferEvents.get(releaseIndex).mType != EVENT_RELEASE_BUFFER) { + // Was not actually presented. + ++missedCnt; + continue; + } + + postTimes.add(bufferEvents.get(i).mDuration); + readyTimes.add( + bufferEvents.get(acquireIndex).mTime - bufferEvents.get(i).mTime); + latencyTimes.add( + bufferEvents.get(releaseIndex).mTime - bufferEvents.get(i).mTime); + } + + if (postTimes.size() < MINIMAL_SAMPLE_CNT_TO_PASS) { + throw new IllegalStateException("Too few sample cnt: " + postTimes.size() +". " + + MINIMAL_SAMPLE_CNT_TO_PASS + " is required."); + } + + Map<String, Double> status = new TreeMap<>(); + addResults(status, testTag, KEY_POST_TIME, postTimes); + addResults(status, testTag, KEY_READY_TIME, readyTimes); + addResults(status, testTag, KEY_LATENCY, latencyTimes); + status.put(testTag + "_" + KEY_MISSED_FRAME_RATE, + 100.0 * missedCnt / (missedCnt + postTimes.size())); + return status; + } + + private static void addResults( + Map<String, Double> status, String tag, String key, List<Long> times) { + Collections.sort(times); + long min = times.get(0); + long max = times.get(0); + for (long time : times) { + min = Math.min(min, time); + max = Math.max(max, time); + } + status.put(tag + "_" + key + "_" + SUFFIX_MIN, (double)min); + status.put(tag + "_" + key + "_" + SUFFIX_MAX, (double)max); + status.put(tag + "_" + key + "_" + SUFFIX_MEDIAN, (double)times.get(times.size() / 2)); + } + + // Helper to convert surface flinger events to buffer events. + private static void toBufferEvents( + int type, List<RawEvent> rawEvents, List<BufferEvent> bufferEvents) { + for (RawEvent event : rawEvents) { + if (event.mChildren.isEmpty()) { + throw new IllegalStateException("Buffer name is expected"); + } + final String bufferName = event.mChildren.get(0).mName; + if (bufferName.startsWith("SurfaceView - android.gameperformance")) { + bufferEvents.add( + new BufferEvent(type, event.mTime, event.mDuration, bufferName)); + } + } + } + + private static int findNextOfType(List<BufferEvent> events, int startIndex, int type) { + for (int i = startIndex; i < events.size(); ++i) { + if (events.get(i).mType == type) { + return i; + } + } + return -1; + } + + private static int findNextOfBufferId( + List<BufferEvent> events, int startIndex, String bufferId) { + for (int i = startIndex; i < events.size(); ++i) { + if (bufferId.equals(events.get(i).mBufferId)) { + return i; + } + } + return -1; + } + + public static void main(String[] args) { + if (args.length != 1) { + System.err.println("Usage: " + TAG + " atrace.log"); + return; + } + + try { + System.out.println("Results:"); + for (Map.Entry<?, ?> entry : + processGraphicBufferResult(args[0], "default").entrySet()) { + System.out.println(" " + entry.getKey() + " = " + entry.getValue()); + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/tests/GamePerformance/src/android/gameperformance/Utils.java b/tests/GamePerformance/src/android/gameperformance/Utils.java new file mode 100644 index 000000000000..64819712bf6d --- /dev/null +++ b/tests/GamePerformance/src/android/gameperformance/Utils.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.gameperformance; + +import java.io.Closeable; +import java.io.IOException; + +public class Utils { + public static void closeQuietly(Closeable closeable) { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException e) { + // Ignore + } + } +} diff --git a/tests/HwAccelerationTest/Android.bp b/tests/HwAccelerationTest/Android.bp index abcd73b115af..37d3f5d4d97f 100644 --- a/tests/HwAccelerationTest/Android.bp +++ b/tests/HwAccelerationTest/Android.bp @@ -18,4 +18,5 @@ android_test { name: "HwAccelerationTest", srcs: ["**/*.java"], platform_apis: true, + certificate: "platform", } diff --git a/tests/HwAccelerationTest/AndroidManifest.xml b/tests/HwAccelerationTest/AndroidManifest.xml index c8f96c9f0670..7b8c154dea1e 100644 --- a/tests/HwAccelerationTest/AndroidManifest.xml +++ b/tests/HwAccelerationTest/AndroidManifest.xml @@ -310,6 +310,15 @@ <category android:name="com.android.test.hwui.TEST" /> </intent-filter> </activity> + + <activity + android:name="PictureCaptureDemo" + android:label="Debug/Picture Capture"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="com.android.test.hwui.TEST" /> + </intent-filter> + </activity> <activity android:name="SmallCircleActivity" @@ -1018,5 +1027,35 @@ </intent-filter> </activity> + <activity + android:name="PositionListenerActivity" + android:label="RenderNode/PositionListener" + android:screenOrientation="fullSensor"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="com.android.test.hwui.TEST" /> + </intent-filter> + </activity> + + <activity + android:name="CustomRenderer" + android:label="HardwareRenderer/HelloTakeSurface" + android:screenOrientation="fullSensor"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="com.android.test.hwui.TEST" /> + </intent-filter> + </activity> + + <activity + android:name="MyLittleTextureView" + android:label="HardwareRenderer/MyLittleTextureView" + android:screenOrientation="fullSensor"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="com.android.test.hwui.TEST" /> + </intent-filter> + </activity> + </application> </manifest> diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/CirclePropActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/CirclePropActivity.java index 5bc8934279d6..571f623aea99 100644 --- a/tests/HwAccelerationTest/src/com/android/test/hwui/CirclePropActivity.java +++ b/tests/HwAccelerationTest/src/com/android/test/hwui/CirclePropActivity.java @@ -22,9 +22,9 @@ import android.graphics.Canvas; import android.graphics.CanvasProperty; import android.graphics.Paint; import android.graphics.Paint.Style; +import android.graphics.RecordingCanvas; import android.os.Bundle; import android.os.Trace; -import android.view.DisplayListCanvas; import android.view.RenderNodeAnimator; import android.view.View; import android.widget.LinearLayout; @@ -88,8 +88,8 @@ public class CirclePropActivity extends Activity { super.onDraw(canvas); if (canvas.isHardwareAccelerated()) { - DisplayListCanvas displayListCanvas = (DisplayListCanvas) canvas; - displayListCanvas.drawCircle(mX, mY, mRadius, mPaint); + RecordingCanvas recordingCanvas = (RecordingCanvas) canvas; + recordingCanvas.drawCircle(mX, mY, mRadius, mPaint); } } diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/ColorFiltersMutateActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/ColorFiltersMutateActivity.java index a7bdabd64684..0787d823756c 100644 --- a/tests/HwAccelerationTest/src/com/android/test/hwui/ColorFiltersMutateActivity.java +++ b/tests/HwAccelerationTest/src/com/android/test/hwui/ColorFiltersMutateActivity.java @@ -106,7 +106,7 @@ public class ColorFiltersMutateActivity extends Activity { mPorterDuffColor = porterDuffColor; final PorterDuffColorFilter filter = (PorterDuffColorFilter) mBlendPaint.getColorFilter(); - filter.setColor(mPorterDuffColor); + mBlendPaint.setColorFilter(new PorterDuffColorFilter(porterDuffColor, filter.getMode())); invalidate(); } diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/CustomRenderer.java b/tests/HwAccelerationTest/src/com/android/test/hwui/CustomRenderer.java new file mode 100644 index 000000000000..5ad7fb9027a2 --- /dev/null +++ b/tests/HwAccelerationTest/src/com/android/test/hwui/CustomRenderer.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.hwui; + +import android.animation.ObjectAnimator; +import android.app.Activity; +import android.graphics.Color; +import android.graphics.HardwareRenderer; +import android.graphics.Paint; +import android.graphics.RecordingCanvas; +import android.graphics.RenderNode; +import android.os.Bundle; +import android.os.Handler; +import android.view.SurfaceHolder; + +public class CustomRenderer extends Activity { + private RenderNode mRootNode = new RenderNode("CustomRenderer"); + private RenderNode mChildNode = new RenderNode("RedBox"); + private HardwareRenderer mRenderer = new HardwareRenderer(); + private ObjectAnimator mAnimator; + private Handler mRedrawHandler = new Handler(true); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getWindow().takeSurface(mSurfaceCallbacks); + } + + @Override + protected void onStart() { + super.onStart(); + mAnimator = ObjectAnimator.ofFloat(mChildNode, "translationY", 0, 300); + mAnimator.setRepeatMode(ObjectAnimator.REVERSE); + mAnimator.setRepeatCount(ObjectAnimator.INFINITE); + final Runnable redraw = this::draw; + mAnimator.addUpdateListener(animation -> { + mRedrawHandler.post(redraw); + }); + } + + @Override + protected void onStop() { + super.onStop(); + mAnimator.end(); + mAnimator = null; + } + + private void setupRoot(int width, int height) { + mRootNode.setPosition(0, 0, width, height); + + RecordingCanvas canvas = mRootNode.beginRecording(); + canvas.drawColor(Color.WHITE); + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setColor(Color.BLACK); + paint.setTextAlign(Paint.Align.CENTER); + float textSize = Math.min(width, height) * .05f; + paint.setTextSize(textSize); + canvas.drawText("Hello custom renderer!", width / 2, textSize * 2, paint); + + canvas.translate(0, height / 4); + canvas.drawRenderNode(mChildNode); + canvas.translate(width / 2, 0); + canvas.drawRenderNode(mChildNode); + mRootNode.endRecording(); + + setupChild(width / 2, height / 2); + } + + private void setupChild(int width, int height) { + mChildNode.setPosition(0, 0, width, height); + mChildNode.setScaleX(.5f); + mChildNode.setScaleY(.5f); + + RecordingCanvas canvas = mChildNode.beginRecording(); + canvas.drawColor(Color.RED); + mChildNode.endRecording(); + } + + private void draw() { + // Since we are constantly pumping frames between onStart & onStop we don't really + // care about any errors that may happen. They will self-correct. + mRenderer.createRenderRequest() + .setVsyncTime(System.nanoTime()) + .syncAndDraw(); + } + + private SurfaceHolder.Callback2 mSurfaceCallbacks = new SurfaceHolder.Callback2() { + + @Override + public void surfaceRedrawNeeded(SurfaceHolder holder) { + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + setupRoot(width, height); + + mRenderer.setContentRoot(mRootNode); + mRenderer.setSurface(holder.getSurface()); + draw(); + if (!mAnimator.isStarted()) { + mAnimator.start(); + } + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + mRenderer.destroy(); + } + }; +} diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/DrawIntoHwBitmapActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/DrawIntoHwBitmapActivity.java index af8e10bc07ae..220016aa8ab7 100644 --- a/tests/HwAccelerationTest/src/com/android/test/hwui/DrawIntoHwBitmapActivity.java +++ b/tests/HwAccelerationTest/src/com/android/test/hwui/DrawIntoHwBitmapActivity.java @@ -16,25 +16,13 @@ package com.android.test.hwui; -import static android.graphics.GraphicBuffer.USAGE_HW_TEXTURE; -import static android.graphics.GraphicBuffer.USAGE_SW_READ_NEVER; -import static android.graphics.GraphicBuffer.USAGE_SW_WRITE_NEVER; -import static android.graphics.GraphicBuffer.USAGE_SW_WRITE_RARELY; - import android.app.Activity; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; -import android.graphics.GraphicBuffer; import android.graphics.Paint; import android.graphics.Picture; -import android.graphics.PixelFormat; -import android.graphics.SurfaceTexture; import android.os.Bundle; -import android.view.DisplayListCanvas; -import android.view.RenderNode; -import android.view.Surface; -import android.view.ThreadedRenderer; import android.widget.ImageView; public class DrawIntoHwBitmapActivity extends Activity { diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/MultiProducerActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/MultiProducerActivity.java index 7713f5da36ed..e7d7f2b11801 100644 --- a/tests/HwAccelerationTest/src/com/android/test/hwui/MultiProducerActivity.java +++ b/tests/HwAccelerationTest/src/com/android/test/hwui/MultiProducerActivity.java @@ -21,12 +21,12 @@ import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; +import android.graphics.RecordingCanvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.view.DisplayListCanvas; import android.view.ThreadedRenderer; -import android.view.RenderNode; +import android.graphics.RenderNode; import android.view.View; import android.view.View.OnClickListener; import android.widget.AbsoluteLayout; @@ -206,7 +206,7 @@ public class MultiProducerActivity extends Activity implements OnClickListener { } // Draw frame - DisplayListCanvas canvas = nodeFrame.start(currentFrameBounds.width(), + RecordingCanvas canvas = nodeFrame.start(currentFrameBounds.width(), currentFrameBounds.height()); mFrameContent.draw(canvas); nodeFrame.end(canvas); @@ -228,7 +228,7 @@ public class MultiProducerActivity extends Activity implements OnClickListener { } // Draw Backdrop - DisplayListCanvas canvas = nodeBack.start(currentBackBounds.width(), + RecordingCanvas canvas = nodeBack.start(currentBackBounds.width(), currentBackBounds.height()); mBackContent.draw(canvas); nodeBack.end(canvas); diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/MyLittleTextureView.java b/tests/HwAccelerationTest/src/com/android/test/hwui/MyLittleTextureView.java new file mode 100644 index 000000000000..08d5d4fff50a --- /dev/null +++ b/tests/HwAccelerationTest/src/com/android/test/hwui/MyLittleTextureView.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.hwui; + +import android.app.Activity; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorSpace; +import android.graphics.HardwareRenderer; +import android.graphics.Outline; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RenderNode; +import android.hardware.HardwareBuffer; +import android.media.Image; +import android.media.ImageReader; +import android.os.Bundle; +import android.widget.ImageView; + +public class MyLittleTextureView extends Activity { + private RenderNode mContent = new RenderNode("CustomRenderer"); + private HardwareRenderer mRenderer = new HardwareRenderer(); + private ImageView mImageView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mImageView = new ImageView(this); + mImageView.setScaleType(ImageView.ScaleType.FIT_CENTER); + setContentView(mImageView); + + ImageReader reader = ImageReader.newInstance(100, 100, PixelFormat.RGBA_8888, 3, + HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE | HardwareBuffer.USAGE_GPU_COLOR_OUTPUT); + mRenderer.setSurface(reader.getSurface()); + mRenderer.setLightSourceAlpha(0.0f, 1.0f); + mRenderer.setLightSourceGeometry(100 / 2f, 0f, 800.0f, 20.0f); + mContent.setLeftTopRightBottom(0, 0, 100, 100); + + Rect childRect = new Rect(25, 25, 65, 65); + RenderNode childNode = new RenderNode("shadowCaster"); + childNode.setLeftTopRightBottom(childRect.left, childRect.top, + childRect.right, childRect.bottom); + Outline outline = new Outline(); + outline.setRect(new Rect(0, 0, childRect.width(), childRect.height())); + outline.setAlpha(1f); + childNode.setOutline(outline); + { + Canvas canvas = childNode.beginRecording(); + canvas.drawColor(Color.BLUE); + } + childNode.endRecording(); + childNode.setElevation(20f); + + { + Canvas canvas = mContent.beginRecording(); + canvas.drawColor(Color.WHITE); + canvas.enableZ(); + canvas.drawRenderNode(childNode); + canvas.disableZ(); + } + mContent.endRecording(); + mRenderer.setContentRoot(mContent); + mRenderer.createRenderRequest() + .setWaitForPresent(true) + .syncAndDraw(); + Image image = reader.acquireNextImage(); + Bitmap bitmap = Bitmap.wrapHardwareBuffer(image.getHardwareBuffer(), + ColorSpace.get(ColorSpace.Named.SRGB)); + mImageView.setImageBitmap(bitmap); + image.close(); + } +} diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/PictureCaptureDemo.java b/tests/HwAccelerationTest/src/com/android/test/hwui/PictureCaptureDemo.java new file mode 100644 index 000000000000..029e302d0382 --- /dev/null +++ b/tests/HwAccelerationTest/src/com/android/test/hwui/PictureCaptureDemo.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.hwui; + +import android.app.Activity; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Picture; +import android.os.Bundle; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewDebug; +import android.webkit.WebChromeClient; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.LinearLayout.LayoutParams; +import android.widget.ProgressBar; + +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +public class PictureCaptureDemo extends Activity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + + final LinearLayout inner = new LinearLayout(this); + inner.setOrientation(LinearLayout.HORIZONTAL); + ProgressBar spinner = new ProgressBar(this, null, android.R.attr.progressBarStyleLarge); + inner.addView(spinner, + new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + + inner.addView(new View(this), new LayoutParams(50, 1)); + + Picture picture = new Picture(); + Canvas canvas = picture.beginRecording(100, 100); + canvas.drawColor(Color.RED); + Paint paint = new Paint(); + paint.setTextSize(32); + paint.setColor(Color.BLACK); + canvas.drawText("Hello", 0, 50, paint); + picture.endRecording(); + + ImageView iv1 = new ImageView(this); + iv1.setImageBitmap(Bitmap.createBitmap(picture, 100, 100, Bitmap.Config.ARGB_8888)); + inner.addView(iv1, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + + inner.addView(new View(this), new LayoutParams(50, 1)); + + ImageView iv2 = new ImageView(this); + iv2.setImageBitmap(Bitmap.createBitmap(picture, 100, 100, Bitmap.Config.HARDWARE)); + inner.addView(iv2, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + + layout.addView(inner, + new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + // For testing with a functor in the tree + WebView wv = new WebView(this); + wv.setWebViewClient(new WebViewClient()); + wv.setWebChromeClient(new WebChromeClient()); + wv.loadUrl("https://google.com"); + layout.addView(wv, new LayoutParams(LayoutParams.MATCH_PARENT, 400)); + + SurfaceView mySurfaceView = new SurfaceView(this); + layout.addView(mySurfaceView, + new LayoutParams(LayoutParams.MATCH_PARENT, 600)); + + setContentView(layout); + + mySurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() { + private AutoCloseable mStopCapture; + + @Override + public void surfaceCreated(SurfaceHolder holder) { + final Random rand = new Random(); + mStopCapture = ViewDebug.startRenderingCommandsCapture(mySurfaceView, + mCaptureThread, (picture) -> { + if (rand.nextInt(20) == 0) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + } + Canvas canvas = holder.lockCanvas(); + if (canvas == null) { + return false; + } + canvas.drawPicture(picture); + holder.unlockCanvasAndPost(canvas); + picture.close(); + return true; + }); + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + if (mStopCapture != null) { + try { + mStopCapture.close(); + } catch (Exception e) { + } + mStopCapture = null; + } + } + }); + } + + ExecutorService mCaptureThread = Executors.newSingleThreadExecutor(); + ExecutorService mExecutor = Executors.newSingleThreadExecutor(); + + Picture deepCopy(Picture src) { + try { + PipedInputStream inputStream = new PipedInputStream(); + PipedOutputStream outputStream = new PipedOutputStream(inputStream); + Future<Picture> future = mExecutor.submit(() -> Picture.createFromStream(inputStream)); + src.writeToStream(outputStream); + outputStream.close(); + return future.get(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/PositionListenerActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/PositionListenerActivity.java new file mode 100644 index 000000000000..818d899413de --- /dev/null +++ b/tests/HwAccelerationTest/src/com/android/test/hwui/PositionListenerActivity.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.hwui; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.RenderNode; +import android.os.Bundle; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.ScrollView; +import android.widget.TextView; + +@SuppressWarnings({"UnusedDeclaration"}) +public class PositionListenerActivity extends Activity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + + ProgressBar spinner = new ProgressBar(this, null, android.R.attr.progressBarStyleLarge); + layout.addView(spinner); + + ScrollView scrollingThing = new ScrollView(this); + scrollingThing.addView(new MyPositionReporter(this)); + layout.addView(scrollingThing); + + setContentView(layout); + } + + static class MyPositionReporter extends TextView implements RenderNode.PositionUpdateListener { + RenderNode mNode; + int mCurrentCount = 0; + int mTranslateY = 0; + + MyPositionReporter(Context c) { + super(c); + mNode = new RenderNode("positionListener"); + mNode.addPositionUpdateListener(this); + setTextAlignment(TEXT_ALIGNMENT_VIEW_START); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + setMeasuredDimension(getMeasuredWidth(), 10000); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + mNode.setLeftTopRightBottom(left, top, right, bottom); + } + + @Override + protected void onDraw(Canvas canvas) { + ScrollView parent = (ScrollView) getParent(); + canvas.translate(0, parent.getScrollY()); + super.onDraw(canvas); + canvas.translate(0, -parent.getScrollY()); + // Inject our listener proxy + canvas.drawRenderNode(mNode); + } + + @Override + public void positionChanged(long frameNumber, int left, int top, int right, int bottom) { + post(() -> { + mCurrentCount++; + setText(String.format("%d: Position [%d, %d, %d, %d]", mCurrentCount, + left, top, right, bottom)); + }); + } + + @Override + public void positionLost(long frameNumber) { + post(() -> { + mCurrentCount++; + setText(mCurrentCount + " No position"); + }); + } + } +} diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/ProjectionActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/ProjectionActivity.java index be5d7f98783d..4eb40722f6dd 100644 --- a/tests/HwAccelerationTest/src/com/android/test/hwui/ProjectionActivity.java +++ b/tests/HwAccelerationTest/src/com/android/test/hwui/ProjectionActivity.java @@ -8,9 +8,8 @@ import android.os.Bundle; import android.app.Activity; import android.util.AttributeSet; -import android.view.RenderNode; +import android.graphics.RenderNode; import android.view.View; -import android.widget.LinearLayout; public class ProjectionActivity extends Activity { /** diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/ProjectionClippingActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/ProjectionClippingActivity.java index 2ae960bd08db..9abd7ea5f361 100644 --- a/tests/HwAccelerationTest/src/com/android/test/hwui/ProjectionClippingActivity.java +++ b/tests/HwAccelerationTest/src/com/android/test/hwui/ProjectionClippingActivity.java @@ -1,13 +1,7 @@ package com.android.test.hwui; import android.app.Activity; -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.RectF; import android.os.Bundle; -import android.util.AttributeSet; -import android.view.RenderNode; import android.view.View; public class ProjectionClippingActivity extends Activity { diff --git a/tests/ImfTest/Android.mk b/tests/ImfTest/Android.mk deleted file mode 100644 index a8f5b0867c65..000000000000 --- a/tests/ImfTest/Android.mk +++ /dev/null @@ -1,16 +0,0 @@ -LOCAL_PATH:= $(call my-dir) -include $(CLEAR_VARS) - -# We only want this apk build for tests. -LOCAL_MODULE_TAGS := tests - -# Only compile source java files in this apk. -LOCAL_SRC_FILES := $(call all-java-files-under, src) - -LOCAL_PACKAGE_NAME := ImfTest -LOCAL_PRIVATE_PLATFORM_APIS := true - -include $(BUILD_PACKAGE) - -# Use the following include to make our test apk. -include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/tests/ImfTest/AndroidManifest.xml b/tests/ImfTest/AndroidManifest.xml deleted file mode 100644 index 82dbe753534c..000000000000 --- a/tests/ImfTest/AndroidManifest.xml +++ /dev/null @@ -1,146 +0,0 @@ -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.imftest"> - - <uses-permission android:name="android.permission.DISABLE_KEYGUARD" /> - - <application> - - <activity android:name=".samples.InputTypeActivity" android:label="Input Type Activity"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER" /> - <category android:name="android.intent.category.IMF_TEST" /> - </intent-filter> - </activity> - - <activity android:name=".samples.ButtonActivity" android:label="Button Activity"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER" /> - <category android:name="android.intent.category.IMF_TEST" /> - </intent-filter> - </activity> - - <activity android:name=".samples.BigEditTextActivityNonScrollablePanScan" android:label="Big ET !Scroll Pan/Scan"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER" /> - <category android:name="android.intent.category.IMF_TEST" /> - </intent-filter> - </activity> - - <activity android:name=".samples.ManyEditTextActivityNoScrollPanScan" android:label="ManyEditTextActivityNoScrollPanScan"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER" /> - <category android:name="android.intent.category.IMF_TEST" /> - </intent-filter> - </activity> - - <activity android:name=".samples.BigEditTextActivityNonScrollableResize" android:label="Big ET !Scroll Resize"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER" /> - <category android:name="android.intent.category.IMF_TEST" /> - </intent-filter> - </activity> - - <activity android:name=".samples.BigEditTextActivityScrollablePanScan" android:label="Big ET Scroll Pan/Scan"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER" /> - <category android:name="android.intent.category.IMF_TEST" /> - </intent-filter> - </activity> - - <activity android:name=".samples.BigEditTextActivityScrollableResize" android:label="Big ET Scroll Resize"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER" /> - <category android:name="android.intent.category.IMF_TEST" /> - </intent-filter> - </activity> - - <activity android:name=".samples.EditTextActivityDialog" android:label="ET Dialog"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER" /> - <category android:name="android.intent.category.IMF_TEST" /> - </intent-filter> - </activity> - - <activity android:name=".samples.ManyEditTextActivityScrollPanScan" android:label="ManyEditTextActivityScrollPanScan"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER" /> - <category android:name="android.intent.category.IMF_TEST" /> - </intent-filter> - </activity> - - <activity android:name=".samples.ManyEditTextActivityScrollResize" android:label="ManyEditTextActivityScrollResize"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER" /> - <category android:name="android.intent.category.IMF_TEST" /> - </intent-filter> - </activity> - - <activity android:name=".samples.BottomEditTextActivityPanScan" android:label="BottomEditTextActivityPanScan"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER" /> - <category android:name="android.intent.category.IMF_TEST" /> - </intent-filter> - </activity> - - <activity android:name=".samples.BottomEditTextActivityResize" android:label="BottomEditTextActivityResize"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER" /> - <category android:name="android.intent.category.IMF_TEST" /> - </intent-filter> - </activity> - - <activity android:name=".samples.OneEditTextActivitySelected" android:label="OneEditTextActivitySelected"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER" /> - <category android:name="android.intent.category.IMF_TEST" /> - </intent-filter> - </activity> - - <activity android:name=".samples.OneEditTextActivityNotSelected" android:label="OneEditTextActivityNotSelected"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER" /> - <category android:name="android.intent.category.IMF_TEST" /> - </intent-filter> - </activity> - - <activity android:name=".samples.AutoCompleteTextViewActivityPortrait" android:label="AutoCompleteTextViewActivityPortrait" android:screenOrientation="portrait"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER" /> - <category android:name="android.intent.category.IMF_TEST" /> - </intent-filter> - </activity> - - <activity android:name=".samples.AutoCompleteTextViewActivityLandscape" android:label="AutoCompleteTextViewActivityLandscape" android:screenOrientation="landscape"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER" /> - <category android:name="android.intent.category.IMF_TEST" /> - </intent-filter> - </activity> - - <activity android:name=".samples.DialogActivity" android:label="DialogActivity" android:screenOrientation="portrait"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER" /> - <category android:name="android.intent.category.IMF_TEST" /> - </intent-filter> - </activity> - - </application> - -</manifest> diff --git a/tests/ImfTest/res/layout/full_screen_edit_text.xml b/tests/ImfTest/res/layout/full_screen_edit_text.xml deleted file mode 100644 index e760ac181245..000000000000 --- a/tests/ImfTest/res/layout/full_screen_edit_text.xml +++ /dev/null @@ -1,27 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/* //device/samples/SampleCode/res/layout/baseline_1.xml -** -** Copyright 2009, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at -** -** http://www.apache.org/licenses/LICENSE-2.0 -** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and -** limitations under the License. -*/ ---> - -<EditText xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/data" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:minLines="15" - android:gravity="top"/> - diff --git a/tests/ImfTest/res/layout/one_edit_text_activity.xml b/tests/ImfTest/res/layout/one_edit_text_activity.xml deleted file mode 100644 index 055822823bf5..000000000000 --- a/tests/ImfTest/res/layout/one_edit_text_activity.xml +++ /dev/null @@ -1,50 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/* //device/samples/SampleCode/res/layout/baseline_1.xml -** -** Copyright 2007, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at -** -** http://www.apache.org/licenses/LICENSE-2.0 -** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and -** limitations under the License. -*/ ---> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="vertical" -> - <LinearLayout - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical" - android:baselineAligned="false"> - - <View android:id="@+id/blank" - android:layout_height="0dip" - android:layout_width="match_parent" - android:layout_weight="1" - /> - - <EditText android:id="@+id/dialog_edit_text" - android:layout_height="wrap_content" - android:layout_width="match_parent" - android:scrollHorizontally="true" - android:textAppearance="?android:attr/textAppearanceMedium" - /> - </LinearLayout> - - <View - android:layout_width="match_parent" - android:layout_height="1dip" - android:background="@android:drawable/divider_horizontal_dark" - /> -</LinearLayout> diff --git a/tests/ImfTest/res/layout/sample_edit_text.xml b/tests/ImfTest/res/layout/sample_edit_text.xml deleted file mode 100644 index 3ff676727662..000000000000 --- a/tests/ImfTest/res/layout/sample_edit_text.xml +++ /dev/null @@ -1,53 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/* //device/samples/SampleCode/res/layout/baseline_1.xml -** -** Copyright 2007, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at -** -** http://www.apache.org/licenses/LICENSE-2.0 -** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and -** limitations under the License. -*/ ---> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="vertical" -> - <LinearLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:minHeight="?android:attr/listPreferredItemHeight" - android:orientation="horizontal" - android:baselineAligned="false" - android:gravity="center_vertical" - > - - <TextView android:id="@+id/label" - android:layout_width="100dip" - android:layout_height="wrap_content" - android:gravity="right|center_vertical" - /> - - <EditText android:id="@+id/data" - android:layout_width="0dip" - android:layout_weight="1" - android:layout_height="wrap_content" - android:layout_marginLeft="8dip" - /> - </LinearLayout> - - <View - android:layout_width="match_parent" - android:layout_height="1dip" - android:background="@android:drawable/divider_horizontal_dark" - /> -</LinearLayout> diff --git a/tests/ImfTest/res/values/strings.xml b/tests/ImfTest/res/values/strings.xml deleted file mode 100644 index fc87480788d6..000000000000 --- a/tests/ImfTest/res/values/strings.xml +++ /dev/null @@ -1,50 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/* -** -** Copyright 2008, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at -** -** http://www.apache.org/licenses/LICENSE-2.0 -** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and -** limitations under the License. -*/ ---> -<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <!-- Strings for sample activities --> - <string name="normal_edit_text_label">Normal</string> - <string name="uri_edit_text_label">Uri</string> - <string name="email_address_edit_text_label">Email Address</string> - <string name="email_subject_edit_text_label">Email Subject</string> - <string name="email_content_edit_text_label">Email Content</string> - <string name="person_name_edit_text_label">Person Name</string> - <string name="postal_address_edit_text_label">Postal Address</string> - <string name="password_edit_text_label">Password</string> - <string name="search_string_edit_text_label">Search String</string> - <string name="web_edit_text_label">Web Edit Text</string> - <string name="signed_number_edit_text_label">Signed Number</string> - <string name="decimal_number_edit_text_label">Decimal Number</string> - <string name="phone_number_edit_text_label">Phone Number</string> - <string name="normal_datetime_edit_text_label">Datetime</string> - <string name="date_edit_text_label">Date</string> - <string name="time_edit_text_label">Time</string> - <string name="cap_chars_edit_text_label">Cap Chars</string> - <string name="cap_words_edit_text_label">Cap Words</string> - <string name="multiline_edit_text_label">Multiline</string> - <string name="search_edit_text_label">Search (flag)</string> - <string name="cap_sentences_edit_text_label">Cap Sentences</string> - <string name="auto_complete_edit_text_label">Auto Complete</string> - <string name="auto_correct_edit_text_label">Auto Correct</string> - <string name="test_dialog">Test Dialog</string> - <string name="open_dialog_scrollable">open scrollable dialog</string> - <string name="open_dialog_nonscrollable">open nonscrollable dialog</string> - - -</resources> diff --git a/tests/ImfTest/src/com/android/imftest/samples/AutoCompleteTextViewActivityLandscape.java b/tests/ImfTest/src/com/android/imftest/samples/AutoCompleteTextViewActivityLandscape.java deleted file mode 100644 index 6115fd5a0fd0..000000000000 --- a/tests/ImfTest/src/com/android/imftest/samples/AutoCompleteTextViewActivityLandscape.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import android.app.Activity; -import android.os.Bundle; -import android.os.RemoteException; -import android.provider.MediaStore; -import android.view.KeyEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.Window; -import android.view.WindowManager; -import android.widget.LinearLayout; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; -import android.widget.TextView; -import android.widget.AutoCompleteTextView; -import android.widget.ArrayAdapter; -import android.content.Intent; -import android.content.pm.ActivityInfo; - -import com.android.internal.R; - -/* - * Activity with AutoCompleteTextView forced to landscape mode - */ -public class AutoCompleteTextViewActivityLandscape extends Activity -{ - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - - setContentView(R.layout.auto_complete_list); - - ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, - android.R.layout.simple_dropdown_item_1line, COUNTRIES); - AutoCompleteTextView textView = findViewById(R.id.edit); - textView.setAdapter(adapter); - } - - static final String[] COUNTRIES = new String[] { - "Afghanistan", "Albania", "Algeria", "American Samoa", "Andorra", - "Angola", "Anguilla", "Antarctica", "Antigua and Barbuda", "Argentina", - "Armenia", "Aruba", "Australia", "Austria", "Azerbaijan", - "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", - "Belize", "Benin", "Bermuda", "Bhutan", "Bolivia", - "Bosnia and Herzegovina", "Botswana", "Bouvet Island", "Brazil", "British Indian Ocean Territory", - "British Virgin Islands", "Brunei", "Bulgaria", "Burkina Faso", "Burundi", - "Cote d'Ivoire", "Cambodia", "Cameroon", "Canada", "Cape Verde", - "Cayman Islands", "Central African Republic", "Chad", "Chile", "China", - "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros", "Congo", - "Cook Islands", "Costa Rica", "Croatia", "Cuba", "Cyprus", "Czechia", - "Democratic Republic of the Congo", "Denmark", "Djibouti", "Dominica", "Dominican Republic", - "East Timor", "Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Eritrea", - "Estonia", "Ethiopia", "Faeroe Islands", "Falkland Islands", "Fiji", "Finland", - "Former Yugoslav Republic of Macedonia", "France", "French Guiana", "French Polynesia", - "French Southern Territories", "Gabon", "Georgia", "Germany", "Ghana", "Gibraltar", - "Greece", "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guinea", "Guinea-Bissau", - "Guyana", "Haiti", "Heard Island and McDonald Islands", "Honduras", "Hong Kong", "Hungary", - "Iceland", "India", "Indonesia", "Iran", "Iraq", "Ireland", "Israel", "Italy", "Jamaica", - "Japan", "Jordan", "Kazakhstan", "Kenya", "Kiribati", "Kuwait", "Kyrgyzstan", "Laos", - "Latvia", "Lebanon", "Lesotho", "Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg", - "Macau", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", - "Martinique", "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia", "Moldova", - "Monaco", "Mongolia", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", - "Nauru", "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", - "Nicaragua", "Niger", "Nigeria", "Niue", "Norfolk Island", "North Korea", "Northern Marianas", - "Norway", "Oman", "Pakistan", "Palau", "Panama", "Papua New Guinea", "Paraguay", "Peru", - "Philippines", "Pitcairn Islands", "Poland", "Portugal", "Puerto Rico", "Qatar", - "Reunion", "Romania", "Russia", "Rwanda", "Sqo Tome and Principe", "Saint Helena", - "Saint Kitts and Nevis", "Saint Lucia", "Saint Pierre and Miquelon", - "Saint Vincent and the Grenadines", "Samoa", "San Marino", "Saudi Arabia", "Senegal", - "Seychelles", "Sierra Leone", "Singapore", "Slovakia", "Slovenia", "Solomon Islands", - "Somalia", "South Africa", "South Georgia and the South Sandwich Islands", "South Korea", - "Spain", "Sri Lanka", "Sudan", "Suriname", "Svalbard and Jan Mayen", "Swaziland", "Sweden", - "Switzerland", "Syria", "Taiwan", "Tajikistan", "Tanzania", "Thailand", "The Bahamas", - "The Gambia", "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", - "Turkmenistan", "Turks and Caicos Islands", "Tuvalu", "Virgin Islands", "Uganda", - "Ukraine", "United Arab Emirates", "United Kingdom", - "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", - "Vanuatu", "Vatican City", "Venezuela", "Vietnam", "Wallis and Futuna", "Western Sahara", - "Yemen", "Yugoslavia", "Zambia", "Zimbabwe" - }; -} diff --git a/tests/ImfTest/src/com/android/imftest/samples/AutoCompleteTextViewActivityPortrait.java b/tests/ImfTest/src/com/android/imftest/samples/AutoCompleteTextViewActivityPortrait.java deleted file mode 100644 index 253c50fe5810..000000000000 --- a/tests/ImfTest/src/com/android/imftest/samples/AutoCompleteTextViewActivityPortrait.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import android.app.Activity; -import android.os.Bundle; -import android.view.KeyEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.LinearLayout; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; -import android.widget.TextView; -import android.widget.AutoCompleteTextView; -import android.widget.ArrayAdapter; - -import com.android.internal.R; - -/* - * Activity with AutoCompleteTextView (Candidate bar should not appear) - */ -public class AutoCompleteTextViewActivityPortrait extends Activity -{ - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - - setContentView(R.layout.auto_complete_list); - - ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, - android.R.layout.simple_dropdown_item_1line, COUNTRIES); - AutoCompleteTextView textView = findViewById(R.id.edit); - textView.setAdapter(adapter); - } - - static final String[] COUNTRIES = new String[] { - "Afghanistan", "Albania", "Algeria", "American Samoa", "Andorra", - "Angola", "Anguilla", "Antarctica", "Antigua and Barbuda", "Argentina", - "Armenia", "Aruba", "Australia", "Austria", "Azerbaijan", - "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", - "Belize", "Benin", "Bermuda", "Bhutan", "Bolivia", - "Bosnia and Herzegovina", "Botswana", "Bouvet Island", "Brazil", "British Indian Ocean Territory", - "British Virgin Islands", "Brunei", "Bulgaria", "Burkina Faso", "Burundi", - "Cote d'Ivoire", "Cambodia", "Cameroon", "Canada", "Cape Verde", - "Cayman Islands", "Central African Republic", "Chad", "Chile", "China", - "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros", "Congo", - "Cook Islands", "Costa Rica", "Croatia", "Cuba", "Cyprus", "Czechia", - "Democratic Republic of the Congo", "Denmark", "Djibouti", "Dominica", "Dominican Republic", - "East Timor", "Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Eritrea", - "Estonia", "Ethiopia", "Faeroe Islands", "Falkland Islands", "Fiji", "Finland", - "Former Yugoslav Republic of Macedonia", "France", "French Guiana", "French Polynesia", - "French Southern Territories", "Gabon", "Georgia", "Germany", "Ghana", "Gibraltar", - "Greece", "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guinea", "Guinea-Bissau", - "Guyana", "Haiti", "Heard Island and McDonald Islands", "Honduras", "Hong Kong", "Hungary", - "Iceland", "India", "Indonesia", "Iran", "Iraq", "Ireland", "Israel", "Italy", "Jamaica", - "Japan", "Jordan", "Kazakhstan", "Kenya", "Kiribati", "Kuwait", "Kyrgyzstan", "Laos", - "Latvia", "Lebanon", "Lesotho", "Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg", - "Macau", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", - "Martinique", "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia", "Moldova", - "Monaco", "Mongolia", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", - "Nauru", "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", - "Nicaragua", "Niger", "Nigeria", "Niue", "Norfolk Island", "North Korea", "Northern Marianas", - "Norway", "Oman", "Pakistan", "Palau", "Panama", "Papua New Guinea", "Paraguay", "Peru", - "Philippines", "Pitcairn Islands", "Poland", "Portugal", "Puerto Rico", "Qatar", - "Reunion", "Romania", "Russia", "Rwanda", "Sqo Tome and Principe", "Saint Helena", - "Saint Kitts and Nevis", "Saint Lucia", "Saint Pierre and Miquelon", - "Saint Vincent and the Grenadines", "Samoa", "San Marino", "Saudi Arabia", "Senegal", - "Seychelles", "Sierra Leone", "Singapore", "Slovakia", "Slovenia", "Solomon Islands", - "Somalia", "South Africa", "South Georgia and the South Sandwich Islands", "South Korea", - "Spain", "Sri Lanka", "Sudan", "Suriname", "Svalbard and Jan Mayen", "Swaziland", "Sweden", - "Switzerland", "Syria", "Taiwan", "Tajikistan", "Tanzania", "Thailand", "The Bahamas", - "The Gambia", "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", - "Turkmenistan", "Turks and Caicos Islands", "Tuvalu", "Virgin Islands", "Uganda", - "Ukraine", "United Arab Emirates", "United Kingdom", - "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", - "Vanuatu", "Vatican City", "Venezuela", "Vietnam", "Wallis and Futuna", "Western Sahara", - "Yemen", "Yugoslavia", "Zambia", "Zimbabwe" - }; -} diff --git a/tests/ImfTest/src/com/android/imftest/samples/BigEditTextActivityNonScrollablePanScan.java b/tests/ImfTest/src/com/android/imftest/samples/BigEditTextActivityNonScrollablePanScan.java deleted file mode 100644 index 033082fbab42..000000000000 --- a/tests/ImfTest/src/com/android/imftest/samples/BigEditTextActivityNonScrollablePanScan.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import com.android.imftest.R; - -import android.app.Activity; -import android.os.Bundle; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.LinearLayout; - -public class BigEditTextActivityNonScrollablePanScan extends Activity { - - private View mRootView; - private View mDefaultFocusedView; - - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - - getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); - - mRootView = new LinearLayout(this); - ((LinearLayout) mRootView).setOrientation(LinearLayout.VERTICAL); - mRootView.setLayoutParams(new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT)); - - View view = getLayoutInflater().inflate( - R.layout.full_screen_edit_text, ((LinearLayout) mRootView), false); - - ((LinearLayout) mRootView).addView(view); - - mDefaultFocusedView = view.findViewById(R.id.data); - - setContentView(mRootView); - } - - public View getRootView() { - return mRootView; - } - - public View getDefaultFocusedView() { - return mDefaultFocusedView; - } - -} diff --git a/tests/ImfTest/src/com/android/imftest/samples/BigEditTextActivityNonScrollableResize.java b/tests/ImfTest/src/com/android/imftest/samples/BigEditTextActivityNonScrollableResize.java deleted file mode 100644 index 8a16deab9a40..000000000000 --- a/tests/ImfTest/src/com/android/imftest/samples/BigEditTextActivityNonScrollableResize.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import com.android.imftest.R; - -import android.app.Activity; -import android.os.Bundle; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.LinearLayout; - -public class BigEditTextActivityNonScrollableResize extends Activity { - - private View mRootView; - private View mDefaultFocusedView; - - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - - getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); - - mRootView = new LinearLayout(this); - ((LinearLayout) mRootView).setOrientation(LinearLayout.VERTICAL); - mRootView.setLayoutParams(new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT)); - - View view = getLayoutInflater().inflate( - R.layout.full_screen_edit_text, ((LinearLayout) mRootView), false); - - ((LinearLayout) mRootView).addView(view); - - mDefaultFocusedView = view.findViewById(R.id.data); - - setContentView(mRootView); - } - - public View getRootView() { - return mRootView; - } - - public View getDefaultFocusedView() { - return mDefaultFocusedView; - } - -} diff --git a/tests/ImfTest/src/com/android/imftest/samples/BigEditTextActivityScrollablePanScan.java b/tests/ImfTest/src/com/android/imftest/samples/BigEditTextActivityScrollablePanScan.java deleted file mode 100644 index b4fdc4c07584..000000000000 --- a/tests/ImfTest/src/com/android/imftest/samples/BigEditTextActivityScrollablePanScan.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import com.android.imftest.R; - -import android.app.Activity; -import android.os.Bundle; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.LinearLayout; -import android.widget.ScrollView; - -public class BigEditTextActivityScrollablePanScan extends Activity { - - private View mRootView; - private View mDefaultFocusedView; - private LinearLayout mLayout; - - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - - getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); - - mRootView = new ScrollView(this); - ((ScrollView) mRootView).setFillViewport(true); - mRootView.setLayoutParams(new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT)); - - mLayout = new LinearLayout(this); - mLayout.setOrientation(LinearLayout.VERTICAL); - mLayout.setLayoutParams(new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT)); - - View view = getLayoutInflater().inflate( - R.layout.full_screen_edit_text, ((ScrollView) mRootView), false); - - mLayout.addView(view); - - ((ScrollView) mRootView).addView(mLayout); - mDefaultFocusedView = view.findViewById(R.id.data); - - setContentView(mRootView); - } - - public View getRootView() { - return mRootView; - } - - public View getDefaultFocusedView() { - return mDefaultFocusedView; - } - -} diff --git a/tests/ImfTest/src/com/android/imftest/samples/BigEditTextActivityScrollableResize.java b/tests/ImfTest/src/com/android/imftest/samples/BigEditTextActivityScrollableResize.java deleted file mode 100644 index 757b6b5d57ce..000000000000 --- a/tests/ImfTest/src/com/android/imftest/samples/BigEditTextActivityScrollableResize.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import com.android.imftest.R; - -import android.app.Activity; -import android.os.Bundle; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.LinearLayout; -import android.widget.ScrollView; - -public class BigEditTextActivityScrollableResize extends Activity { - - private View mRootView; - private View mDefaultFocusedView; - private LinearLayout mLayout; - - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - - getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); - - mRootView = new ScrollView(this); - ((ScrollView) mRootView).setFillViewport(true); - mRootView.setLayoutParams(new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT)); - - mLayout = new LinearLayout(this); - mLayout.setOrientation(LinearLayout.VERTICAL); - mLayout.setLayoutParams(new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT)); - - View view = getLayoutInflater().inflate( - R.layout.full_screen_edit_text, ((ScrollView) mRootView), false); - - mLayout.addView(view); - - ((ScrollView) mRootView).addView(mLayout); - mDefaultFocusedView = view.findViewById(R.id.data); - - setContentView(mRootView); - } - - public View getRootView() { - return mRootView; - } - - public View getDefaultFocusedView() { - return mDefaultFocusedView; - } - -} diff --git a/tests/ImfTest/src/com/android/imftest/samples/BottomEditTextActivityPanScan.java b/tests/ImfTest/src/com/android/imftest/samples/BottomEditTextActivityPanScan.java deleted file mode 100644 index 91a329d25f90..000000000000 --- a/tests/ImfTest/src/com/android/imftest/samples/BottomEditTextActivityPanScan.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import android.app.Activity; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.WindowManager; -import android.widget.LinearLayout; -import android.widget.EditText; -import android.widget.ScrollView; -import android.widget.TextView; - -import com.android.imftest.R; - -/* - * Activity with EditText at the bottom (Pan&Scan) - */ -public class BottomEditTextActivityPanScan extends Activity -{ - private View mRootView; - private View mDefaultFocusedView; - - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - - mRootView = new LinearLayout(this); - ((LinearLayout) mRootView).setOrientation(LinearLayout.VERTICAL); - - View view = getLayoutInflater().inflate(R.layout.one_edit_text_activity, ((LinearLayout) mRootView), false); - mDefaultFocusedView = view.findViewById(R.id.dialog_edit_text); - ((LinearLayout) mRootView).addView(view); - - setContentView(mRootView); - this.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); - } - - public View getRootView() { - return mRootView; - } - - public View getDefaultFocusedView() { - return mDefaultFocusedView; - } -} diff --git a/tests/ImfTest/src/com/android/imftest/samples/BottomEditTextActivityResize.java b/tests/ImfTest/src/com/android/imftest/samples/BottomEditTextActivityResize.java deleted file mode 100644 index c4c41bc7d4ee..000000000000 --- a/tests/ImfTest/src/com/android/imftest/samples/BottomEditTextActivityResize.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import android.app.Activity; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.WindowManager; -import android.widget.LinearLayout; -import android.widget.EditText; -import android.widget.ScrollView; -import android.widget.TextView; - -import com.android.imftest.R; - -/* - * Activity with EditText at the bottom (Resize) - */ -public class BottomEditTextActivityResize extends Activity -{ - private View mRootView; - private View mDefaultFocusedView; - - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - - mRootView = new LinearLayout(this); - ((LinearLayout) mRootView).setOrientation(LinearLayout.VERTICAL); - - View view = getLayoutInflater().inflate(R.layout.one_edit_text_activity, ((LinearLayout) mRootView), false); - mDefaultFocusedView = view.findViewById(R.id.dialog_edit_text); - ((LinearLayout) mRootView).addView(view); - - setContentView(mRootView); - this.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); - } - - public View getRootView() { - return mRootView; - } - - public View getDefaultFocusedView() { - return mDefaultFocusedView; - } -} diff --git a/tests/ImfTest/src/com/android/imftest/samples/ButtonActivity.java b/tests/ImfTest/src/com/android/imftest/samples/ButtonActivity.java deleted file mode 100644 index dbaedf9f0af1..000000000000 --- a/tests/ImfTest/src/com/android/imftest/samples/ButtonActivity.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import android.app.Activity; -import android.os.Bundle; -import android.view.KeyEvent; -import android.view.View; -import android.widget.LinearLayout; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; -import android.widget.Button; -import android.widget.TextView; - -public class ButtonActivity extends Activity -{ - static boolean mKeyboardIsActive = false; - public static final int BUTTON_ID = 0; - private View mRootView; - - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - final ButtonActivity instance = this; - - final Button myButton = new Button(this); - myButton.setClickable(true); - myButton.setText("Keyboard UP!"); - myButton.setId(BUTTON_ID); - myButton.setFocusableInTouchMode(true); - myButton.setOnClickListener(new View.OnClickListener() - { - public void onClick (View v) - { - InputMethodManager imm = InputMethodManager.getInstance(); - if (mKeyboardIsActive) - { - imm.hideSoftInputFromInputMethod(v.getWindowToken(), 0); - myButton.setText("Keyboard UP!"); - - } - else - { - myButton.requestFocusFromTouch(); - imm.showSoftInput(v, 0); - myButton.setText("Keyboard DOWN!"); - } - - mKeyboardIsActive = !mKeyboardIsActive; - } - }); - - LinearLayout layout = new LinearLayout(this); - layout.setOrientation(LinearLayout.VERTICAL); - layout.addView(myButton); - setContentView(layout); - mRootView = layout; - } - - public View getRootView() { - return mRootView; - } -} diff --git a/tests/ImfTest/src/com/android/imftest/samples/DialogActivity.java b/tests/ImfTest/src/com/android/imftest/samples/DialogActivity.java deleted file mode 100644 index 3ed03862a04f..000000000000 --- a/tests/ImfTest/src/com/android/imftest/samples/DialogActivity.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import android.app.Activity; -import android.os.Bundle; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; -import android.widget.EditText; -import android.widget.Button; -import android.view.LayoutInflater; -import android.app.Dialog; - -public class DialogActivity extends Activity { - - private static final int DIALOG_WITHOUT_EDITTEXT = 0; - private static final int DIALOG_WITH_EDITTEXT = 1; - - private LinearLayout mLayout; - private LayoutInflater mInflater; - private Button mButton1; - private Button mButton2; - private EditText mEditText; - - - @Override - protected void onCreate(Bundle icicle) - { - super.onCreate(icicle); - - mLayout = new LinearLayout(this); - mLayout.setOrientation(LinearLayout.VERTICAL); - mLayout.setLayoutParams(new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT)); - - mButton1 = new Button(this); - mButton1.setText("Dialog WITHOUT EditText");//(R.string.open_dialog_scrollable); - mButton1.setOnClickListener(new View.OnClickListener() - { - public void onClick(View v) - { - showDialog(DIALOG_WITHOUT_EDITTEXT); - } - }); - - mButton2 = new Button(this); - mButton2.setText("Dialog WITH EditText");//(R.string.open_dialog_nonscrollable); - mButton2.setOnClickListener(new View.OnClickListener() - { - public void onClick(View v) - { - showDialog(DIALOG_WITH_EDITTEXT); - } - }); - - mEditText = new EditText(this); - mLayout.addView(mEditText); - mLayout.addView(mButton1); - mLayout.addView(mButton2); - - setContentView(mLayout); - } - - @Override - protected Dialog onCreateDialog(int id) - { - switch (id) - { - case DIALOG_WITHOUT_EDITTEXT: - return createDialog(false); - case DIALOG_WITH_EDITTEXT: - return createDialog(true); - } - - return super.onCreateDialog(id); - } - - protected Dialog createDialog(boolean bEditText) - { - LinearLayout layout; - layout = new LinearLayout(this); - layout.setOrientation(LinearLayout.VERTICAL); - - if(bEditText) - { - EditText editText; - editText = new EditText(this); - layout.addView(editText); - } - - Dialog d = new Dialog(this); - d.setTitle("The DIALOG!!!"); - d.setCancelable(true); - d.setContentView(layout); - return d; - } - - } diff --git a/tests/ImfTest/src/com/android/imftest/samples/EditTextActivityDialog.java b/tests/ImfTest/src/com/android/imftest/samples/EditTextActivityDialog.java deleted file mode 100644 index 2591b7c96ae9..000000000000 --- a/tests/ImfTest/src/com/android/imftest/samples/EditTextActivityDialog.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import com.android.imftest.R; - -import android.app.Activity; -import android.app.Dialog; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.ScrollView; - -public class EditTextActivityDialog extends Activity { - - private static final int SCROLLABLE_DIALOG_ID = 0; - private static final int NONSCROLLABLE_DIALOG_ID = 1; - - private LinearLayout mLayout; - private ScrollView mScrollView; - private LayoutInflater mInflater; - private Button mButton1; - private Button mButton2; - - - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - - mLayout = new LinearLayout(this); - mLayout.setOrientation(LinearLayout.VERTICAL); - mLayout.setLayoutParams(new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT)); - - mButton1 = new Button(this); - mButton1.setText(R.string.open_dialog_scrollable); - mButton1.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - showDialog(SCROLLABLE_DIALOG_ID); - } - }); - - mButton2 = new Button(this); - mButton2.setText(R.string.open_dialog_nonscrollable); - mButton2.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - showDialog(NONSCROLLABLE_DIALOG_ID); - } - }); - - mLayout.addView(mButton1); - mLayout.addView(mButton2); - - setContentView(mLayout); - } - - @Override - protected Dialog onCreateDialog(int id) { - switch (id) { - case SCROLLABLE_DIALOG_ID: - return createDialog(true); - case NONSCROLLABLE_DIALOG_ID: - return createDialog(false); - } - - return super.onCreateDialog(id); - } - - protected Dialog createDialog(boolean scrollable) { - View layout; - EditText editText; - - if (scrollable) { - layout = new ScrollView(EditTextActivityDialog.this); - ((ScrollView) layout).setMinimumHeight(mLayout.getHeight()); - - ((ScrollView) layout).addView(( - LinearLayout) View.inflate(EditTextActivityDialog.this, - R.layout.dialog_edit_text_no_scroll, null)); - } else { - layout = View.inflate(EditTextActivityDialog.this, - R.layout.dialog_edit_text_no_scroll, null); - } - - Dialog d = new Dialog(EditTextActivityDialog.this); - d.setTitle(getString(R.string.test_dialog)); - d.setCancelable(true); - d.setContentView(layout); - return d; - } - -} diff --git a/tests/ImfTest/src/com/android/imftest/samples/InputTypeActivity.java b/tests/ImfTest/src/com/android/imftest/samples/InputTypeActivity.java deleted file mode 100644 index 299e6bb2778e..000000000000 --- a/tests/ImfTest/src/com/android/imftest/samples/InputTypeActivity.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import com.android.imftest.R; - -import android.app.Activity; -import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.ScrollView; -import android.widget.TextView; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.EditorInfo; - -public class InputTypeActivity extends Activity { - - private LinearLayout mLayout; - private ScrollView mScrollView; - private LayoutInflater mInflater; - private ViewGroup mParent; - - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - - mScrollView = new ScrollView(this); - - mLayout = new LinearLayout(this); - mLayout.setOrientation(LinearLayout.VERTICAL); - mLayout.setLayoutParams(new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT)); - - mInflater = getLayoutInflater(); - mParent = mLayout; - - /* Normal Edit Text */ - mLayout.addView(buildEntryView(EditorInfo.TYPE_CLASS_TEXT|EditorInfo.TYPE_TEXT_VARIATION_NORMAL, - R.string.normal_edit_text_label)); - - /* Normal Edit Text w/Cap Chars Flag*/ - mLayout.addView(buildEntryView(EditorInfo.TYPE_CLASS_TEXT|EditorInfo.TYPE_TEXT_VARIATION_NORMAL|EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS, - R.string.cap_chars_edit_text_label)); - - /* Normal Edit Text w/Cap Words Flag*/ - mLayout.addView(buildEntryView(EditorInfo.TYPE_CLASS_TEXT|EditorInfo.TYPE_TEXT_VARIATION_NORMAL|EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS, - R.string.cap_words_edit_text_label)); - - /* Normal Edit Text w/Cap Multiline Flag */ - mLayout.addView(buildEntryView(EditorInfo.TYPE_CLASS_TEXT|EditorInfo.TYPE_TEXT_VARIATION_NORMAL|EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE, - R.string.multiline_edit_text_label)); - - /* Normal Edit Text w/Cap Sentences Flag */ - mLayout.addView(buildEntryView(EditorInfo.TYPE_CLASS_TEXT|EditorInfo.TYPE_TEXT_VARIATION_NORMAL|EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES, - R.string.cap_sentences_edit_text_label)); - - /* Normal Edit Text w/Auto-complete Flag */ - mLayout.addView(buildEntryView(EditorInfo.TYPE_CLASS_TEXT|EditorInfo.TYPE_TEXT_VARIATION_NORMAL|EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE, - R.string.auto_complete_edit_text_label)); - - /* Normal Edit Text w/Auto-correct Flag */ - mLayout.addView(buildEntryView(EditorInfo.TYPE_CLASS_TEXT|EditorInfo.TYPE_TEXT_VARIATION_NORMAL|EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT, - R.string.auto_correct_edit_text_label)); - - /* Uri Edit Text */ - mLayout.addView(buildEntryView(EditorInfo.TYPE_CLASS_TEXT|EditorInfo.TYPE_TEXT_VARIATION_URI, - R.string.uri_edit_text_label)); - - /* Email Address Edit Text */ - mLayout.addView(buildEntryView(EditorInfo.TYPE_CLASS_TEXT|EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS, - R.string.email_address_edit_text_label)); - - /* Email Subject Text */ - mLayout.addView(buildEntryView(EditorInfo.TYPE_CLASS_TEXT|EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT, - R.string.email_subject_edit_text_label)); - - /* Email Content Edit Text */ - mLayout.addView(buildEntryView(EditorInfo.TYPE_CLASS_TEXT|EditorInfo.TYPE_TEXT_VARIATION_LONG_MESSAGE, - R.string.email_content_edit_text_label)); - - /* Person Name Edit Text */ - mLayout.addView(buildEntryView(EditorInfo.TYPE_CLASS_TEXT|EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME, - R.string.person_name_edit_text_label)); - - /* Postal Address Edit Text */ - mLayout.addView(buildEntryView(EditorInfo.TYPE_CLASS_TEXT|EditorInfo.TYPE_TEXT_VARIATION_POSTAL_ADDRESS, - R.string.postal_address_edit_text_label)); - - /* Password Edit Text */ - mLayout.addView(buildEntryView(EditorInfo.TYPE_CLASS_TEXT|EditorInfo.TYPE_TEXT_VARIATION_PASSWORD, - R.string.password_edit_text_label)); - - /* Web Edit Text */ - mLayout.addView(buildEntryView(EditorInfo.TYPE_CLASS_TEXT|EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT, - R.string.web_edit_text_label)); - - /* Signed Number Edit Text */ - mLayout.addView(buildEntryView(EditorInfo.TYPE_CLASS_NUMBER|EditorInfo.TYPE_NUMBER_FLAG_SIGNED, - R.string.signed_number_edit_text_label)); - - /* Decimal Number Edit Text */ - mLayout.addView(buildEntryView(EditorInfo.TYPE_CLASS_NUMBER|EditorInfo.TYPE_NUMBER_FLAG_DECIMAL, - R.string.decimal_number_edit_text_label)); - - /* Phone Number Edit Text */ - mLayout.addView(buildEntryView(EditorInfo.TYPE_CLASS_PHONE, - R.string.phone_number_edit_text_label)); - - /* Normal Datetime Edit Text */ - mLayout.addView(buildEntryView(EditorInfo.TYPE_CLASS_DATETIME|EditorInfo.TYPE_DATETIME_VARIATION_NORMAL, - R.string.normal_datetime_edit_text_label)); - - /* Date Edit Text */ - mLayout.addView(buildEntryView(EditorInfo.TYPE_CLASS_DATETIME|EditorInfo.TYPE_DATETIME_VARIATION_DATE, - R.string.date_edit_text_label)); - - /* Time Edit Text */ - mLayout.addView(buildEntryView(EditorInfo.TYPE_CLASS_DATETIME|EditorInfo.TYPE_DATETIME_VARIATION_TIME, - R.string.time_edit_text_label)); - - mScrollView.addView(mLayout); - setContentView(mScrollView); - } - - private View buildEntryView(int inputType, int label) { - - - View view = mInflater.inflate(R.layout.sample_edit_text, mParent, false); - - EditText editText = (EditText) view.findViewById(R.id.data); - editText.setInputType(inputType); - - TextView textView = (TextView) view.findViewById(R.id.label); - textView.setText(label); - - return view; - } - -} diff --git a/tests/ImfTest/src/com/android/imftest/samples/ManyEditTextActivityNoScrollPanScan.java b/tests/ImfTest/src/com/android/imftest/samples/ManyEditTextActivityNoScrollPanScan.java deleted file mode 100644 index 646e4805f32e..000000000000 --- a/tests/ImfTest/src/com/android/imftest/samples/ManyEditTextActivityNoScrollPanScan.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import android.app.Activity; -import android.os.Bundle; -import android.view.KeyEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.LinearLayout; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; -import android.widget.Button; -import android.widget.TextView; -import android.widget.ScrollView; - -import com.android.internal.R; - -/* - * Full screen of EditTexts (Non-Scrollable, Pan&Scan) - */ -public class ManyEditTextActivityNoScrollPanScan extends Activity -{ - public static final int NUM_EDIT_TEXTS = 9; - - private View mRootView; - - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - - mRootView = new LinearLayout(this); - ((LinearLayout) mRootView).setOrientation(LinearLayout.VERTICAL); - - for (int i=0; i<NUM_EDIT_TEXTS; i++) - { - final EditText editText = new EditText(this); - editText.setText(String.valueOf(i)); - editText.setId(i); - ((LinearLayout) mRootView).addView(editText); - } - setContentView(mRootView); - this.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); - } - - public View getRootView() { - return mRootView; - } - -} diff --git a/tests/ImfTest/src/com/android/imftest/samples/ManyEditTextActivityScrollPanScan.java b/tests/ImfTest/src/com/android/imftest/samples/ManyEditTextActivityScrollPanScan.java deleted file mode 100644 index 0387e1ec5d13..000000000000 --- a/tests/ImfTest/src/com/android/imftest/samples/ManyEditTextActivityScrollPanScan.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import android.app.Activity; -import android.os.Bundle; -import android.view.KeyEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.LinearLayout; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; -import android.widget.Button; -import android.widget.TextView; -import android.widget.ScrollView; - -import com.android.internal.R; - -/* - * Full screen of EditTexts (Scrollable, Pan&Scan) - */ -public class ManyEditTextActivityScrollPanScan extends Activity -{ - public static final int NUM_EDIT_TEXTS = 12; - - private View mRootView; - - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - mRootView = new ScrollView(this); - - LinearLayout layout = new LinearLayout(this); - layout.setOrientation(LinearLayout.VERTICAL); - - for (int i=0; i<NUM_EDIT_TEXTS; i++) - { - final EditText editText = new EditText(this); - editText.setText(String.valueOf(i)); - editText.setId(i); - layout.addView(editText); - } - - ((ScrollView) mRootView).addView(layout); - setContentView(mRootView); - this.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); - } - - public View getRootView() { - return mRootView; - } -} diff --git a/tests/ImfTest/src/com/android/imftest/samples/ManyEditTextActivityScrollResize.java b/tests/ImfTest/src/com/android/imftest/samples/ManyEditTextActivityScrollResize.java deleted file mode 100644 index 7793b553d746..000000000000 --- a/tests/ImfTest/src/com/android/imftest/samples/ManyEditTextActivityScrollResize.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import android.app.Activity; -import android.os.Bundle; -import android.view.View; -import android.view.WindowManager; -import android.widget.LinearLayout; -import android.widget.EditText; -import android.widget.ScrollView; - -/* - * Full screen of EditTexts (Scrollable, Resize) - */ -public class ManyEditTextActivityScrollResize extends Activity -{ - public static final int NUM_EDIT_TEXTS = 12; - - private View mRootView; - - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - mRootView = new ScrollView(this); - - LinearLayout layout = new LinearLayout(this); - layout.setOrientation(LinearLayout.VERTICAL); - - for (int i=0; i<NUM_EDIT_TEXTS; i++) - { - final EditText editText = new EditText(this); - editText.setText(String.valueOf(i)); - editText.setId(i); - layout.addView(editText); - } - - ((ScrollView) mRootView).addView(layout); - setContentView(mRootView); - this.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); - } - - public View getRootView() { - return mRootView; - } -} diff --git a/tests/ImfTest/src/com/android/imftest/samples/OneEditTextActivityNotSelected.java b/tests/ImfTest/src/com/android/imftest/samples/OneEditTextActivityNotSelected.java deleted file mode 100644 index c4be21c7f2d8..000000000000 --- a/tests/ImfTest/src/com/android/imftest/samples/OneEditTextActivityNotSelected.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import android.app.Activity; -import android.os.Bundle; -import android.os.Debug; -import android.view.KeyEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.LinearLayout; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; -import android.widget.Button; -import android.widget.TextView; -import android.widget.ScrollView; - -import com.android.internal.R; - -/* - * Activity with non-EditText view selected initially - */ -public class OneEditTextActivityNotSelected extends Activity -{ - private View mRootView; - private View mDefaultFocusedView; - - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - - LinearLayout layout = new LinearLayout(this); - layout.setOrientation(LinearLayout.VERTICAL); - mRootView = new ScrollView(this); - - EditText editText = new EditText(this); - Button button = new Button(this); - button.setText("The focus is here."); - button.setFocusableInTouchMode(true); - button.requestFocus(); - mDefaultFocusedView = button; - layout.addView(button); - layout.addView(editText); - - ((ScrollView) mRootView).addView(layout); - setContentView(mRootView); - } - - public View getRootView() { - return mRootView; - } - - public View getDefaultFocusedView() { - return mDefaultFocusedView; - } -} diff --git a/tests/ImfTest/src/com/android/imftest/samples/OneEditTextActivitySelected.java b/tests/ImfTest/src/com/android/imftest/samples/OneEditTextActivitySelected.java deleted file mode 100644 index 64882aa30eb1..000000000000 --- a/tests/ImfTest/src/com/android/imftest/samples/OneEditTextActivitySelected.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import android.app.Activity; -import android.os.Bundle; -import android.view.KeyEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.LinearLayout; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; -import android.widget.Button; -import android.widget.TextView; -import android.widget.ScrollView; - -import com.android.internal.R; - -/* - * Activity with EditText selected initially - */ -public class OneEditTextActivitySelected extends Activity -{ - private View mRootView; - private View mDefaultFocusedView; - - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - - LinearLayout layout = new LinearLayout(this); - layout.setOrientation(LinearLayout.VERTICAL); - mRootView = new ScrollView(this); - - EditText editText = new EditText(this); - editText.requestFocus(); - mDefaultFocusedView = editText; - layout.addView(editText); - - ((ScrollView) mRootView).addView(layout); - setContentView(mRootView); - - // set to resize so IME is always shown (and also so - // ImfBaseTestCase#destructiveCheckImeInitialState thinks it should always be shown - this.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); - } - - public View getRootView() { - return mRootView; - } - - public View getDefaultFocusedView() { - return mDefaultFocusedView; - } -} diff --git a/tests/ImfTest/tests/Android.mk b/tests/ImfTest/tests/Android.mk deleted file mode 100644 index 14186d7a5a87..000000000000 --- a/tests/ImfTest/tests/Android.mk +++ /dev/null @@ -1,18 +0,0 @@ -LOCAL_PATH:= $(call my-dir) -include $(CLEAR_VARS) - -# We only want this apk build for tests. -LOCAL_MODULE_TAGS := tests - -# Include all test java files. -LOCAL_SRC_FILES := $(call all-subdir-java-files) - -LOCAL_JAVA_LIBRARIES := android.test.runner android.test.base -LOCAL_STATIC_JAVA_LIBRARIES := junit - -LOCAL_PACKAGE_NAME := ImfTestTests -LOCAL_PRIVATE_PLATFORM_APIS := true - -LOCAL_INSTRUMENTATION_FOR := ImfTest - -include $(BUILD_PACKAGE) diff --git a/tests/ImfTest/tests/src/com/android/imftest/samples/BigEditTextActivityNonScrollablePanScanTests.java b/tests/ImfTest/tests/src/com/android/imftest/samples/BigEditTextActivityNonScrollablePanScanTests.java deleted file mode 100644 index 2db11c56e7e3..000000000000 --- a/tests/ImfTest/tests/src/com/android/imftest/samples/BigEditTextActivityNonScrollablePanScanTests.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import android.test.suitebuilder.annotation.LargeTest; -import android.view.View; - -import com.android.imftest.R; - - -public class BigEditTextActivityNonScrollablePanScanTests extends ImfBaseTestCase<BigEditTextActivityNonScrollablePanScan> { - - public final String TAG = "BigEditTextActivityNonScrollablePanScanTests"; - - public BigEditTextActivityNonScrollablePanScanTests() { - super(BigEditTextActivityNonScrollablePanScan.class); - } - - @LargeTest - public void testAppAdjustmentPanScan() { - // Give the IME 2 seconds to appear. - pause(2000); - - View rootView = ((BigEditTextActivityNonScrollablePanScan) mTargetActivity).getRootView(); - View servedView = ((BigEditTextActivityNonScrollablePanScan) mTargetActivity).getDefaultFocusedView(); - - assertNotNull(rootView); - assertNotNull(servedView); - - destructiveCheckImeInitialState(rootView, servedView); - - verifyEditTextAdjustment(servedView, rootView.getMeasuredHeight()); - } - -} diff --git a/tests/ImfTest/tests/src/com/android/imftest/samples/BigEditTextActivityNonScrollableResizeTests.java b/tests/ImfTest/tests/src/com/android/imftest/samples/BigEditTextActivityNonScrollableResizeTests.java deleted file mode 100644 index 1050794af642..000000000000 --- a/tests/ImfTest/tests/src/com/android/imftest/samples/BigEditTextActivityNonScrollableResizeTests.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import android.test.suitebuilder.annotation.LargeTest; -import android.view.View; - -import com.android.imftest.R; - - -public class BigEditTextActivityNonScrollableResizeTests extends ImfBaseTestCase<BigEditTextActivityNonScrollableResize> { - - public final String TAG = "BigEditTextActivityNonScrollableResizeTests"; - - public BigEditTextActivityNonScrollableResizeTests() { - super(BigEditTextActivityNonScrollableResize.class); - } - - @LargeTest - public void testAppAdjustmentPanScan() { // Give the IME 2 seconds to appear. - pause(2000); - - View rootView = ((BigEditTextActivityNonScrollableResize) mTargetActivity).getRootView(); - View servedView = ((BigEditTextActivityNonScrollableResize) mTargetActivity).getDefaultFocusedView(); - - assertNotNull(rootView); - assertNotNull(servedView); - - destructiveCheckImeInitialState(rootView, servedView); - - verifyEditTextAdjustment(servedView, rootView.getMeasuredHeight()); - } - -} diff --git a/tests/ImfTest/tests/src/com/android/imftest/samples/BigEditTextActivityScrollablePanScanTests.java b/tests/ImfTest/tests/src/com/android/imftest/samples/BigEditTextActivityScrollablePanScanTests.java deleted file mode 100644 index 1e848b051439..000000000000 --- a/tests/ImfTest/tests/src/com/android/imftest/samples/BigEditTextActivityScrollablePanScanTests.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import android.test.suitebuilder.annotation.LargeTest; -import android.view.View; - -import com.android.imftest.R; - - -public class BigEditTextActivityScrollablePanScanTests extends ImfBaseTestCase<BigEditTextActivityScrollablePanScan> { - - public final String TAG = "BigEditTextActivityScrollablePanScanTests"; - - public BigEditTextActivityScrollablePanScanTests() { - super(BigEditTextActivityScrollablePanScan.class); - } - - @LargeTest - public void testAppAdjustmentPanScan() { // Give the IME 2 seconds to appear. - pause(2000); - - View rootView = ((BigEditTextActivityScrollablePanScan) mTargetActivity).getRootView(); - View servedView = ((BigEditTextActivityScrollablePanScan) mTargetActivity).getDefaultFocusedView(); - - assertNotNull(rootView); - assertNotNull(servedView); - - destructiveCheckImeInitialState(rootView, servedView); - - verifyEditTextAdjustment(servedView, rootView.getMeasuredHeight()); - } - -} diff --git a/tests/ImfTest/tests/src/com/android/imftest/samples/BigEditTextActivityScrollableResizeTests.java b/tests/ImfTest/tests/src/com/android/imftest/samples/BigEditTextActivityScrollableResizeTests.java deleted file mode 100644 index de607d659ebe..000000000000 --- a/tests/ImfTest/tests/src/com/android/imftest/samples/BigEditTextActivityScrollableResizeTests.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import android.test.suitebuilder.annotation.LargeTest; -import android.view.View; - -import com.android.imftest.R; - - -public class BigEditTextActivityScrollableResizeTests extends ImfBaseTestCase<BigEditTextActivityScrollableResize> { - - public final String TAG = "BigEditTextActivityScrollableResizeTests"; - - public BigEditTextActivityScrollableResizeTests() { - super(BigEditTextActivityScrollableResize.class); - } - - @LargeTest - public void testAppAdjustmentPanScan() { - // Give the IME 2 seconds to appear. - pause(2000); - - View rootView = ((BigEditTextActivityScrollableResize) mTargetActivity).getRootView(); - View servedView = ((BigEditTextActivityScrollableResize) mTargetActivity).getDefaultFocusedView(); - - assertNotNull(rootView); - assertNotNull(servedView); - - destructiveCheckImeInitialState(rootView, servedView); - - verifyEditTextAdjustment(servedView, rootView.getMeasuredHeight()); - } - -} diff --git a/tests/ImfTest/tests/src/com/android/imftest/samples/BottomEditTextActivityPanScanTests.java b/tests/ImfTest/tests/src/com/android/imftest/samples/BottomEditTextActivityPanScanTests.java deleted file mode 100644 index c52190552e4f..000000000000 --- a/tests/ImfTest/tests/src/com/android/imftest/samples/BottomEditTextActivityPanScanTests.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import android.test.suitebuilder.annotation.LargeTest; -import android.view.View; - -import com.android.imftest.R; - - -public class BottomEditTextActivityPanScanTests extends ImfBaseTestCase<BottomEditTextActivityPanScan> { - - public final String TAG = "BottomEditTextActivityPanScanTests"; - - public BottomEditTextActivityPanScanTests() { - super(BottomEditTextActivityPanScan.class); - } - - @LargeTest - public void testAppAdjustmentPanScan() { - // Give the IME 2 seconds to appear. - pause(2000); - - View rootView = ((BottomEditTextActivityPanScan) mTargetActivity).getRootView(); - View servedView = ((BottomEditTextActivityPanScan) mTargetActivity).getDefaultFocusedView(); - - assertNotNull(rootView); - assertNotNull(servedView); - - destructiveCheckImeInitialState(rootView, servedView); - - verifyEditTextAdjustment(servedView, rootView.getMeasuredHeight()); - } - -} diff --git a/tests/ImfTest/tests/src/com/android/imftest/samples/BottomEditTextActivityResizeTests.java b/tests/ImfTest/tests/src/com/android/imftest/samples/BottomEditTextActivityResizeTests.java deleted file mode 100644 index 9a69fd509844..000000000000 --- a/tests/ImfTest/tests/src/com/android/imftest/samples/BottomEditTextActivityResizeTests.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import android.test.suitebuilder.annotation.LargeTest; -import android.view.View; - -import com.android.imftest.R; - - -public class BottomEditTextActivityResizeTests extends ImfBaseTestCase<BottomEditTextActivityResize> { - - public final String TAG = "BottomEditTextActivityResizeTests"; - - public BottomEditTextActivityResizeTests() { - super(BottomEditTextActivityResize.class); - } - - @LargeTest - public void testAppAdjustmentResize() { - // Give the IME 2 seconds to appear. - pause(2000); - - View rootView = ((BottomEditTextActivityResize) mTargetActivity).getRootView(); - View servedView = ((BottomEditTextActivityResize) mTargetActivity).getDefaultFocusedView(); - - assertNotNull(rootView); - assertNotNull(servedView); - - destructiveCheckImeInitialState(rootView, servedView); - - verifyEditTextAdjustment(servedView, rootView.getMeasuredHeight()); - } - -} diff --git a/tests/ImfTest/tests/src/com/android/imftest/samples/ButtonActivityTest.java b/tests/ImfTest/tests/src/com/android/imftest/samples/ButtonActivityTest.java deleted file mode 100644 index f6f97b517090..000000000000 --- a/tests/ImfTest/tests/src/com/android/imftest/samples/ButtonActivityTest.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import android.test.suitebuilder.annotation.LargeTest; -import android.view.KeyEvent; -import android.widget.Button; - - -public class ButtonActivityTest extends ImfBaseTestCase<ButtonActivity> { - - final public String TAG = "ButtonActivityTest"; - - public ButtonActivityTest() { - super(ButtonActivity.class); - } - - @LargeTest - public void testButtonActivatesIme() { - - final Button button = (Button) mTargetActivity.findViewById(ButtonActivity.BUTTON_ID); - - // Push button - // Bring the target EditText into focus. - mTargetActivity.runOnUiThread(new Runnable() { - public void run() { - button.requestFocus(); - } - }); - - sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); - - // Give it a couple seconds - pause(2000); - - // We should have initialized imm.mServedView and imm.mCurrentTextBoxAttribute - assertTrue(mImm.isActive()); - // imm.mServedInputConnection should be null since Button doesn't override onCreateInputConnection(). - assertFalse(mImm.isAcceptingText()); - - destructiveCheckImeInitialState(mTargetActivity.getRootView(), button); - - } -} diff --git a/tests/ImfTest/tests/src/com/android/imftest/samples/ImfBaseTestCase.java b/tests/ImfTest/tests/src/com/android/imftest/samples/ImfBaseTestCase.java deleted file mode 100644 index 32f80a3ad2d5..000000000000 --- a/tests/ImfTest/tests/src/com/android/imftest/samples/ImfBaseTestCase.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import android.app.Activity; -import android.app.KeyguardManager; -import android.content.Context; -import android.content.Intent; -import android.content.res.Configuration; -import android.os.SystemClock; -import android.test.InstrumentationTestCase; -import android.view.KeyEvent; -import android.view.View; -import android.view.WindowManager; -import android.view.inputmethod.InputMethodManager; - -import com.android.imftest.R; - -public abstract class ImfBaseTestCase<T extends Activity> extends InstrumentationTestCase { - - /* - * The amount of time we are willing to wait for the IME to appear after a user action - * before we give up and fail the test. - */ - public final long WAIT_FOR_IME = 5000; - - /* - * Unfortunately there is now way for us to know how tall the IME is, - * so we have to hard code a minimum and maximum value. - */ - public final int IME_MIN_HEIGHT = 150; - public final int IME_MAX_HEIGHT = 300; - - protected InputMethodManager mImm; - protected T mTargetActivity; - protected boolean mExpectAutoPop; - private Class<T> mTargetActivityClass; - - public ImfBaseTestCase(Class<T> activityClass) { - mTargetActivityClass = activityClass; - } - - @Override - public void setUp() throws Exception { - super.setUp(); - final String packageName = getInstrumentation().getTargetContext().getPackageName(); - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); - mTargetActivity = launchActivityWithIntent(packageName, mTargetActivityClass, intent); - // expect ime to auto pop up if device has no hard keyboard - int keyboardType = mTargetActivity.getResources().getConfiguration().keyboard; - mExpectAutoPop = (keyboardType == Configuration.KEYBOARD_NOKEYS || - keyboardType == Configuration.KEYBOARD_UNDEFINED); - - mImm = InputMethodManager.getInstance(); - - KeyguardManager keyguardManager = - (KeyguardManager) getInstrumentation().getContext().getSystemService( - Context.KEYGUARD_SERVICE); - keyguardManager.newKeyguardLock("imftest").disableKeyguard(); - } - - // Utility test methods - public void verifyEditTextAdjustment(final View editText, int rootViewHeight) { - - int[] origLocation = new int[2]; - int[] newLocation = new int[2]; - - // Tell the keyboard to go away. - mImm.hideSoftInputFromWindow(editText.getWindowToken(), 0); - - // Bring the target EditText into focus. - mTargetActivity.runOnUiThread(new Runnable() { - public void run() { - editText.requestFocus(); - } - }); - - // Get the original location of the EditText. - editText.getLocationOnScreen(origLocation); - - // Tap the EditText to bring up the IME. - sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); - - // Wait until the EditText pops above the IME or until we hit the timeout. - editText.getLocationOnScreen(newLocation); - long timeoutTime = SystemClock.uptimeMillis() + WAIT_FOR_IME; - while (newLocation[1] > rootViewHeight - IME_MIN_HEIGHT && SystemClock.uptimeMillis() < timeoutTime) { - editText.getLocationOnScreen(newLocation); - pause(100); - } - - assertTrue(newLocation[1] <= rootViewHeight - IME_MIN_HEIGHT); - - // Tell the keyboard to go away. - mImm.hideSoftInputFromWindow(editText.getWindowToken(), 0); - } - - public void destructiveCheckImeInitialState(View rootView, View servedView) { - int windowSoftInputMode = mTargetActivity.getWindow().getAttributes().softInputMode; - int adjustMode = windowSoftInputMode & WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST; - if (mExpectAutoPop && adjustMode == WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) { - assertTrue(destructiveCheckImeUp(rootView, servedView)); - } else { - assertFalse(destructiveCheckImeUp(rootView, servedView)); - } - } - - public boolean destructiveCheckImeUp(View rootView, View servedView) { - int origHeight; - int newHeight; - - origHeight = rootView.getHeight(); - - // Tell the keyboard to go away. - mImm.hideSoftInputFromWindow(servedView.getWindowToken(), 0); - - // Give it five seconds to adjust - newHeight = rootView.getHeight(); - long timeoutTime = SystemClock.uptimeMillis() + WAIT_FOR_IME; - while (Math.abs(newHeight - origHeight) < IME_MIN_HEIGHT && SystemClock.uptimeMillis() < timeoutTime) { - newHeight = rootView.getHeight(); - } - - return (Math.abs(origHeight - newHeight) >= IME_MIN_HEIGHT); - } - - void pause(int millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException e) { - } - } - -} diff --git a/tests/ImfTest/tests/src/com/android/imftest/samples/ManyEditTextActivityBaseTestCase.java b/tests/ImfTest/tests/src/com/android/imftest/samples/ManyEditTextActivityBaseTestCase.java deleted file mode 100644 index 278efb1f68db..000000000000 --- a/tests/ImfTest/tests/src/com/android/imftest/samples/ManyEditTextActivityBaseTestCase.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import android.app.Activity; -import android.widget.EditText; - - -public abstract class ManyEditTextActivityBaseTestCase<T extends Activity> extends ImfBaseTestCase<T> { - - public ManyEditTextActivityBaseTestCase(Class<T> activityClass){ - super(activityClass); - } - - public abstract void testAllEditTextsAdjust(); - - public void verifyAllEditTextAdjustment(int numEditTexts, int rootViewHeight) { - - for (int i = 0; i < numEditTexts; i++) { - final EditText lastEditText = (EditText) mTargetActivity.findViewById(i); - verifyEditTextAdjustment(lastEditText, rootViewHeight); - } - - } - -} diff --git a/tests/ImfTest/tests/src/com/android/imftest/samples/ManyEditTextActivityNoScrollPanScanTests.java b/tests/ImfTest/tests/src/com/android/imftest/samples/ManyEditTextActivityNoScrollPanScanTests.java deleted file mode 100644 index 4f8d14e88ad5..000000000000 --- a/tests/ImfTest/tests/src/com/android/imftest/samples/ManyEditTextActivityNoScrollPanScanTests.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import android.test.suitebuilder.annotation.LargeTest; - - -public class ManyEditTextActivityNoScrollPanScanTests extends ManyEditTextActivityBaseTestCase<ManyEditTextActivityNoScrollPanScan> { - - public final String TAG = "ManyEditTextActivityNoScrollPanScanTests"; - - public ManyEditTextActivityNoScrollPanScanTests() { - super(ManyEditTextActivityNoScrollPanScan.class); - } - - - @LargeTest - public void testAllEditTextsAdjust() { - verifyAllEditTextAdjustment(mTargetActivity.NUM_EDIT_TEXTS, - mTargetActivity.getRootView().getMeasuredHeight()); - } -} diff --git a/tests/ImfTest/tests/src/com/android/imftest/samples/ManyEditTextActivityScrollPanScanTests.java b/tests/ImfTest/tests/src/com/android/imftest/samples/ManyEditTextActivityScrollPanScanTests.java deleted file mode 100644 index 7f98f7fbdf21..000000000000 --- a/tests/ImfTest/tests/src/com/android/imftest/samples/ManyEditTextActivityScrollPanScanTests.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import android.test.suitebuilder.annotation.LargeTest; - - -public class ManyEditTextActivityScrollPanScanTests extends ManyEditTextActivityBaseTestCase<ManyEditTextActivityScrollPanScan> { - - public final String TAG = "ManyEditTextActivityScrollPanScanTests"; - - - public ManyEditTextActivityScrollPanScanTests() { - super(ManyEditTextActivityScrollPanScan.class); - } - - @LargeTest - public void testAllEditTextsAdjust() { - verifyAllEditTextAdjustment(mTargetActivity.NUM_EDIT_TEXTS, - mTargetActivity.getRootView().getMeasuredHeight()); - } - -} diff --git a/tests/ImfTest/tests/src/com/android/imftest/samples/ManyEditTextActivityScrollResizeTests.java b/tests/ImfTest/tests/src/com/android/imftest/samples/ManyEditTextActivityScrollResizeTests.java deleted file mode 100644 index 68dae87ea5c9..000000000000 --- a/tests/ImfTest/tests/src/com/android/imftest/samples/ManyEditTextActivityScrollResizeTests.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import android.test.suitebuilder.annotation.LargeTest; - - -public class ManyEditTextActivityScrollResizeTests extends ManyEditTextActivityBaseTestCase<ManyEditTextActivityScrollResize> { - - public final String TAG = "ManyEditTextActivityScrollResizeTests"; - - - public ManyEditTextActivityScrollResizeTests() { - super(ManyEditTextActivityScrollResize.class); - } - - @LargeTest - public void testAllEditTextsAdjust() { - verifyAllEditTextAdjustment(mTargetActivity.NUM_EDIT_TEXTS, - mTargetActivity.getRootView().getMeasuredHeight()); - } - -} diff --git a/tests/ImfTest/tests/src/com/android/imftest/samples/OneEditTextActivityNotSelectedTests.java b/tests/ImfTest/tests/src/com/android/imftest/samples/OneEditTextActivityNotSelectedTests.java deleted file mode 100644 index 6147d3c5c6c4..000000000000 --- a/tests/ImfTest/tests/src/com/android/imftest/samples/OneEditTextActivityNotSelectedTests.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import android.test.suitebuilder.annotation.LargeTest; -import android.view.View; - - -public class OneEditTextActivityNotSelectedTests extends ImfBaseTestCase<OneEditTextActivityNotSelected> { - - public final String TAG = "OneEditTextActivityNotSelectedTests"; - - public OneEditTextActivityNotSelectedTests() { - super(OneEditTextActivityNotSelected.class); - } - - @LargeTest - public void testSoftKeyboardNoAutoPop() { - - // Give the IME 2 seconds to appear. - pause(2000); - - assertFalse(mImm.isAcceptingText()); - - View rootView = ((OneEditTextActivityNotSelected) mTargetActivity).getRootView(); - View servedView = ((OneEditTextActivityNotSelected) mTargetActivity).getDefaultFocusedView(); - - assertNotNull(rootView); - assertNotNull(servedView); - - destructiveCheckImeInitialState(rootView, servedView); - - verifyEditTextAdjustment(servedView, rootView.getMeasuredHeight()); - } - -} diff --git a/tests/ImfTest/tests/src/com/android/imftest/samples/OneEditTextActivitySelectedTests.java b/tests/ImfTest/tests/src/com/android/imftest/samples/OneEditTextActivitySelectedTests.java deleted file mode 100644 index 42fcd66f7922..000000000000 --- a/tests/ImfTest/tests/src/com/android/imftest/samples/OneEditTextActivitySelectedTests.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.imftest.samples; - -import com.android.imftest.R; - -import android.test.suitebuilder.annotation.LargeTest; -import android.view.KeyEvent; -import android.view.View; - - -public class OneEditTextActivitySelectedTests extends ImfBaseTestCase<OneEditTextActivitySelected> { - - public final String TAG = "OneEditTextActivitySelectedTests"; - - public OneEditTextActivitySelectedTests() { - super(OneEditTextActivitySelected.class); - } - - @LargeTest - public void testSoftKeyboardAutoPop() { - - // Give the IME 2 seconds to appear. - pause(2000); - - assertTrue(mImm.isAcceptingText()); - - View rootView = ((OneEditTextActivitySelected) mTargetActivity).getRootView(); - View servedView = ((OneEditTextActivitySelected) mTargetActivity).getDefaultFocusedView(); - - assertNotNull(rootView); - assertNotNull(servedView); - - destructiveCheckImeInitialState(rootView, servedView); - - verifyEditTextAdjustment(servedView, rootView.getMeasuredHeight()); - } - -} diff --git a/tests/Internal/Android.bp b/tests/Internal/Android.bp index db3e03bf20ed..e233fed7e785 100644 --- a/tests/Internal/Android.bp +++ b/tests/Internal/Android.bp @@ -8,8 +8,9 @@ android_test { libs: ["android.test.runner"], static_libs: [ "junit", - "android-support-test", + "androidx.test.rules", "mockito-target-minus-junit4", + "truth-prebuilt", ], java_resource_dirs: ["res"], certificate: "platform", diff --git a/tests/Internal/AndroidManifest.xml b/tests/Internal/AndroidManifest.xml index e5a56949fe4e..c85c3b12504a 100644 --- a/tests/Internal/AndroidManifest.xml +++ b/tests/Internal/AndroidManifest.xml @@ -38,7 +38,7 @@ </service> </application> - <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner" + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" android:targetPackage="com.android.internal.tests" android:label="Internal Tests" /> </manifest> diff --git a/tests/Internal/AndroidTest.xml b/tests/Internal/AndroidTest.xml index 6531c9355e3d..7b67e9ebcced 100644 --- a/tests/Internal/AndroidTest.xml +++ b/tests/Internal/AndroidTest.xml @@ -24,6 +24,6 @@ <option name="test-tag" value="InternalTests" /> <test class="com.android.tradefed.testtype.AndroidJUnitTest" > <option name="package" value="com.android.internal.tests" /> - <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> </test> </configuration>
\ No newline at end of file diff --git a/tests/Internal/res/xml/livewallpaper.xml b/tests/Internal/res/xml/livewallpaper.xml index dbb0e4761cba..36e7e4182c31 100644 --- a/tests/Internal/res/xml/livewallpaper.xml +++ b/tests/Internal/res/xml/livewallpaper.xml @@ -16,5 +16,5 @@ --> <wallpaper xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" - androidprv:supportsAmbientMode="true"/>
\ No newline at end of file + android:settingsSliceUri="content://com.android.internal.tests/slice" + android:supportsAmbientMode="true"/> diff --git a/tests/Internal/src/android/app/WallpaperColorsTest.java b/tests/Internal/src/android/app/WallpaperColorsTest.java index 881f6284413f..65ff6eb1ba04 100644 --- a/tests/Internal/src/android/app/WallpaperColorsTest.java +++ b/tests/Internal/src/android/app/WallpaperColorsTest.java @@ -20,8 +20,9 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; import org.junit.Assert; import org.junit.Test; diff --git a/tests/Internal/src/android/app/WallpaperInfoTest.java b/tests/Internal/src/android/app/WallpaperInfoTest.java index 9d26270fb96a..476b99155672 100644 --- a/tests/Internal/src/android/app/WallpaperInfoTest.java +++ b/tests/Internal/src/android/app/WallpaperInfoTest.java @@ -23,11 +23,13 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.net.Uri; import android.os.Parcel; import android.service.wallpaper.WallpaperService; -import android.support.test.InstrumentationRegistry; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; @@ -55,13 +57,39 @@ public class WallpaperInfoTest { // Defined as true in the XML assertTrue("supportsAmbientMode should be true, as defined in the XML.", - wallpaperInfo.getSupportsAmbientMode()); + wallpaperInfo.supportsAmbientMode()); Parcel parcel = Parcel.obtain(); wallpaperInfo.writeToParcel(parcel, 0 /* flags */); parcel.setDataPosition(0); WallpaperInfo fromParcel = WallpaperInfo.CREATOR.createFromParcel(parcel); assertTrue("supportsAmbientMode should have been restored from parcelable", - fromParcel.getSupportsAmbientMode()); + fromParcel.supportsAmbientMode()); + parcel.recycle(); + } + + @Test + public void testGetSettingsSliceUri() throws Exception { + Context context = InstrumentationRegistry.getTargetContext(); + + Intent intent = new Intent(WallpaperService.SERVICE_INTERFACE); + intent.setPackage("com.android.internal.tests"); + PackageManager pm = context.getPackageManager(); + List<ResolveInfo> result = pm.queryIntentServices(intent, PackageManager.GET_META_DATA); + assertEquals(1, result.size()); + ResolveInfo info = result.get(0); + WallpaperInfo wallpaperInfo = new WallpaperInfo(context, info); + + // This expected Uri must be the same as that in livewallpaper.xml + Uri expectedUri = Uri.parse("content://com.android.internal.tests/slice"); + Uri settingsUri = wallpaperInfo.getSettingsSliceUri(); + assertEquals("The loaded URI should equal to the string in livewallpaper.xml", + 0, expectedUri.compareTo(settingsUri)); + Parcel parcel = Parcel.obtain(); + wallpaperInfo.writeToParcel(parcel, 0 /* flags */); + parcel.setDataPosition(0); + WallpaperInfo fromParcel = WallpaperInfo.CREATOR.createFromParcel(parcel); + assertEquals("settingsSliceUri should be restorable from parcelable", + 0, expectedUri.compareTo(fromParcel.getSettingsSliceUri())); parcel.recycle(); } } diff --git a/tests/Internal/src/android/service/wallpaper/WallpaperServiceTest.java b/tests/Internal/src/android/service/wallpaper/WallpaperServiceTest.java index 8d8fc84126ec..592aa3ac4a6b 100644 --- a/tests/Internal/src/android/service/wallpaper/WallpaperServiceTest.java +++ b/tests/Internal/src/android/service/wallpaper/WallpaperServiceTest.java @@ -20,7 +20,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import android.support.test.filters.SmallTest; +import androidx.test.filters.SmallTest; import org.junit.Test; import org.junit.runner.RunWith; @@ -38,7 +38,7 @@ public class WallpaperServiceTest { public Engine onCreateEngine() { return new Engine() { @Override - public void onAmbientModeChanged(boolean inAmbientMode, boolean animated) { + public void onAmbientModeChanged(boolean inAmbientMode, long duration) { ambientModeChangedCount[0]++; } }; @@ -47,12 +47,12 @@ public class WallpaperServiceTest { WallpaperService.Engine engine = service.onCreateEngine(); engine.setCreated(true); - engine.doAmbientModeChanged(false, false); + engine.doAmbientModeChanged(false, 0); assertFalse("ambient mode should be false", engine.isInAmbientMode()); assertEquals("onAmbientModeChanged should have been called", ambientModeChangedCount[0], 1); - engine.doAmbientModeChanged(true, false); + engine.doAmbientModeChanged(true, 0); assertTrue("ambient mode should be false", engine.isInAmbientMode()); assertEquals("onAmbientModeChanged should have been called", ambientModeChangedCount[0], 2); diff --git a/tests/Internal/src/com/android/internal/colorextraction/ColorExtractorTest.java b/tests/Internal/src/com/android/internal/colorextraction/ColorExtractorTest.java index 39608a40b416..45ddc3eed39c 100644 --- a/tests/Internal/src/com/android/internal/colorextraction/ColorExtractorTest.java +++ b/tests/Internal/src/com/android/internal/colorextraction/ColorExtractorTest.java @@ -27,9 +27,10 @@ import android.app.WallpaperColors; import android.app.WallpaperManager; import android.content.Context; import android.graphics.Color; -import android.support.test.InstrumentationRegistry; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; import com.android.internal.colorextraction.ColorExtractor.GradientColors; import com.android.internal.colorextraction.types.ExtractionType; @@ -38,6 +39,8 @@ import com.android.internal.colorextraction.types.Tonal; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; /** * Tests color extraction generation. @@ -47,16 +50,19 @@ import org.junit.runner.RunWith; public class ColorExtractorTest { Context mContext; + @Mock + WallpaperManager mWallpaperManager; @Before public void setup() { + MockitoAnnotations.initMocks(this); mContext = InstrumentationRegistry.getContext(); } @Test public void ColorExtractor_extractWhenInitialized() { ExtractionType type = mock(Tonal.class); - new ColorExtractor(mContext, type, true); + new ColorExtractor(mContext, type, true, mWallpaperManager); // 1 for lock and 1 for system verify(type, times(2)) .extractInto(any(), any(), any(), any()); @@ -83,7 +89,7 @@ public class ColorExtractorTest { outGradientColorsDark.set(colorsExpectedDark); outGradientColorsExtraDark.set(colorsExpectedExtraDark); }; - ColorExtractor extractor = new ColorExtractor(mContext, type, true); + ColorExtractor extractor = new ColorExtractor(mContext, type, true, mWallpaperManager); GradientColors colors = extractor.getColors(WallpaperManager.FLAG_SYSTEM, ColorExtractor.TYPE_NORMAL); @@ -98,7 +104,8 @@ public class ColorExtractorTest { public void addOnColorsChangedListener_invokesListener() { ColorExtractor.OnColorsChangedListener mockedListeners = mock(ColorExtractor.OnColorsChangedListener.class); - ColorExtractor extractor = new ColorExtractor(mContext, new Tonal(mContext), true); + ColorExtractor extractor = new ColorExtractor(mContext, new Tonal(mContext), true, + mWallpaperManager); extractor.addOnColorsChangedListener(mockedListeners); extractor.onColorsChanged(new WallpaperColors(Color.valueOf(Color.RED), null, null), diff --git a/tests/Internal/src/com/android/internal/colorextraction/types/TonalTest.java b/tests/Internal/src/com/android/internal/colorextraction/types/TonalTest.java index a7d5ae8f69a3..768e47c03b17 100644 --- a/tests/Internal/src/com/android/internal/colorextraction/types/TonalTest.java +++ b/tests/Internal/src/com/android/internal/colorextraction/types/TonalTest.java @@ -15,18 +15,20 @@ */ package com.android.internal.colorextraction.types; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.app.WallpaperColors; import android.graphics.Color; -import android.support.test.InstrumentationRegistry; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; import android.util.Range; +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + import com.android.internal.colorextraction.ColorExtractor.GradientColors; -import com.android.internal.graphics.ColorUtils; import org.junit.Test; import org.junit.runner.RunWith; @@ -67,6 +69,36 @@ public class TonalTest { } @Test + public void extractInto_fromBitmap() { + Tonal tonal = new Tonal(InstrumentationRegistry.getContext()); + GradientColors normal = new GradientColors(); + GradientColors dark = new GradientColors(); + GradientColors extraDark = new GradientColors(); + WallpaperColors wallColors = new WallpaperColors(Color.valueOf(Color.RED), null, null, + WallpaperColors.HINT_FROM_BITMAP); + + // WHEN colors are extracted from a wallpaper with only a red primary color. + tonal.extractInto(wallColors, normal, dark, extraDark); + // THEN the main extracted color is red + assertThat(normal.getMainColor()).isEqualTo(Color.RED); + } + + @Test + public void extractInto_supportsDarkText() { + Tonal tonal = new Tonal(InstrumentationRegistry.getContext()); + GradientColors normal = new GradientColors(); + GradientColors dark = new GradientColors(); + GradientColors extraDark = new GradientColors(); + WallpaperColors wallColors = new WallpaperColors(Color.valueOf(Color.RED), null, null, + WallpaperColors.HINT_SUPPORTS_DARK_TEXT); + + // WHEN colors are extracted from a wallpaper with only a red primary color. + tonal.extractInto(wallColors, normal, dark, extraDark); + // THEN the main extracted color is red + assertThat(normal.getMainColor()).isEqualTo(Color.RED); + } + + @Test public void colorRange_containsColor() { Tonal.ColorRange colorRange = new Tonal.ColorRange(new Range<>(0f, 50f), new Range<>(0f, 1f), new Range<>(0f, 1f)); @@ -95,7 +127,6 @@ public class TonalTest { Tonal.ConfigParser config = new Tonal.ConfigParser(InstrumentationRegistry.getContext()); // 1 to avoid regression where only first item would be parsed. assertTrue("Tonal palettes are empty", config.getTonalPalettes().size() > 1); - assertTrue("Blacklisted colors are empty", config.getBlacklistedColors().size() > 1); } @Test @@ -112,41 +143,4 @@ public class TonalTest { assertTrue("L should be <= to 1.", palette.l[1] <= 1); } } - - @Test - public void tonal_blacklistTest() { - // Make sure that palette generation will fail. - final Tonal tonal = new Tonal(InstrumentationRegistry.getContext()); - - // Creating a WallpaperColors object that contains *only* blacklisted colors. - final float[] hsl = tonal.getBlacklistedColors().get(0).getCenter(); - final int blacklistedColor = ColorUtils.HSLToColor(hsl); - WallpaperColors colorsFromBitmap = new WallpaperColors(Color.valueOf(blacklistedColor), - null, null, WallpaperColors.HINT_FROM_BITMAP); - - // Make sure that palette generation will fail - final GradientColors normal = new GradientColors(); - tonal.extractInto(colorsFromBitmap, normal, new GradientColors(), - new GradientColors()); - assertTrue("Cannot generate a tonal palette from blacklisted colors.", - normal.getMainColor() == Tonal.MAIN_COLOR_DARK); - } - - @Test - public void tonal_ignoreBlacklistTest() { - final Tonal tonal = new Tonal(InstrumentationRegistry.getContext()); - - // Creating a WallpaperColors object that contains *only* blacklisted colors. - final float[] hsl = tonal.getBlacklistedColors().get(0).getCenter(); - final int blacklistedColor = ColorUtils.HSLToColor(hsl); - WallpaperColors colors = new WallpaperColors(Color.valueOf(blacklistedColor), - null, null); - - // Blacklist should be ignored when HINT_FROM_BITMAP isn't present. - final GradientColors normal = new GradientColors(); - tonal.extractInto(colors, normal, new GradientColors(), - new GradientColors()); - assertTrue("Blacklist should never be used on WallpaperColors generated using " - + "default constructor.", normal.getMainColor() == blacklistedColor); - } } diff --git a/tests/Internal/src/com/android/internal/graphics/ColorUtilsTest.java b/tests/Internal/src/com/android/internal/graphics/ColorUtilsTest.java index 73df9a09ea75..d0bb8e3745bc 100644 --- a/tests/Internal/src/com/android/internal/graphics/ColorUtilsTest.java +++ b/tests/Internal/src/com/android/internal/graphics/ColorUtilsTest.java @@ -16,12 +16,13 @@ package com.android.internal.graphics; +import static org.junit.Assert.assertTrue; + import android.graphics.Color; -import android.support.test.filters.SmallTest; -import org.junit.Test; +import androidx.test.filters.SmallTest; -import static org.junit.Assert.assertTrue; +import org.junit.Test; @SmallTest public class ColorUtilsTest { diff --git a/tests/Internal/src/com/android/internal/ml/clustering/KMeansTest.java b/tests/Internal/src/com/android/internal/ml/clustering/KMeansTest.java index a64f8a60d485..540a1ec2bd5a 100644 --- a/tests/Internal/src/com/android/internal/ml/clustering/KMeansTest.java +++ b/tests/Internal/src/com/android/internal/ml/clustering/KMeansTest.java @@ -20,8 +20,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import android.annotation.SuppressLint; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; import org.junit.Assert; import org.junit.Before; diff --git a/tests/JankBench/Android.bp b/tests/JankBench/Android.bp index 4ca33154e927..166639d2e7db 100644 --- a/tests/JankBench/Android.bp +++ b/tests/JankBench/Android.bp @@ -8,12 +8,12 @@ android_test { // regressions are reflected in test data resource_dirs: ["app/src/main/res"], static_libs: [ - "android-support-design", - "android-support-v4", - "android-support-v7-appcompat", - "android-support-v7-cardview", - "android-support-v7-recyclerview", - "android-support-v17-leanback", + "com.google.android.material_material", + "androidx.legacy_legacy-support-v4", + "androidx.appcompat_appcompat", + "androidx.cardview_cardview", + "androidx.recyclerview_recyclerview", + "androidx.leanback_leanback", "apache-commons-math", "junit", ], diff --git a/tests/JankBench/app/src/main/java/com/android/benchmark/app/BenchmarkDashboardFragment.java b/tests/JankBench/app/src/main/java/com/android/benchmark/app/BenchmarkDashboardFragment.java index b0a97ae0995b..c5edd7ae75e0 100644 --- a/tests/JankBench/app/src/main/java/com/android/benchmark/app/BenchmarkDashboardFragment.java +++ b/tests/JankBench/app/src/main/java/com/android/benchmark/app/BenchmarkDashboardFragment.java @@ -16,7 +16,7 @@ package com.android.benchmark.app; -import android.support.v4.app.Fragment; +import androidx.fragment.app.Fragment; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; diff --git a/tests/JankBench/app/src/main/java/com/android/benchmark/app/HomeActivity.java b/tests/JankBench/app/src/main/java/com/android/benchmark/app/HomeActivity.java index 79bafd62e2a2..4de51fb57308 100644 --- a/tests/JankBench/app/src/main/java/com/android/benchmark/app/HomeActivity.java +++ b/tests/JankBench/app/src/main/java/com/android/benchmark/app/HomeActivity.java @@ -19,22 +19,24 @@ package com.android.benchmark.app; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; -import android.support.design.widget.FloatingActionButton; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; import android.view.LayoutInflater; -import android.view.View; import android.view.Menu; import android.view.MenuItem; +import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ExpandableListView; import android.widget.Toast; -import com.android.benchmark.registry.BenchmarkRegistry; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; + import com.android.benchmark.R; +import com.android.benchmark.registry.BenchmarkRegistry; import com.android.benchmark.results.GlobalResultsStore; +import com.google.android.material.floatingactionbutton.FloatingActionButton; + import java.io.IOException; import java.util.LinkedList; import java.util.Queue; diff --git a/tests/JankBench/app/src/main/java/com/android/benchmark/app/RunLocalBenchmarksActivity.java b/tests/JankBench/app/src/main/java/com/android/benchmark/app/RunLocalBenchmarksActivity.java index 0433f9234b47..c16efbda1830 100644 --- a/tests/JankBench/app/src/main/java/com/android/benchmark/app/RunLocalBenchmarksActivity.java +++ b/tests/JankBench/app/src/main/java/com/android/benchmark/app/RunLocalBenchmarksActivity.java @@ -20,10 +20,10 @@ import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentTransaction; -import android.support.v4.app.ListFragment; -import android.support.v7.app.AppCompatActivity; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; +import androidx.fragment.app.ListFragment; +import androidx.appcompat.app.AppCompatActivity; import android.util.Log; import android.view.LayoutInflater; import android.view.View; diff --git a/tests/JankBench/app/src/main/java/com/android/benchmark/app/UiResultsFragment.java b/tests/JankBench/app/src/main/java/com/android/benchmark/app/UiResultsFragment.java index 56e94d538ab5..68aeb4ccd06d 100644 --- a/tests/JankBench/app/src/main/java/com/android/benchmark/app/UiResultsFragment.java +++ b/tests/JankBench/app/src/main/java/com/android/benchmark/app/UiResultsFragment.java @@ -19,8 +19,8 @@ package com.android.benchmark.app; import android.annotation.TargetApi; import android.os.AsyncTask; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.app.ListFragment; +import androidx.annotation.Nullable; +import androidx.fragment.app.ListFragment; import android.util.Log; import android.view.FrameMetrics; import android.widget.SimpleAdapter; diff --git a/tests/JankBench/app/src/main/java/com/android/benchmark/registry/BenchmarkCategory.java b/tests/JankBench/app/src/main/java/com/android/benchmark/registry/BenchmarkCategory.java index d91e5798f05b..d13b5692c6b8 100644 --- a/tests/JankBench/app/src/main/java/com/android/benchmark/registry/BenchmarkCategory.java +++ b/tests/JankBench/app/src/main/java/com/android/benchmark/registry/BenchmarkCategory.java @@ -16,7 +16,7 @@ package com.android.benchmark.registry; -import android.support.annotation.IntDef; +import androidx.annotation.IntDef; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/tests/JankBench/app/src/main/java/com/android/benchmark/ui/BitmapUploadActivity.java b/tests/JankBench/app/src/main/java/com/android/benchmark/ui/BitmapUploadActivity.java index 1c96cace1606..7692836cfacc 100644 --- a/tests/JankBench/app/src/main/java/com/android/benchmark/ui/BitmapUploadActivity.java +++ b/tests/JankBench/app/src/main/java/com/android/benchmark/ui/BitmapUploadActivity.java @@ -25,7 +25,7 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Rect; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatActivity; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.MotionEvent; diff --git a/tests/JankBench/app/src/main/java/com/android/benchmark/ui/EditTextInputActivity.java b/tests/JankBench/app/src/main/java/com/android/benchmark/ui/EditTextInputActivity.java index ea6fb58f4775..3817ab50e120 100644 --- a/tests/JankBench/app/src/main/java/com/android/benchmark/ui/EditTextInputActivity.java +++ b/tests/JankBench/app/src/main/java/com/android/benchmark/ui/EditTextInputActivity.java @@ -18,8 +18,8 @@ package com.android.benchmark.ui; import android.content.Intent; import android.os.Bundle; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; import android.view.KeyEvent; import android.widget.EditText; diff --git a/tests/JankBench/app/src/main/java/com/android/benchmark/ui/FullScreenOverdrawActivity.java b/tests/JankBench/app/src/main/java/com/android/benchmark/ui/FullScreenOverdrawActivity.java index 95fce3834f9b..c508bf99ffba 100644 --- a/tests/JankBench/app/src/main/java/com/android/benchmark/ui/FullScreenOverdrawActivity.java +++ b/tests/JankBench/app/src/main/java/com/android/benchmark/ui/FullScreenOverdrawActivity.java @@ -23,7 +23,7 @@ import android.content.Intent; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; -import android.support.v7.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.view.MotionEvent; import android.view.View; diff --git a/tests/JankBench/app/src/main/java/com/android/benchmark/ui/ListActivityBase.java b/tests/JankBench/app/src/main/java/com/android/benchmark/ui/ListActivityBase.java index b973bc76c13f..7d7b04c27f84 100644 --- a/tests/JankBench/app/src/main/java/com/android/benchmark/ui/ListActivityBase.java +++ b/tests/JankBench/app/src/main/java/com/android/benchmark/ui/ListActivityBase.java @@ -18,9 +18,9 @@ package com.android.benchmark.ui; import android.app.ActionBar; import android.os.Bundle; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.ListFragment; -import android.support.v7.app.AppCompatActivity; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.ListFragment; +import androidx.appcompat.app.AppCompatActivity; import android.view.Window; import android.widget.ListAdapter; diff --git a/tests/JankBench/app/src/main/java/com/android/benchmark/ui/ListViewScrollActivity.java b/tests/JankBench/app/src/main/java/com/android/benchmark/ui/ListViewScrollActivity.java index 3ffb7706675f..8b3a96374de6 100644 --- a/tests/JankBench/app/src/main/java/com/android/benchmark/ui/ListViewScrollActivity.java +++ b/tests/JankBench/app/src/main/java/com/android/benchmark/ui/ListViewScrollActivity.java @@ -18,7 +18,7 @@ package com.android.benchmark.ui; import android.content.Intent; import android.os.Bundle; -import android.support.v7.app.ActionBar; +import androidx.appcompat.app.ActionBar; import android.view.FrameMetrics; import android.view.MotionEvent; import android.widget.ArrayAdapter; diff --git a/tests/JankBench/app/src/main/java/com/android/benchmark/ui/ShadowGridActivity.java b/tests/JankBench/app/src/main/java/com/android/benchmark/ui/ShadowGridActivity.java index 68f75a3f277b..c96899b136d0 100644 --- a/tests/JankBench/app/src/main/java/com/android/benchmark/ui/ShadowGridActivity.java +++ b/tests/JankBench/app/src/main/java/com/android/benchmark/ui/ShadowGridActivity.java @@ -17,9 +17,9 @@ package com.android.benchmark.ui; import android.content.Intent; import android.os.Bundle; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.ListFragment; -import android.support.v7.app.AppCompatActivity; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.ListFragment; +import androidx.appcompat.app.AppCompatActivity; import android.view.View; import android.widget.ArrayAdapter; import android.widget.ListView; diff --git a/tests/JankBench/app/src/main/java/com/android/benchmark/ui/automation/FrameTimingStats.java b/tests/JankBench/app/src/main/java/com/android/benchmark/ui/automation/FrameTimingStats.java index 1fd0998f8dd6..bf882dc83c89 100644 --- a/tests/JankBench/app/src/main/java/com/android/benchmark/ui/automation/FrameTimingStats.java +++ b/tests/JankBench/app/src/main/java/com/android/benchmark/ui/automation/FrameTimingStats.java @@ -16,7 +16,7 @@ package com.android.benchmark.ui.automation; -import android.support.annotation.IntDef; +import androidx.annotation.IntDef; import java.io.DataInput; import java.io.DataInputStream; diff --git a/tests/JankBench/app/src/main/java/com/android/benchmark/ui/automation/Interaction.java b/tests/JankBench/app/src/main/java/com/android/benchmark/ui/automation/Interaction.java index 370fed28e1b2..31a26698c6d2 100644 --- a/tests/JankBench/app/src/main/java/com/android/benchmark/ui/automation/Interaction.java +++ b/tests/JankBench/app/src/main/java/com/android/benchmark/ui/automation/Interaction.java @@ -17,7 +17,7 @@ package com.android.benchmark.ui.automation; import android.os.SystemClock; -import android.support.annotation.IntDef; +import androidx.annotation.IntDef; import android.view.MotionEvent; import java.util.ArrayList; diff --git a/tests/JankBench/app/src/main/jni/Android.bp.converted b/tests/JankBench/app/src/main/jni/Android.bp.converted index b53c79ad8b7c..9fecf1599fd9 100644 --- a/tests/JankBench/app/src/main/jni/Android.bp.converted +++ b/tests/JankBench/app/src/main/jni/Android.bp.converted @@ -12,20 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -<<<<<<< HEAD cc_library_shared { name: "libnativebench", cflags: [ "-Wno-unused-parameter", "-Wno-unused-variable", ], -======= -// ANDROIDMK TRANSLATION WARNING: No 'include $(CLEAR_VARS)' detected before first assignment; clearing vars now - -cc_library_shared { - name: "libnativebench", - cflags: ["-Wno-unused-parameter"], ->>>>>>> Convert Android.mk file to Android.bp srcs: [ "Bench.cpp", "WorkerPool.cpp", diff --git a/tests/JankBench/app/src/main/res/layout/activity_bitmap_upload.xml b/tests/JankBench/app/src/main/res/layout/activity_bitmap_upload.xml index 6b3c8992d414..15cd0a4c528e 100644 --- a/tests/JankBench/app/src/main/res/layout/activity_bitmap_upload.xml +++ b/tests/JankBench/app/src/main/res/layout/activity_bitmap_upload.xml @@ -22,7 +22,7 @@ android:layout_height="match_parent" android:padding="10dp" android:clipToPadding="false"> - <android.support.v7.widget.CardView + <androidx.cardview.widget.CardView android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"> @@ -30,22 +30,22 @@ android:id="@+id/upload_view" android:layout_width="match_parent" android:layout_height="match_parent"/> - </android.support.v7.widget.CardView> + </androidx.cardview.widget.CardView> - <android.support.v4.widget.Space + <androidx.legacy.widget.Space android:layout_height="10dp" android:layout_width="match_parent" /> - <android.support.v7.widget.CardView + <androidx.cardview.widget.CardView android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> - <android.support.v4.widget.Space + <androidx.legacy.widget.Space android:layout_height="10dp" android:layout_width="match_parent" /> - <android.support.v7.widget.CardView + <androidx.cardview.widget.CardView android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> diff --git a/tests/JankBench/app/src/main/res/layout/activity_home.xml b/tests/JankBench/app/src/main/res/layout/activity_home.xml index c4f429922aab..160d9d7cd41e 100644 --- a/tests/JankBench/app/src/main/res/layout/activity_home.xml +++ b/tests/JankBench/app/src/main/res/layout/activity_home.xml @@ -15,7 +15,7 @@ ~ --> -<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" +<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" @@ -23,20 +23,20 @@ android:fitsSystemWindows="true" tools:context=".app.HomeActivity"> - <android.support.design.widget.AppBarLayout + <com.google.android.material.appbar.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/AppTheme.AppBarOverlay"> - <android.support.v7.widget.Toolbar + <androidx.appcompat.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" app:popupTheme="@style/AppTheme.PopupOverlay" /> - </android.support.design.widget.AppBarLayout> + </com.google.android.material.appbar.AppBarLayout> <include layout="@layout/content_main" /> -</android.support.design.widget.CoordinatorLayout> +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/tests/JankBench/app/src/main/res/layout/card_row.xml b/tests/JankBench/app/src/main/res/layout/card_row.xml index 215f9df9b7fd..9ebff8e2d6ca 100644 --- a/tests/JankBench/app/src/main/res/layout/card_row.xml +++ b/tests/JankBench/app/src/main/res/layout/card_row.xml @@ -24,7 +24,7 @@ android:paddingBottom="5dp" android:clipToPadding="false" android:background="@null"> - <android.support.v7.widget.CardView + <androidx.cardview.widget.CardView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1"> @@ -32,13 +32,13 @@ android:id="@+id/card_text" android:layout_width="match_parent" android:layout_height="match_parent"/> - </android.support.v7.widget.CardView> + </androidx.cardview.widget.CardView> - <android.support.v4.widget.Space + <androidx.legacy.widget.Space android:layout_height="match_parent" android:layout_width="10dp" /> - <android.support.v7.widget.CardView + <androidx.cardview.widget.CardView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" /> diff --git a/tests/JankBench/app/src/main/res/layout/fragment_dashboard.xml b/tests/JankBench/app/src/main/res/layout/fragment_dashboard.xml index f3100c7ea75d..2f4813f7e48d 100644 --- a/tests/JankBench/app/src/main/res/layout/fragment_dashboard.xml +++ b/tests/JankBench/app/src/main/res/layout/fragment_dashboard.xml @@ -15,21 +15,21 @@ ~ --> -<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" +<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/main_content" android:layout_width="match_parent" android:layout_height="fill_parent" android:fitsSystemWindows="true"> - <android.support.design.widget.AppBarLayout + <com.google.android.material.appbar.AppBarLayout android:id="@+id/appbar" android:layout_width="match_parent" android:layout_height="@dimen/detail_backdrop_height" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" android:fitsSystemWindows="true"> - <android.support.design.widget.CollapsingToolbarLayout + <com.google.android.material.appbar.CollapsingToolbarLayout android:id="@+id/collapsing_toolbar" android:layout_width="match_parent" android:layout_height="match_parent" @@ -39,7 +39,7 @@ app:expandedTitleMarginStart="48dp" app:expandedTitleMarginEnd="64dp"> - <android.support.v7.widget.Toolbar + <androidx.appcompat.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" @@ -55,11 +55,11 @@ android:fitsSystemWindows="true" app:layout_collapseMode="parallax" /> - </android.support.design.widget.CollapsingToolbarLayout> + </com.google.android.material.appbar.CollapsingToolbarLayout> - </android.support.design.widget.AppBarLayout> + </com.google.android.material.appbar.AppBarLayout> - <android.support.v4.widget.NestedScrollView + <androidx.core.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"> @@ -76,9 +76,9 @@ </LinearLayout> - </android.support.v4.widget.NestedScrollView> + </androidx.core.widget.NestedScrollView> - <android.support.design.widget.FloatingActionButton + <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/start_button" android:layout_height="wrap_content" android:layout_width="wrap_content" @@ -88,4 +88,4 @@ android:layout_margin="@dimen/fab_margin" android:clickable="true"/> -</android.support.design.widget.CoordinatorLayout> +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/tests/MemoryUsage/src/com/android/tests/memoryusage/MemoryUsageTest.java b/tests/MemoryUsage/src/com/android/tests/memoryusage/MemoryUsageTest.java index 1ae318a96a50..653282d0d365 100644 --- a/tests/MemoryUsage/src/com/android/tests/memoryusage/MemoryUsageTest.java +++ b/tests/MemoryUsage/src/com/android/tests/memoryusage/MemoryUsageTest.java @@ -18,7 +18,9 @@ package com.android.tests.memoryusage; import android.app.ActivityManager; import android.app.ActivityManager.ProcessErrorStateInfo; import android.app.ActivityManager.RunningAppProcessInfo; +import android.app.ActivityTaskManager; import android.app.IActivityManager; +import android.app.IActivityTaskManager; import android.app.UiAutomation; import android.content.Context; import android.content.Intent; @@ -66,6 +68,7 @@ public class MemoryUsageTest extends InstrumentationTestCase { private Map<String, String> mNameToResultKey; private Set<String> mPersistentProcesses; private IActivityManager mAm; + private IActivityTaskManager mAtm; @Override protected void setUp() throws Exception { @@ -84,6 +87,7 @@ public class MemoryUsageTest extends InstrumentationTestCase { (MemoryUsageInstrumentation) getInstrumentation(); Bundle args = instrumentation.getBundle(); mAm = ActivityManager.getService(); + mAtm = ActivityTaskManager.getService(); createMappings(); parseArgs(args); @@ -316,7 +320,7 @@ public class MemoryUsageTest extends InstrumentationTestCase { UserHandle.USER_CURRENT); } - mAm.startActivityAndWait(null, null, mLaunchIntent, mimeType, + mAtm.startActivityAndWait(null, null, mLaunchIntent, mimeType, null, null, 0, mLaunchIntent.getFlags(), null, null, UserHandle.USER_CURRENT_OR_SELF); } catch (RemoteException e) { diff --git a/tests/NativeProcessesMemoryTest/Android.bp b/tests/NativeProcessesMemoryTest/Android.bp new file mode 100644 index 000000000000..f2625bf2db11 --- /dev/null +++ b/tests/NativeProcessesMemoryTest/Android.bp @@ -0,0 +1,21 @@ +// Copyright (C) 2018 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +java_test_host { + name: "native-processes-memory-test", + srcs: ["src/**/*.java"], + + libs: ["tradefed"], + test_suites: ["general-tests"], +} diff --git a/tests/NativeProcessesMemoryTest/AndroidTest.xml b/tests/NativeProcessesMemoryTest/AndroidTest.xml new file mode 100644 index 000000000000..0f326fd0bd98 --- /dev/null +++ b/tests/NativeProcessesMemoryTest/AndroidTest.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<configuration description="Runs the native processes memory tests"> + <option name="test-suite-tag" value="native-processes-memory-test" /> + <test class="com.android.tradefed.testtype.HostTest" > + <option name="class" value="com.android.tests.nativeprocesses.NativeProcessesMemoryTest" /> + </test> +</configuration> diff --git a/tests/NativeProcessesMemoryTest/src/com/android/tests/nativeprocesses/NativeProcessesMemoryTest.java b/tests/NativeProcessesMemoryTest/src/com/android/tests/nativeprocesses/NativeProcessesMemoryTest.java new file mode 100644 index 000000000000..51302cea730a --- /dev/null +++ b/tests/NativeProcessesMemoryTest/src/com/android/tests/nativeprocesses/NativeProcessesMemoryTest.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.tests.nativeprocesses; + +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.log.LogUtil.CLog; +import com.android.tradefed.result.ByteArrayInputStreamSource; +import com.android.tradefed.result.LogDataType; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestMetrics; +import com.android.tradefed.testtype.IDeviceTest; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.InputMismatchException; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Scanner; + +/** + * Test to gather native processes count and memory usage. + * + * Native processes are parsed from dumpsys meminfo --oom -c + * + * <pre> + * time,35857009,35857009 + * oom,native,331721,N/A + * proc,native,init,1,2715,N/A,e + * proc,native,init,445,1492,N/A,e + * ... + * </pre> + * + * For each native process we also look at its showmap output, and gather the PSS, VSS, and RSS. + * The showmap output is also saved to test logs. + * + * The metrics reported are: + * + * <pre> + * - number of native processes + * - total memory use of native processes + * - memory usage of each native process (one measurement for each process) + * </pre> + */ +@RunWith(DeviceJUnit4ClassRunner.class) +public class NativeProcessesMemoryTest implements IDeviceTest { + + @Rule public TestMetrics metrics = new TestMetrics(); + @Rule public TestLogData logs = new TestLogData(); + + // the dumpsys process comes and go as we run this test, changing pids, so ignore it + private static final List<String> PROCESSES_TO_IGNORE = Arrays.asList("dumpsys"); + + // -c gives us a compact output which is comma separated + private static final String DUMPSYS_MEMINFO_OOM_CMD = "dumpsys meminfo --oom -c"; + + private static final String SEPARATOR = ","; + private static final String LINE_SEPARATOR = "\\n"; + + // key used to report the number of native processes + private static final String NUM_NATIVE_PROCESSES_KEY = "Num_native_processes"; + + // identity for summing over MemoryMetric + private final MemoryMetric mZero = new MemoryMetric(0, 0, 0); + + private ITestDevice mTestDevice; + + @Test + public void run() throws DeviceNotAvailableException { + // showmap requires root, we enable it here for the rest of the test + getDevice().enableAdbRoot(); + // process name -> list of pids with that name + Map<String, List<String>> nativeProcesses = collectNativeProcesses(); + sampleAndLogAllProcesses(nativeProcesses); + + // want to also record the number of native processes + metrics.addTestMetric( + NUM_NATIVE_PROCESSES_KEY, Integer.toString(nativeProcesses.size())); + } + + /** Samples memory of all processes and logs the memory use. */ + private void sampleAndLogAllProcesses(Map<String, List<String>> nativeProcesses) + throws DeviceNotAvailableException { + for (Map.Entry<String, List<String>> entry : nativeProcesses.entrySet()) { + String processName = entry.getKey(); + if (PROCESSES_TO_IGNORE.contains(processName)) { + continue; + } + + // for all pids associated with this process name, record their memory usage + List<MemoryMetric> allMemsForProcess = new ArrayList<>(); + for (String pid : entry.getValue()) { + Optional<MemoryMetric> mem = snapMemoryUsage(processName, pid); + if (mem.isPresent()) { + allMemsForProcess.add(mem.get()); + } + } + + // if for some reason a process does not have any MemoryMetric, don't log it + if (allMemsForProcess.isEmpty()) { + continue; + } + + // combine all the memory metrics of process with the same name + MemoryMetric combined = allMemsForProcess.stream().reduce(mZero, MemoryMetric::sum); + logMemoryMetric(processName, combined); + } + } + + @Override + public void setDevice(ITestDevice device) { + mTestDevice = device; + } + + @Override + public ITestDevice getDevice() { + return mTestDevice; + } + + /** + * Query system for a list of native process. + * + * @return a map from process name to a list of pids that share the same name + */ + private Map<String, List<String>> collectNativeProcesses() throws DeviceNotAvailableException { + HashMap<String, List<String>> nativeProcesses = new HashMap<>(); + String memInfo = getDevice().executeShellCommand(DUMPSYS_MEMINFO_OOM_CMD); + + for (String line : memInfo.split(LINE_SEPARATOR)) { + String[] splits = line.split(SEPARATOR); + // ignore lines that don't list a native process + if (splits.length < 4 || !splits[0].equals("proc") || !splits[1].equals("native")) { + continue; + } + + String processName = splits[2]; + String pid = splits[3]; + if (nativeProcesses.containsKey(processName)) { + nativeProcesses.get(processName).add(pid); + } else { + nativeProcesses.put(processName, new ArrayList<>(Arrays.asList(pid))); + } + } + return nativeProcesses; + } + + /** Logs an entire showmap output to the test logs. */ + private void logShowmap(String label, String showmap) { + try (ByteArrayInputStreamSource source = + new ByteArrayInputStreamSource(showmap.getBytes())) { + logs.addTestLog(label + "_showmap", LogDataType.TEXT, source); + } + } + + /** + * Extract VSS, PSS, and RSS from showmap of a process. + * The showmap output is also added to test logs. + */ + private Optional<MemoryMetric> snapMemoryUsage(String processName, String pid) + throws DeviceNotAvailableException { + // TODO(zhin): copied from com.android.tests.sysmem.host.Metrics#sample(), extract? + String showmap = getDevice().executeShellCommand("showmap " + pid); + logShowmap(processName + "_" + pid, showmap); + + // CHECKSTYLE:OFF Generated code + // The last lines of the showmap output looks like: + // ------- -------- -------- -------- -------- -------- -------- -------- -------- ---- ------------------------------ + // virtual shared shared private private + // size RSS PSS clean dirty clean dirty swap swapPSS # object + // -------- -------- -------- -------- -------- -------- -------- -------- -------- ---- ------------------------------ + // 12848 4240 1543 2852 64 36 1288 0 0 171 TOTAL + // CHECKSTYLE:ON Generated code + try { + int pos = showmap.lastIndexOf("----"); + Scanner sc = new Scanner(showmap.substring(pos)); + sc.next(); + long vss = sc.nextLong(); + long rss = sc.nextLong(); + long pss = sc.nextLong(); + return Optional.of(new MemoryMetric(pss, rss, vss)); + } catch (InputMismatchException e) { + // this might occur if we have transient processes, it was collected earlier, + // but by the time we look at showmap the process is gone + CLog.e("Unable to parse MemoryMetric from showmap of pid: " + pid + " processName: " + + processName); + CLog.e(showmap); + return Optional.empty(); + } + } + + /** Logs a MemoryMetric of a process. */ + private void logMemoryMetric(String processName, MemoryMetric memoryMetric) { + metrics.addTestMetric(processName + "_pss", Long.toString(memoryMetric.pss)); + metrics.addTestMetric(processName + "_rss", Long.toString(memoryMetric.rss)); + metrics.addTestMetric(processName + "_vss", Long.toString(memoryMetric.vss)); + } + + /** Container of memory numbers we want to log. */ + private final class MemoryMetric { + final long pss; + final long rss; + final long vss; + + MemoryMetric(long pss, long rss, long vss) { + this.pss = pss; + this.rss = rss; + this.vss = vss; + } + + MemoryMetric sum(MemoryMetric other) { + return new MemoryMetric( + pss + other.pss, rss + other.rss, vss + other.vss); + } + } +} diff --git a/tests/PackageWatchdog/Android.bp b/tests/PackageWatchdog/Android.bp new file mode 100644 index 000000000000..88d92c4d94fe --- /dev/null +++ b/tests/PackageWatchdog/Android.bp @@ -0,0 +1,35 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// PackageWatchdogTest +android_test { + name: "PackageWatchdogTest", + srcs: ["src/**/*.java"], + static_libs: [ + "junit", + "mockito-target-extended-minus-junit4", + "frameworks-base-testutils", + "androidx.test.rules", + "services.core", + "services.net", + ], + libs: ["android.test.runner"], + jni_libs: [ + // mockito-target-extended dependencies + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], + platform_apis: true, + test_suites: ["device-tests"], +} diff --git a/tests/PackageWatchdog/AndroidManifest.xml b/tests/PackageWatchdog/AndroidManifest.xml new file mode 100644 index 000000000000..540edb41f66f --- /dev/null +++ b/tests/PackageWatchdog/AndroidManifest.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.tests.packagewatchdog" > + + <application android:debuggable="true"> + <uses-library android:name="android.test.runner" /> + </application> + + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.tests.packagewatchdog" + android:label="PackageWatchdog Test"/> +</manifest> diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java new file mode 100644 index 000000000000..c42201fa0d3e --- /dev/null +++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java @@ -0,0 +1,891 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import static android.service.watchdog.ExplicitHealthCheckService.PackageConfig; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.VersionedPackage; +import android.net.ConnectivityModuleConnector; +import android.net.ConnectivityModuleConnector.ConnectivityModuleHealthListener; +import android.os.Handler; +import android.os.test.TestLooper; +import android.provider.DeviceConfig; +import android.util.AtomicFile; + +import androidx.test.InstrumentationRegistry; + +import com.android.server.PackageWatchdog.MonitoredPackage; +import com.android.server.PackageWatchdog.PackageHealthObserver; +import com.android.server.PackageWatchdog.PackageHealthObserverImpact; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +// TODO: Write test without using PackageWatchdog#getPackages. Just rely on +// behavior of observers receiving crash notifications or not to determine if it's registered +// TODO: Use Truth in tests. +/** + * Test PackageWatchdog. + */ +public class PackageWatchdogTest { + private static final String APP_A = "com.package.a"; + private static final String APP_B = "com.package.b"; + private static final String APP_C = "com.package.c"; + private static final String APP_D = "com.package.d"; + private static final long VERSION_CODE = 1L; + private static final String OBSERVER_NAME_1 = "observer1"; + private static final String OBSERVER_NAME_2 = "observer2"; + private static final String OBSERVER_NAME_3 = "observer3"; + private static final String OBSERVER_NAME_4 = "observer4"; + private static final long SHORT_DURATION = TimeUnit.SECONDS.toMillis(1); + private static final long LONG_DURATION = TimeUnit.SECONDS.toMillis(5); + private TestLooper mTestLooper; + private Context mSpyContext; + @Mock + private ConnectivityModuleConnector mConnectivityModuleConnector; + @Mock + private PackageManager mMockPackageManager; + @Captor + private ArgumentCaptor<ConnectivityModuleHealthListener> mConnectivityModuleCallbackCaptor; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + new File(InstrumentationRegistry.getContext().getFilesDir(), + "package-watchdog.xml").delete(); + adoptShellPermissions(Manifest.permission.READ_DEVICE_CONFIG); + mTestLooper = new TestLooper(); + mSpyContext = spy(InstrumentationRegistry.getContext()); + when(mSpyContext.getPackageManager()).thenReturn(mMockPackageManager); + when(mMockPackageManager.getPackageInfo(anyString(), anyInt())).then(inv -> { + final PackageInfo res = new PackageInfo(); + res.packageName = inv.getArgument(0); + res.setLongVersionCode(VERSION_CODE); + return res; + }); + } + + @After + public void tearDown() throws Exception { + dropShellPermissions(); + } + + /** + * Test registration, unregistration, package expiry and duration reduction + */ + @Test + public void testRegistration() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer1 = new TestObserver(OBSERVER_NAME_1); + TestObserver observer2 = new TestObserver(OBSERVER_NAME_2); + TestObserver observer3 = new TestObserver(OBSERVER_NAME_3); + + // Start observing for observer1 which will be unregistered + watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); + // Start observing for observer2 which will expire + watchdog.startObservingHealth(observer2, Arrays.asList(APP_A, APP_B), SHORT_DURATION); + // Start observing for observer3 which will have expiry duration reduced + watchdog.startObservingHealth(observer3, Arrays.asList(APP_A), LONG_DURATION); + + // Verify packages observed at start + // 1 + assertEquals(1, watchdog.getPackages(observer1).size()); + assertTrue(watchdog.getPackages(observer1).contains(APP_A)); + // 2 + assertEquals(2, watchdog.getPackages(observer2).size()); + assertTrue(watchdog.getPackages(observer2).contains(APP_A)); + assertTrue(watchdog.getPackages(observer2).contains(APP_B)); + // 3 + assertEquals(1, watchdog.getPackages(observer3).size()); + assertTrue(watchdog.getPackages(observer3).contains(APP_A)); + + // Then unregister observer1 + watchdog.unregisterHealthObserver(observer1); + + // Verify observer2 and observer3 left + // 1 + assertNull(watchdog.getPackages(observer1)); + // 2 + assertEquals(2, watchdog.getPackages(observer2).size()); + assertTrue(watchdog.getPackages(observer2).contains(APP_A)); + assertTrue(watchdog.getPackages(observer2).contains(APP_B)); + // 3 + assertEquals(1, watchdog.getPackages(observer3).size()); + assertTrue(watchdog.getPackages(observer3).contains(APP_A)); + + // Then advance time a little and run messages in Handlers so observer2 expires + Thread.sleep(SHORT_DURATION); + mTestLooper.dispatchAll(); + + // Verify observer3 left with reduced expiry duration + // 1 + assertNull(watchdog.getPackages(observer1)); + // 2 + assertNull(watchdog.getPackages(observer2)); + // 3 + assertEquals(1, watchdog.getPackages(observer3).size()); + assertTrue(watchdog.getPackages(observer3).contains(APP_A)); + + // Then advance time some more and run messages in Handlers so observer3 expires + Thread.sleep(LONG_DURATION); + mTestLooper.dispatchAll(); + + // Verify observer3 expired + // 1 + assertNull(watchdog.getPackages(observer1)); + // 2 + assertNull(watchdog.getPackages(observer2)); + // 3 + assertNull(watchdog.getPackages(observer3)); + } + + /** Observing already observed package extends the observation time. */ + @Test + public void testObserveAlreadyObservedPackage() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer = new TestObserver(OBSERVER_NAME_1); + + // Start observing APP_A + watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION); + + // Then advance time half-way + Thread.sleep(SHORT_DURATION / 2); + mTestLooper.dispatchAll(); + + // Start observing APP_A again + watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION); + + // Then advance time such that it should have expired were it not for the second observation + Thread.sleep((SHORT_DURATION / 2) + 1); + mTestLooper.dispatchAll(); + + // Verify that APP_A not expired since second observation extended the time + assertEquals(1, watchdog.getPackages(observer).size()); + assertTrue(watchdog.getPackages(observer).contains(APP_A)); + } + + /** + * Test package observers are persisted and loaded on startup + */ + @Test + public void testPersistence() throws Exception { + PackageWatchdog watchdog1 = createWatchdog(); + TestObserver observer1 = new TestObserver(OBSERVER_NAME_1); + TestObserver observer2 = new TestObserver(OBSERVER_NAME_2); + + watchdog1.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); + watchdog1.startObservingHealth(observer2, Arrays.asList(APP_A, APP_B), SHORT_DURATION); + + // Verify 2 observers are registered and saved internally + // 1 + assertEquals(1, watchdog1.getPackages(observer1).size()); + assertTrue(watchdog1.getPackages(observer1).contains(APP_A)); + // 2 + assertEquals(2, watchdog1.getPackages(observer2).size()); + assertTrue(watchdog1.getPackages(observer2).contains(APP_A)); + assertTrue(watchdog1.getPackages(observer2).contains(APP_B)); + + // Then advance time and run IO Handler so file is saved + mTestLooper.dispatchAll(); + + // Then start a new watchdog + PackageWatchdog watchdog2 = createWatchdog(); + + // Verify the new watchdog loads observers on startup but nothing registered + assertEquals(0, watchdog2.getPackages(observer1).size()); + assertEquals(0, watchdog2.getPackages(observer2).size()); + // Verify random observer not saved returns null + assertNull(watchdog2.getPackages(new TestObserver(OBSERVER_NAME_3))); + + // Then register observer1 + watchdog2.registerHealthObserver(observer1); + watchdog2.registerHealthObserver(observer2); + + // Verify 2 observers are registered after reload + // 1 + assertEquals(1, watchdog1.getPackages(observer1).size()); + assertTrue(watchdog1.getPackages(observer1).contains(APP_A)); + // 2 + assertEquals(2, watchdog1.getPackages(observer2).size()); + assertTrue(watchdog1.getPackages(observer2).contains(APP_A)); + assertTrue(watchdog1.getPackages(observer2).contains(APP_B)); + } + + /** + * Test package failure under threshold does not notify observers + */ + @Test + public void testNoPackageFailureBeforeThreshold() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer1 = new TestObserver(OBSERVER_NAME_1); + TestObserver observer2 = new TestObserver(OBSERVER_NAME_2); + + watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); + + // Then fail APP_A below the threshold + for (int i = 0; i < watchdog.getTriggerFailureCount() - 1; i++) { + watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE))); + } + + // Run handler so package failures are dispatched to observers + mTestLooper.dispatchAll(); + + // Verify that observers are not notified + assertEquals(0, observer1.mFailedPackages.size()); + assertEquals(0, observer2.mFailedPackages.size()); + } + + /** + * Test package failure and does not notify any observer because they are not observing + * the failed packages. + */ + @Test + public void testPackageFailureDifferentPackageNotifyNone() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer1 = new TestObserver(OBSERVER_NAME_1); + TestObserver observer2 = new TestObserver(OBSERVER_NAME_2); + + + watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.startObservingHealth(observer1, Arrays.asList(APP_B), SHORT_DURATION); + + // Then fail APP_C (not observed) above the threshold + for (int i = 0; i < watchdog.getTriggerFailureCount(); i++) { + watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_C, VERSION_CODE))); + } + + // Run handler so package failures are dispatched to observers + mTestLooper.dispatchAll(); + + // Verify that observers are not notified + assertEquals(0, observer1.mFailedPackages.size()); + assertEquals(0, observer2.mFailedPackages.size()); + } + + /** + * Test package failure and does not notify any observer because the failed package version + * does not match the available rollback-from-version. + */ + @Test + public void testPackageFailureDifferentVersionNotifyNone() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + long differentVersionCode = 2L; + TestObserver observer = new TestObserver(OBSERVER_NAME_1) { + @Override + public int onHealthCheckFailed(VersionedPackage versionedPackage) { + if (versionedPackage.getVersionCode() == VERSION_CODE) { + // Only rollback for specific versionCode + return PackageHealthObserverImpact.USER_IMPACT_MEDIUM; + } + return PackageHealthObserverImpact.USER_IMPACT_NONE; + } + }; + + watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION); + + // Then fail APP_A (different version) above the threshold + for (int i = 0; i < watchdog.getTriggerFailureCount(); i++) { + watchdog.onPackageFailure(Arrays.asList( + new VersionedPackage(APP_A, differentVersionCode))); + } + + // Run handler so package failures are dispatched to observers + mTestLooper.dispatchAll(); + + // Verify that observers are not notified + assertEquals(0, observer.mFailedPackages.size()); + } + + + /** + * Test package failure and notifies only least impact observers. + */ + @Test + public void testPackageFailureNotifyAllDifferentImpacts() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observerNone = new TestObserver(OBSERVER_NAME_1, + PackageHealthObserverImpact.USER_IMPACT_NONE); + TestObserver observerHigh = new TestObserver(OBSERVER_NAME_2, + PackageHealthObserverImpact.USER_IMPACT_HIGH); + TestObserver observerMid = new TestObserver(OBSERVER_NAME_3, + PackageHealthObserverImpact.USER_IMPACT_MEDIUM); + TestObserver observerLow = new TestObserver(OBSERVER_NAME_4, + PackageHealthObserverImpact.USER_IMPACT_LOW); + + // Start observing for all impact observers + watchdog.startObservingHealth(observerNone, Arrays.asList(APP_A, APP_B, APP_C, APP_D), + SHORT_DURATION); + watchdog.startObservingHealth(observerHigh, Arrays.asList(APP_A, APP_B, APP_C), + SHORT_DURATION); + watchdog.startObservingHealth(observerMid, Arrays.asList(APP_A, APP_B), + SHORT_DURATION); + watchdog.startObservingHealth(observerLow, Arrays.asList(APP_A), + SHORT_DURATION); + + // Then fail all apps above the threshold + for (int i = 0; i < watchdog.getTriggerFailureCount(); i++) { + watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE), + new VersionedPackage(APP_B, VERSION_CODE), + new VersionedPackage(APP_C, VERSION_CODE), + new VersionedPackage(APP_D, VERSION_CODE))); + } + + // Run handler so package failures are dispatched to observers + mTestLooper.dispatchAll(); + + // Verify least impact observers are notifed of package failures + List<String> observerNonePackages = observerNone.mFailedPackages; + List<String> observerHighPackages = observerHigh.mFailedPackages; + List<String> observerMidPackages = observerMid.mFailedPackages; + List<String> observerLowPackages = observerLow.mFailedPackages; + + // APP_D failure observed by only observerNone is not caught cos its impact is none + assertEquals(0, observerNonePackages.size()); + // APP_C failure is caught by observerHigh cos it's the lowest impact observer + assertEquals(1, observerHighPackages.size()); + assertEquals(APP_C, observerHighPackages.get(0)); + // APP_B failure is caught by observerMid cos it's the lowest impact observer + assertEquals(1, observerMidPackages.size()); + assertEquals(APP_B, observerMidPackages.get(0)); + // APP_A failure is caught by observerLow cos it's the lowest impact observer + assertEquals(1, observerLowPackages.size()); + assertEquals(APP_A, observerLowPackages.get(0)); + } + + /** + * Test package failure and least impact observers are notified successively. + * State transistions: + * + * <ul> + * <li>(observer1:low, observer2:mid) -> {observer1} + * <li>(observer1:high, observer2:mid) -> {observer2} + * <li>(observer1:high, observer2:none) -> {observer1} + * <li>(observer1:none, observer2:none) -> {} + * <ul> + */ + @Test + public void testPackageFailureNotifyLeastImpactSuccessively() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observerFirst = new TestObserver(OBSERVER_NAME_1, + PackageHealthObserverImpact.USER_IMPACT_LOW); + TestObserver observerSecond = new TestObserver(OBSERVER_NAME_2, + PackageHealthObserverImpact.USER_IMPACT_MEDIUM); + + // Start observing for observerFirst and observerSecond with failure handling + watchdog.startObservingHealth(observerFirst, Arrays.asList(APP_A), LONG_DURATION); + watchdog.startObservingHealth(observerSecond, Arrays.asList(APP_A), LONG_DURATION); + + // Then fail APP_A above the threshold + for (int i = 0; i < watchdog.getTriggerFailureCount(); i++) { + watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE))); + } + // Run handler so package failures are dispatched to observers + mTestLooper.dispatchAll(); + + // Verify only observerFirst is notifed + assertEquals(1, observerFirst.mFailedPackages.size()); + assertEquals(APP_A, observerFirst.mFailedPackages.get(0)); + assertEquals(0, observerSecond.mFailedPackages.size()); + + // After observerFirst handles failure, next action it has is high impact + observerFirst.mImpact = PackageHealthObserverImpact.USER_IMPACT_HIGH; + observerFirst.mFailedPackages.clear(); + observerSecond.mFailedPackages.clear(); + + // Then fail APP_A again above the threshold + for (int i = 0; i < watchdog.getTriggerFailureCount(); i++) { + watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE))); + } + // Run handler so package failures are dispatched to observers + mTestLooper.dispatchAll(); + + // Verify only observerSecond is notifed cos it has least impact + assertEquals(1, observerSecond.mFailedPackages.size()); + assertEquals(APP_A, observerSecond.mFailedPackages.get(0)); + assertEquals(0, observerFirst.mFailedPackages.size()); + + // After observerSecond handles failure, it has no further actions + observerSecond.mImpact = PackageHealthObserverImpact.USER_IMPACT_NONE; + observerFirst.mFailedPackages.clear(); + observerSecond.mFailedPackages.clear(); + + // Then fail APP_A again above the threshold + for (int i = 0; i < watchdog.getTriggerFailureCount(); i++) { + watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE))); + } + // Run handler so package failures are dispatched to observers + mTestLooper.dispatchAll(); + + // Verify only observerFirst is notifed cos it has the only action + assertEquals(1, observerFirst.mFailedPackages.size()); + assertEquals(APP_A, observerFirst.mFailedPackages.get(0)); + assertEquals(0, observerSecond.mFailedPackages.size()); + + // After observerFirst handles failure, it too has no further actions + observerFirst.mImpact = PackageHealthObserverImpact.USER_IMPACT_NONE; + observerFirst.mFailedPackages.clear(); + observerSecond.mFailedPackages.clear(); + + // Then fail APP_A again above the threshold + for (int i = 0; i < watchdog.getTriggerFailureCount(); i++) { + watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE))); + } + // Run handler so package failures are dispatched to observers + mTestLooper.dispatchAll(); + + // Verify no observer is notified cos no actions left + assertEquals(0, observerFirst.mFailedPackages.size()); + assertEquals(0, observerSecond.mFailedPackages.size()); + } + + /** + * Test package failure and notifies only one observer even with observer impact tie. + */ + @Test + public void testPackageFailureNotifyOneSameImpact() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer1 = new TestObserver(OBSERVER_NAME_1, + PackageHealthObserverImpact.USER_IMPACT_HIGH); + TestObserver observer2 = new TestObserver(OBSERVER_NAME_2, + PackageHealthObserverImpact.USER_IMPACT_HIGH); + + // Start observing for observer1 and observer2 with failure handling + watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); + + // Then fail APP_A above the threshold + for (int i = 0; i < watchdog.getTriggerFailureCount(); i++) { + watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE))); + } + + // Run handler so package failures are dispatched to observers + mTestLooper.dispatchAll(); + + // Verify only one observer is notifed + assertEquals(1, observer1.mFailedPackages.size()); + assertEquals(APP_A, observer1.mFailedPackages.get(0)); + assertEquals(0, observer2.mFailedPackages.size()); + } + + /** + * Test package passing explicit health checks does not fail and vice versa. + */ + @Test + public void testExplicitHealthChecks() throws Exception { + TestController controller = new TestController(); + PackageWatchdog watchdog = createWatchdog(controller, true /* withPackagesReady */); + TestObserver observer1 = new TestObserver(OBSERVER_NAME_1, + PackageHealthObserverImpact.USER_IMPACT_HIGH); + TestObserver observer2 = new TestObserver(OBSERVER_NAME_2, + PackageHealthObserverImpact.USER_IMPACT_HIGH); + TestObserver observer3 = new TestObserver(OBSERVER_NAME_3, + PackageHealthObserverImpact.USER_IMPACT_HIGH); + + + // Start observing with explicit health checks for APP_A and APP_B respectively + // with observer1 and observer2 + controller.setSupportedPackages(Arrays.asList(APP_A, APP_B)); + watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.startObservingHealth(observer2, Arrays.asList(APP_B), SHORT_DURATION); + + // Run handler so requests are dispatched to the controller + mTestLooper.dispatchAll(); + + // Verify we requested health checks for APP_A and APP_B + List<String> requestedPackages = controller.getRequestedPackages(); + assertEquals(2, requestedPackages.size()); + assertEquals(APP_A, requestedPackages.get(0)); + assertEquals(APP_B, requestedPackages.get(1)); + + // Then health check passed for APP_A (observer1 is aware) + controller.setPackagePassed(APP_A); + + // Then start observing APP_A with explicit health checks for observer3. + // Observer3 didn't exist when we got the explicit health check above, so + // it starts out with a non-passing explicit health check and has to wait for a pass + // otherwise it would be notified of APP_A failure on expiry + watchdog.startObservingHealth(observer3, Arrays.asList(APP_A), SHORT_DURATION); + + // Then expire observers + Thread.sleep(SHORT_DURATION); + // Run handler so package failures are dispatched to observers + mTestLooper.dispatchAll(); + + // Verify we cancelled all requests on expiry + assertEquals(0, controller.getRequestedPackages().size()); + + // Verify observer1 is not notified + assertEquals(0, observer1.mFailedPackages.size()); + + // Verify observer2 is notifed because health checks for APP_B never passed + assertEquals(1, observer2.mFailedPackages.size()); + assertEquals(APP_B, observer2.mFailedPackages.get(0)); + + // Verify observer3 is notifed because health checks for APP_A did not pass before expiry + assertEquals(1, observer3.mFailedPackages.size()); + assertEquals(APP_A, observer3.mFailedPackages.get(0)); + } + + /** + * Test explicit health check state can be disabled and enabled correctly. + */ + @Test + public void testExplicitHealthCheckStateChanges() throws Exception { + adoptShellPermissions( + Manifest.permission.WRITE_DEVICE_CONFIG, + Manifest.permission.READ_DEVICE_CONFIG); + + TestController controller = new TestController(); + PackageWatchdog watchdog = createWatchdog(controller, true /* withPackagesReady */); + TestObserver observer = new TestObserver(OBSERVER_NAME_1, + PackageHealthObserverImpact.USER_IMPACT_MEDIUM); + + // Start observing with explicit health checks for APP_A and APP_B + controller.setSupportedPackages(Arrays.asList(APP_A, APP_B, APP_C)); + watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.startObservingHealth(observer, Arrays.asList(APP_B), LONG_DURATION); + + // Run handler so requests are dispatched to the controller + mTestLooper.dispatchAll(); + + // Verify we requested health checks for APP_A and APP_B + List<String> requestedPackages = controller.getRequestedPackages(); + assertEquals(2, requestedPackages.size()); + assertEquals(APP_A, requestedPackages.get(0)); + assertEquals(APP_B, requestedPackages.get(1)); + + // Disable explicit health checks (marks APP_A and APP_B as passed) + setExplicitHealthCheckEnabled(false); + + // Run handler so requests/cancellations are dispatched to the controller + mTestLooper.dispatchAll(); + + // Verify all checks are cancelled + assertEquals(0, controller.getRequestedPackages().size()); + + // Then expire APP_A + Thread.sleep(SHORT_DURATION); + mTestLooper.dispatchAll(); + + // Verify APP_A is not failed (APP_B) is not expired yet + assertEquals(0, observer.mFailedPackages.size()); + + // Re-enable explicit health checks + setExplicitHealthCheckEnabled(true); + + // Run handler so requests/cancellations are dispatched to the controller + mTestLooper.dispatchAll(); + + // Verify no requests are made cos APP_A is expired and APP_B was marked as passed + assertEquals(0, controller.getRequestedPackages().size()); + + // Then set new supported packages + controller.setSupportedPackages(Arrays.asList(APP_C)); + // Start observing APP_A and APP_C; only APP_C has support for explicit health checks + watchdog.startObservingHealth(observer, Arrays.asList(APP_A, APP_C), SHORT_DURATION); + + // Run handler so requests/cancellations are dispatched to the controller + mTestLooper.dispatchAll(); + + // Verify requests are only made for APP_C + requestedPackages = controller.getRequestedPackages(); + assertEquals(1, requestedPackages.size()); + assertEquals(APP_C, requestedPackages.get(0)); + + // Then expire APP_A and APP_C + Thread.sleep(SHORT_DURATION); + mTestLooper.dispatchAll(); + + // Verify only APP_C is failed because explicit health checks was not supported for APP_A + assertEquals(1, observer.mFailedPackages.size()); + assertEquals(APP_C, observer.mFailedPackages.get(0)); + } + + /** + * Tests failure when health check duration is different from package observation duration + * Failure is also notified only once. + */ + @Test + public void testExplicitHealthCheckFailureBeforeExpiry() throws Exception { + TestController controller = new TestController(); + PackageWatchdog watchdog = createWatchdog(controller, true /* withPackagesReady */); + TestObserver observer = new TestObserver(OBSERVER_NAME_1, + PackageHealthObserverImpact.USER_IMPACT_MEDIUM); + + // Start observing with explicit health checks for APP_A and + // package observation duration == LONG_DURATION + // health check duration == SHORT_DURATION (set by default in the TestController) + controller.setSupportedPackages(Arrays.asList(APP_A)); + watchdog.startObservingHealth(observer, Arrays.asList(APP_A), LONG_DURATION); + + // Then APP_A has exceeded health check duration + Thread.sleep(SHORT_DURATION); + mTestLooper.dispatchAll(); + + // Verify that health check is failed + assertEquals(1, observer.mFailedPackages.size()); + assertEquals(APP_A, observer.mFailedPackages.get(0)); + + // Then clear failed packages and start observing a random package so requests are synced + // and PackageWatchdog#onSupportedPackages is called and APP_A has a chance to fail again + // this time due to package expiry. + observer.mFailedPackages.clear(); + watchdog.startObservingHealth(observer, Arrays.asList(APP_B), LONG_DURATION); + + // Verify that health check failure is not notified again + assertTrue(observer.mFailedPackages.isEmpty()); + } + + /** Tests {@link MonitoredPackage} health check state transitions. */ + @Test + public void testPackageHealthCheckStateTransitions() { + TestController controller = new TestController(); + PackageWatchdog wd = createWatchdog(controller, true /* withPackagesReady */); + MonitoredPackage m1 = wd.new MonitoredPackage(APP_A, LONG_DURATION, + false /* hasPassedHealthCheck */); + MonitoredPackage m2 = wd.new MonitoredPackage(APP_B, LONG_DURATION, false); + MonitoredPackage m3 = wd.new MonitoredPackage(APP_C, LONG_DURATION, false); + MonitoredPackage m4 = wd.new MonitoredPackage(APP_D, LONG_DURATION, SHORT_DURATION, true); + + // Verify transition: inactive -> active -> passed + // Verify initially inactive + assertEquals(MonitoredPackage.STATE_INACTIVE, m1.getHealthCheckStateLocked()); + // Verify still inactive, until we #setHealthCheckActiveLocked + assertEquals(MonitoredPackage.STATE_INACTIVE, m1.handleElapsedTimeLocked(SHORT_DURATION)); + // Verify now active + assertEquals(MonitoredPackage.STATE_ACTIVE, m1.setHealthCheckActiveLocked(SHORT_DURATION)); + // Verify now passed + assertEquals(MonitoredPackage.STATE_PASSED, m1.tryPassHealthCheckLocked()); + + // Verify transition: inactive -> active -> failed + // Verify initially inactive + assertEquals(MonitoredPackage.STATE_INACTIVE, m2.getHealthCheckStateLocked()); + // Verify now active + assertEquals(MonitoredPackage.STATE_ACTIVE, m2.setHealthCheckActiveLocked(SHORT_DURATION)); + // Verify now failed + assertEquals(MonitoredPackage.STATE_FAILED, m2.handleElapsedTimeLocked(SHORT_DURATION)); + + // Verify transition: inactive -> failed + // Verify initially inactive + assertEquals(MonitoredPackage.STATE_INACTIVE, m3.getHealthCheckStateLocked()); + // Verify now failed because package expired + assertEquals(MonitoredPackage.STATE_FAILED, m3.handleElapsedTimeLocked(LONG_DURATION)); + // Verify remains failed even when asked to pass + assertEquals(MonitoredPackage.STATE_FAILED, m3.tryPassHealthCheckLocked()); + + // Verify transition: passed + assertEquals(MonitoredPackage.STATE_PASSED, m4.getHealthCheckStateLocked()); + // Verify remains passed even if health check fails + assertEquals(MonitoredPackage.STATE_PASSED, m4.handleElapsedTimeLocked(SHORT_DURATION)); + // Verify remains passed even if package expires + assertEquals(MonitoredPackage.STATE_PASSED, m4.handleElapsedTimeLocked(LONG_DURATION)); + } + + @Test + public void testNetworkStackFailure() { + final PackageWatchdog wd = createWatchdog(); + + // Start observing with failure handling + TestObserver observer = new TestObserver(OBSERVER_NAME_1, + PackageHealthObserverImpact.USER_IMPACT_HIGH); + wd.startObservingHealth(observer, Collections.singletonList(APP_A), SHORT_DURATION); + + // Notify of NetworkStack failure + mConnectivityModuleCallbackCaptor.getValue().onNetworkStackFailure(APP_A); + + // Run handler so package failures are dispatched to observers + mTestLooper.dispatchAll(); + + // Verify the NetworkStack observer is notified + assertEquals(1, observer.mFailedPackages.size()); + assertEquals(APP_A, observer.mFailedPackages.get(0)); + } + + private void adoptShellPermissions(String... permissions) { + InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity(permissions); + } + + private void dropShellPermissions() { + InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .dropShellPermissionIdentity(); + } + + private void setExplicitHealthCheckEnabled(boolean enabled) { + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK, + PackageWatchdog.PROPERTY_WATCHDOG_EXPLICIT_HEALTH_CHECK_ENABLED, + Boolean.toString(enabled), /*makeDefault*/false); + //give time for DeviceConfig to broadcast the property value change + try { + Thread.sleep(SHORT_DURATION); + } catch (InterruptedException e) { + fail("Thread.sleep unexpectedly failed!"); + } + } + + private PackageWatchdog createWatchdog() { + return createWatchdog(new TestController(), true /* withPackagesReady */); + } + + private PackageWatchdog createWatchdog(TestController controller, boolean withPackagesReady) { + AtomicFile policyFile = + new AtomicFile(new File(mSpyContext.getFilesDir(), "package-watchdog.xml")); + Handler handler = new Handler(mTestLooper.getLooper()); + PackageWatchdog watchdog = + new PackageWatchdog(mSpyContext, policyFile, handler, handler, controller, + mConnectivityModuleConnector); + // Verify controller is not automatically started + assertFalse(controller.mIsEnabled); + if (withPackagesReady) { + // Only capture the NetworkStack callback for the latest registered watchdog + reset(mConnectivityModuleConnector); + watchdog.onPackagesReady(); + // Verify controller by default is started when packages are ready + assertTrue(controller.mIsEnabled); + + verify(mConnectivityModuleConnector).registerHealthListener( + mConnectivityModuleCallbackCaptor.capture()); + } + return watchdog; + } + + private static class TestObserver implements PackageHealthObserver { + private final String mName; + private int mImpact; + final List<String> mFailedPackages = new ArrayList<>(); + + TestObserver(String name) { + mName = name; + mImpact = PackageHealthObserverImpact.USER_IMPACT_MEDIUM; + } + + TestObserver(String name, int impact) { + mName = name; + mImpact = impact; + } + + public int onHealthCheckFailed(VersionedPackage versionedPackage) { + return mImpact; + } + + public boolean execute(VersionedPackage versionedPackage) { + mFailedPackages.add(versionedPackage.getPackageName()); + return true; + } + + public String getName() { + return mName; + } + } + + private static class TestController extends ExplicitHealthCheckController { + TestController() { + super(null /* controller */); + } + + private boolean mIsEnabled; + private List<String> mSupportedPackages = new ArrayList<>(); + private List<String> mRequestedPackages = new ArrayList<>(); + private Consumer<String> mPassedConsumer; + private Consumer<List<PackageConfig>> mSupportedConsumer; + private Runnable mNotifySyncRunnable; + + @Override + public void setEnabled(boolean enabled) { + mIsEnabled = enabled; + if (!mIsEnabled) { + mSupportedPackages.clear(); + } + } + + @Override + public void setCallbacks(Consumer<String> passedConsumer, + Consumer<List<PackageConfig>> supportedConsumer, Runnable notifySyncRunnable) { + mPassedConsumer = passedConsumer; + mSupportedConsumer = supportedConsumer; + mNotifySyncRunnable = notifySyncRunnable; + } + + @Override + public void syncRequests(Set<String> packages) { + mRequestedPackages.clear(); + if (mIsEnabled) { + packages.retainAll(mSupportedPackages); + mRequestedPackages.addAll(packages); + List<PackageConfig> packageConfigs = new ArrayList<>(); + for (String packageName: packages) { + packageConfigs.add(new PackageConfig(packageName, SHORT_DURATION)); + } + mSupportedConsumer.accept(packageConfigs); + } else { + mSupportedConsumer.accept(Collections.emptyList()); + } + } + + public void setSupportedPackages(List<String> packages) { + mSupportedPackages.clear(); + mSupportedPackages.addAll(packages); + } + + public void setPackagePassed(String packageName) { + mPassedConsumer.accept(packageName); + } + + public List<String> getRequestedPackages() { + if (mIsEnabled) { + return mRequestedPackages; + } else { + return Collections.emptyList(); + } + } + } +} diff --git a/tests/ProtoInputStreamTests/Android.mk b/tests/ProtoInputStreamTests/Android.mk new file mode 100644 index 000000000000..eb747cc2cdcc --- /dev/null +++ b/tests/ProtoInputStreamTests/Android.mk @@ -0,0 +1,34 @@ +# Copyright (C) 2019 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_PACKAGE_NAME := ProtoInputStreamTests +LOCAL_PROTOC_OPTIMIZE_TYPE := nano +LOCAL_MODULE_TAGS := tests optional +LOCAL_SRC_FILES := \ + $(call all-java-files-under, src) \ + $(call all-proto-files-under, src) +LOCAL_PRIVATE_PLATFORM_APIS := true +LOCAL_CERTIFICATE := platform +LOCAL_COMPATIBILITY_SUITE := device-tests + +LOCAL_JAVA_LIBRARIES := android.test.runner +LOCAL_STATIC_JAVA_LIBRARIES := \ + androidx.test.rules \ + frameworks-base-testutils \ + mockito-target-minus-junit4 + +include $(BUILD_PACKAGE)
\ No newline at end of file diff --git a/tests/ProtoInputStreamTests/AndroidManifest.xml b/tests/ProtoInputStreamTests/AndroidManifest.xml new file mode 100644 index 000000000000..c11aa7393431 --- /dev/null +++ b/tests/ProtoInputStreamTests/AndroidManifest.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.test.protoinputstream"> + <application> + <uses-library android:name="android.test.runner"/> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.test.protoinputstream" + android:label="ProtoInputStream Tests"> + </instrumentation> +</manifest>
\ No newline at end of file diff --git a/tests/ProtoInputStreamTests/AndroidTest.xml b/tests/ProtoInputStreamTests/AndroidTest.xml new file mode 100644 index 000000000000..51ab88e4d4d0 --- /dev/null +++ b/tests/ProtoInputStreamTests/AndroidTest.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2015 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<configuration description="Configuration for ProtoInputStream Tests"> + <option name="test-suite-tag" value="ProtoInputStreamTests" /> + <option name="config-descriptor:metadata" key="component" value="metrics" /> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="test-file-name" value="ProtoInputStreamTests.apk" /> + </target_preparer> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.test.protoinputstream" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration> diff --git a/tests/ProtoInputStreamTests/TEST_MAPPING b/tests/ProtoInputStreamTests/TEST_MAPPING new file mode 100644 index 000000000000..cf9f0772ac2d --- /dev/null +++ b/tests/ProtoInputStreamTests/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "presubmit": [ + { + "name": "ProtoInputStreamTests" + } + ] +}
\ No newline at end of file diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamBoolTest.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamBoolTest.java new file mode 100644 index 000000000000..c21c4033a0ff --- /dev/null +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamBoolTest.java @@ -0,0 +1,500 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.protoinputstream; + +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoStream; +import android.util.proto.WireTypeMismatchException; + +import com.android.test.protoinputstream.nano.Test; + +import com.google.protobuf.nano.MessageNano; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ProtoInputStreamBoolTest extends TestCase { + + /** + * Test reading single bool field + */ + public void testRead() throws IOException { + testRead(0); + testRead(1); + testRead(5); + } + + /** + * Implementation of testRead with a given chunkSize. + */ + private void testRead(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_BOOL; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, not written + // 3 -> 1 + (byte) 0x18, + (byte) 0x01, + // 2 -> 1 + (byte) 0x10, + (byte) 0x01, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + boolean[] results = new boolean[2]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + fail("Should never reach this"); + break; + case (int) fieldId2: + results[1] = pi.readBoolean(fieldId2); + break; + case (int) fieldId3: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals(false, results[0]); + assertEquals(true, results[1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testReadCompat() throws Exception { + testReadCompat(false); + testReadCompat(true); + } + + /** + * Implementation of testReadCompat with a given value. + */ + private void testReadCompat(boolean val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_BOOL; + final long fieldId = fieldFlags | ((long) 130 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.boolField = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + boolean result = false; // start off with default value + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result = pi.readBoolean(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.boolField, result); + } + + + /** + * Test reading repeated bool field + */ + public void testRepeated() throws IOException { + testRepeated(0); + testRepeated(1); + testRepeated(5); + } + + /** + * Implementation of testRepeated with a given chunkSize. + */ + private void testRepeated(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_BOOL; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x08, + (byte) 0x00, + // 2 -> 1 + (byte) 0x10, + (byte) 0x01, + + // 4 -> 0 + (byte) 0x20, + (byte) 0x00, + // 4 -> 1 + (byte) 0x20, + (byte) 0x01, + + // 1 -> 0 - default value, written when repeated + (byte) 0x08, + (byte) 0x00, + // 2 -> 1 + (byte) 0x10, + (byte) 0x01, + + // 3 -> 0 + (byte) 0x18, + (byte) 0x00, + // 3 -> 1 + (byte) 0x18, + (byte) 0x01, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + boolean[][] results = new boolean[3][2]; + int[] indices = new int[3]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readBoolean(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readBoolean(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readBoolean(fieldId3); + break; + case (int) fieldId4: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals(false, results[0][0]); + assertEquals(false, results[0][1]); + assertEquals(true, results[1][0]); + assertEquals(true, results[1][1]); + assertEquals(false, results[2][0]); + assertEquals(true, results[2][1]); + } + + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testRepeatedCompat() throws Exception { + testRepeatedCompat(new boolean[0]); + testRepeatedCompat(new boolean[]{false, true}); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testRepeatedCompat(boolean[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_BOOL; + final long fieldId = fieldFlags | ((long) 131 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.boolFieldRepeated = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + boolean[] result = new boolean[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readBoolean(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.boolFieldRepeated.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.boolFieldRepeated[i], result[i]); + } + } + + /** + * Test reading packed bool field + */ + public void testPacked() throws IOException { + testPacked(0); + testPacked(1); + testPacked(5); + } + + /** + * Implementation of testPacked with a given chunkSize. + */ + public void testPacked(int chunkSize) throws IOException { + + final long fieldFlags = ProtoStream.FIELD_COUNT_PACKED | ProtoStream.FIELD_TYPE_BOOL; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x0a, + (byte) 0x02, + (byte) 0x00, + (byte) 0x00, + // 4 -> 0,1 + (byte) 0x22, + (byte) 0x02, + (byte) 0x00, + (byte) 0x01, + // 2 -> 1 + (byte) 0x12, + (byte) 0x02, + (byte) 0x01, + (byte) 0x01, + // 3 -> 0,1 + (byte) 0x1a, + (byte) 0x02, + (byte) 0x00, + (byte) 0x01, + }; + + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + boolean[][] results = new boolean[3][2]; + int[] indices = new int[3]; + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readBoolean(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readBoolean(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readBoolean(fieldId3); + break; + case (int) fieldId4: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals(false, results[0][0]); + assertEquals(false, results[0][1]); + assertEquals(true, results[1][0]); + assertEquals(true, results[1][1]); + assertEquals(false, results[2][0]); + assertEquals(true, results[2][1]); + } + + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testPackedCompat() throws Exception { + testPackedCompat(new boolean[0]); + testPackedCompat(new boolean[]{false, true}); + } + + /** + * Implementation of testPackedCompat with a given value. + */ + private void testPackedCompat(boolean[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_PACKED | ProtoStream.FIELD_TYPE_BOOL; + final long fieldId = fieldFlags | ((long) 132 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.boolFieldPacked = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + boolean[] result = new boolean[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readBoolean(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.boolFieldPacked.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.boolFieldPacked[i], result[i]); + } + } + + /** + * Test that using the wrong read method throws an exception + */ + public void testBadReadType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_BOOL; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 1 + (byte) 0x08, + (byte) 0x01, + }; + + ProtoInputStream pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readFloat(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readDouble(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readInt(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readLong(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBytes(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readString(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + } + + /** + * Test that unexpected wrong wire types will throw an exception + */ + public void testBadWireType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_BOOL; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 : varint -> 1 + (byte) 0x08, + (byte) 0x01, + // 2 : fixed64 -> 0x1 + (byte) 0x11, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 : length delimited -> { 1 } + (byte) 0x1a, + (byte) 0x01, + (byte) 0x01, + // 6 : fixed32 + (byte) 0x35, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream); + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + try { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + pi.readBoolean(fieldId1); + // don't fail, varint is ok + break; + case (int) fieldId2: + pi.readBoolean(fieldId2); + fail("Should have thrown a WireTypeMismatchException"); + break; + case (int) fieldId3: + pi.readBoolean(fieldId3); + // don't fail, length delimited is ok (represents packed booleans) + break; + case (int) fieldId6: + pi.readBoolean(fieldId6); + fail("Should have thrown a WireTypeMismatchException"); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } catch (WireTypeMismatchException wtme) { + // good + } + } + stream.close(); + } +} diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamBytesTest.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamBytesTest.java new file mode 100644 index 000000000000..09fe40edda6c --- /dev/null +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamBytesTest.java @@ -0,0 +1,423 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.protoinputstream; + +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoStream; +import android.util.proto.WireTypeMismatchException; + +import com.android.test.protoinputstream.nano.Test; + +import com.google.protobuf.nano.MessageNano; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +public class ProtoInputStreamBytesTest extends TestCase { + + public void testRead() throws IOException { + testRead(0); + testRead(1); + testRead(5); + } + + private void testRead(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_BYTES; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> null - default value, written when repeated + (byte) 0x0a, + (byte) 0x00, + // 2 -> { } - default value, written when repeated + (byte) 0x12, + (byte) 0x00, + // 5 -> { 0, 1, 2, 3, 4 } + (byte) 0x2a, + (byte) 0x05, + (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, + // 3 -> { 0, 1, 2, 3, 4, 5 } + (byte) 0x1a, + (byte) 0x06, + (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05, + // 4 -> { (byte)0xff, (byte)0xfe, (byte)0xfd, (byte)0xfc } + (byte) 0x22, + (byte) 0x04, + (byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + byte[][] results = new byte[4][]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0] = pi.readBytes(fieldId1); + break; + case (int) fieldId2: + results[1] = pi.readBytes(fieldId2); + break; + case (int) fieldId3: + results[2] = pi.readBytes(fieldId3); + break; + case (int) fieldId4: + results[3] = pi.readBytes(fieldId4); + break; + case (int) fieldId5: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertTrue("Expected: [] Actual: " + Arrays.toString(results[0]), + Arrays.equals(new byte[]{}, results[0])); + assertTrue("Expected: [] Actual: " + Arrays.toString(results[1]), + Arrays.equals(new byte[]{}, results[1])); + assertTrue("Expected: [0, 1, 2, 3, 4, 5] Actual: " + Arrays.toString(results[2]), + Arrays.equals(new byte[]{0, 1, 2, 3, 4, 5}, results[2])); + assertTrue("Expected: [-1, -2, -3, -4] Actual: " + Arrays.toString(results[3]), + Arrays.equals(new byte[]{(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc}, + results[3])); + } + + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testReadCompat() throws Exception { + testReadCompat(new byte[0]); + testReadCompat(new byte[]{1, 2, 3, 4}); + testReadCompat(new byte[]{(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc}); + } + + /** + * Implementation of testReadCompat with a given value. + */ + private void testReadCompat(byte[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_BYTES; + final long fieldId = fieldFlags | ((long) 150 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.bytesField = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + byte[] result = new byte[val.length]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result = pi.readBytes(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.bytesField.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.bytesField[i], result[i]); + } + } + + + public void testRepeated() throws IOException { + testRepeated(0); + testRepeated(1); + testRepeated(5); + } + + private void testRepeated(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_BYTES; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> null - default value, written when repeated + (byte) 0x0a, + (byte) 0x00, + // 2 -> { } - default value, written when repeated + (byte) 0x12, + (byte) 0x00, + // 3 -> { 0, 1, 2, 3, 4, 5 } + (byte) 0x1a, + (byte) 0x06, + (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05, + // 4 -> { (byte)0xff, (byte)0xfe, (byte)0xfd, (byte)0xfc } + (byte) 0x22, + (byte) 0x04, + (byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, + + // 5 -> { 0, 1, 2, 3, 4} + (byte) 0x2a, + (byte) 0x05, + (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, + + // 1 -> null - default value, written when repeated + (byte) 0x0a, + (byte) 0x00, + // 2 -> { } - default value, written when repeated + (byte) 0x12, + (byte) 0x00, + // 3 -> { 0, 1, 2, 3, 4, 5 } + (byte) 0x1a, + (byte) 0x06, + (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05, + // 4 -> { (byte)0xff, (byte)0xfe, (byte)0xfd, (byte)0xfc } + (byte) 0x22, + (byte) 0x04, + (byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + byte[][][] results = new byte[4][2][]; + int[] indices = new int[4]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readBytes(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readBytes(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readBytes(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readBytes(fieldId4); + break; + case (int) fieldId5: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assert (Arrays.equals(new byte[]{}, results[0][0])); + assert (Arrays.equals(new byte[]{}, results[0][1])); + assert (Arrays.equals(new byte[]{}, results[1][0])); + assert (Arrays.equals(new byte[]{}, results[1][1])); + assert (Arrays.equals(new byte[]{1, 2, 3, 4}, results[2][0])); + assert (Arrays.equals(new byte[]{1, 2, 3, 4}, results[2][1])); + assert (Arrays.equals(new byte[]{(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc}, + results[3][0])); + assert (Arrays.equals(new byte[]{(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc}, + results[3][1])); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testRepeatedCompat() throws Exception { + testRepeatedCompat(new byte[0][]); + testRepeatedCompat(new byte[][]{ + new byte[0], + new byte[]{1, 2, 3, 4}, + new byte[]{(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc} + }); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testRepeatedCompat(byte[][] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_BYTES; + final long fieldId = fieldFlags | ((long) 151 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.bytesFieldRepeated = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + byte[][] result = new byte[val.length][]; // start off with default value + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readBytes(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.bytesFieldRepeated.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.bytesFieldRepeated[i].length, result[i].length); + for (int j = 0; j < result[i].length; j++) { + assertEquals(readback.bytesFieldRepeated[i][j], result[i][j]); + } + } + } + + /** + * Test that using the wrong read method throws an exception + */ + public void testBadReadType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_BYTES; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> {1} + (byte) 0x0a, + (byte) 0x01, + (byte) 0x01, + }; + + ProtoInputStream pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readFloat(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readDouble(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readInt(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readLong(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBoolean(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readString(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + } + + /** + * Test that unexpected wrong wire types will throw an exception + */ + public void testBadWireType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_BYTES; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 : varint -> 1 + (byte) 0x08, + (byte) 0x01, + // 2 : fixed64 -> 0x1 + (byte) 0x11, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 : length delimited -> { 1 } + (byte) 0x1a, + (byte) 0x01, + (byte) 0x01, + // 6 : fixed32 + (byte) 0x35, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream); + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + try { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + pi.readBytes(fieldId1); + fail("Should have thrown a WireTypeMismatchException"); + break; + case (int) fieldId2: + pi.readBytes(fieldId2); + fail("Should have thrown a WireTypeMismatchException"); + break; + case (int) fieldId3: + pi.readBytes(fieldId3); + // don't fail, length delimited is ok + break; + case (int) fieldId6: + pi.readBytes(fieldId6); + fail("Should have thrown a WireTypeMismatchException"); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } catch (WireTypeMismatchException wtme) { + // good + } + } + stream.close(); + } + +} diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamDoubleTest.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamDoubleTest.java new file mode 100644 index 000000000000..118fe3431e01 --- /dev/null +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamDoubleTest.java @@ -0,0 +1,728 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.protoinputstream; + +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoStream; +import android.util.proto.WireTypeMismatchException; + +import com.android.test.protoinputstream.nano.Test; + +import com.google.protobuf.nano.MessageNano; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ProtoInputStreamDoubleTest extends TestCase { + + + public void testRead() throws IOException { + testRead(0); + testRead(1); + testRead(5); + } + + private void testRead(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_DOUBLE; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + final long fieldId7 = fieldFlags | ((long) 7 & 0x0ffffffffL); + final long fieldId8 = fieldFlags | ((long) 8 & 0x0ffffffffL); + final long fieldId9 = fieldFlags | ((long) 9 & 0x0ffffffffL); + final long fieldId10 = fieldFlags | ((long) 10 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, not written + // 2 -> 1 + (byte) 0x11, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xf0, (byte) 0x3f, + // 10 -> 1 + (byte) 0x51, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xf0, (byte) 0x3f, + // 3 -> -1234.432 + (byte) 0x19, + (byte) 0x7d, (byte) 0x3f, (byte) 0x35, (byte) 0x5e, + (byte) 0xba, (byte) 0x49, (byte) 0x93, (byte) 0xc0, + // 4 -> 42.42 + (byte) 0x21, + (byte) 0xf6, (byte) 0x28, (byte) 0x5c, (byte) 0x8f, + (byte) 0xc2, (byte) 0x35, (byte) 0x45, (byte) 0x40, + // 5 -> Double.MIN_NORMAL + (byte) 0x29, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x10, (byte) 0x00, + // 6 -> DOUBLE.MIN_VALUE + (byte) 0x31, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 7 -> Double.NEGATIVE_INFINITY + (byte) 0x39, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xf0, (byte) 0xff, + // 8 -> Double.NaN + (byte) 0x41, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xf8, (byte) 0x7f, + // 9 -> Double.POSITIVE_INFINITY + (byte) 0x49, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xf0, (byte) 0x7f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + double[] results = new double[9]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + fail("Should never reach this"); + break; + case (int) fieldId2: + results[1] = pi.readDouble(fieldId2); + break; + case (int) fieldId3: + results[2] = pi.readDouble(fieldId3); + break; + case (int) fieldId4: + results[3] = pi.readDouble(fieldId4); + break; + case (int) fieldId5: + results[4] = pi.readDouble(fieldId5); + break; + case (int) fieldId6: + results[5] = pi.readDouble(fieldId6); + break; + case (int) fieldId7: + results[6] = pi.readDouble(fieldId7); + break; + case (int) fieldId8: + results[7] = pi.readDouble(fieldId8); + break; + case (int) fieldId9: + results[8] = pi.readDouble(fieldId9); + break; + case (int) fieldId10: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + assertEquals(0.0, results[0]); + assertEquals(1.0, results[1]); + assertEquals(-1234.432, results[2]); + assertEquals(42.42, results[3]); + assertEquals(Double.MIN_NORMAL, results[4]); + assertEquals(Double.MIN_VALUE, results[5]); + assertEquals(Double.NEGATIVE_INFINITY, results[6]); + assertEquals(Double.NaN, results[7]); + assertEquals(Double.POSITIVE_INFINITY, results[8]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testReadCompat() throws Exception { + testReadCompat(0); + testReadCompat(1); + testReadCompat(-1234.432); + testReadCompat(42.42); + testReadCompat(Double.MIN_NORMAL); + testReadCompat(Double.MIN_VALUE); + testReadCompat(Double.NEGATIVE_INFINITY); + testReadCompat(Double.NaN); + testReadCompat(Double.POSITIVE_INFINITY); + } + + /** + * Implementation of testReadCompat with a given value. + */ + private void testReadCompat(double val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_DOUBLE; + final long fieldId = fieldFlags | ((long) 10 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.doubleField = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + double result = 0.0; // start off with default value + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result = pi.readDouble(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.doubleField, result); + } + + + public void testRepeated() throws IOException { + testRepeated(0); + testRepeated(1); + testRepeated(5); + } + + private void testRepeated(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_DOUBLE; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + final long fieldId7 = fieldFlags | ((long) 7 & 0x0ffffffffL); + final long fieldId8 = fieldFlags | ((long) 8 & 0x0ffffffffL); + final long fieldId9 = fieldFlags | ((long) 9 & 0x0ffffffffL); + final long fieldId10 = fieldFlags | ((long) 10 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x09, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 2 -> 1 + (byte) 0x11, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xf0, (byte) 0x3f, + // 3 -> -1234.432 + (byte) 0x19, + (byte) 0x7d, (byte) 0x3f, (byte) 0x35, (byte) 0x5e, + (byte) 0xba, (byte) 0x49, (byte) 0x93, (byte) 0xc0, + // 4 -> 42.42 + (byte) 0x21, + (byte) 0xf6, (byte) 0x28, (byte) 0x5c, (byte) 0x8f, + (byte) 0xc2, (byte) 0x35, (byte) 0x45, (byte) 0x40, + // 5 -> Double.MIN_NORMAL + (byte) 0x29, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x10, (byte) 0x00, + // 6 -> DOUBLE.MIN_VALUE + (byte) 0x31, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 7 -> Double.NEGATIVE_INFINITY + (byte) 0x39, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xf0, (byte) 0xff, + // 8 -> Double.NaN + (byte) 0x41, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xf8, (byte) 0x7f, + // 9 -> Double.POSITIVE_INFINITY + (byte) 0x49, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xf0, (byte) 0x7f, + // 10 -> 1 + (byte) 0x51, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xf0, (byte) 0x3f, + + // 1 -> 0 - default value, written when repeated + (byte) 0x09, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 2 -> 1 + (byte) 0x11, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xf0, (byte) 0x3f, + // 3 -> -1234.432 + (byte) 0x19, + (byte) 0x7d, (byte) 0x3f, (byte) 0x35, (byte) 0x5e, + (byte) 0xba, (byte) 0x49, (byte) 0x93, (byte) 0xc0, + // 4 -> 42.42 + (byte) 0x21, + (byte) 0xf6, (byte) 0x28, (byte) 0x5c, (byte) 0x8f, + (byte) 0xc2, (byte) 0x35, (byte) 0x45, (byte) 0x40, + // 5 -> Double.MIN_NORMAL + (byte) 0x29, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x10, (byte) 0x00, + // 6 -> DOUBLE.MIN_VALUE + (byte) 0x31, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 7 -> Double.NEGATIVE_INFINITY + (byte) 0x39, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xf0, (byte) 0xff, + // 8 -> Double.NaN + (byte) 0x41, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xf8, (byte) 0x7f, + // 9 -> Double.POSITIVE_INFINITY + (byte) 0x49, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xf0, (byte) 0x7f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + double[][] results = new double[9][2]; + int[] indices = new int[9]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readDouble(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readDouble(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readDouble(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readDouble(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readDouble(fieldId5); + break; + case (int) fieldId6: + results[5][indices[5]++] = pi.readDouble(fieldId6); + break; + case (int) fieldId7: + results[6][indices[6]++] = pi.readDouble(fieldId7); + break; + case (int) fieldId8: + results[7][indices[7]++] = pi.readDouble(fieldId8); + break; + case (int) fieldId9: + results[8][indices[8]++] = pi.readDouble(fieldId9); + break; + case (int) fieldId10: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + assertEquals(0.0, results[0][0]); + assertEquals(0.0, results[0][1]); + assertEquals(1.0, results[1][0]); + assertEquals(1.0, results[1][1]); + assertEquals(-1234.432, results[2][0]); + assertEquals(-1234.432, results[2][1]); + assertEquals(42.42, results[3][0]); + assertEquals(42.42, results[3][1]); + assertEquals(Double.MIN_NORMAL, results[4][0]); + assertEquals(Double.MIN_NORMAL, results[4][1]); + assertEquals(Double.MIN_VALUE, results[5][0]); + assertEquals(Double.MIN_VALUE, results[5][1]); + assertEquals(Double.NEGATIVE_INFINITY, results[6][0]); + assertEquals(Double.NEGATIVE_INFINITY, results[6][1]); + assertEquals(Double.NaN, results[7][0]); + assertEquals(Double.NaN, results[7][1]); + assertEquals(Double.POSITIVE_INFINITY, results[8][0]); + assertEquals(Double.POSITIVE_INFINITY, results[8][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testRepeatedCompat() throws Exception { + testRepeatedCompat(new double[0]); + testRepeatedCompat(new double[]{0, 1, -1234.432, 42.42, + Double.MIN_NORMAL, Double.MIN_VALUE, Double.NEGATIVE_INFINITY, Double.NaN, + Double.POSITIVE_INFINITY, + }); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testRepeatedCompat(double[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_DOUBLE; + final long fieldId = fieldFlags | ((long) 11 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.doubleFieldRepeated = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + double[] result = new double[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readDouble(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.doubleFieldRepeated.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.doubleFieldRepeated[i], result[i]); + } + } + + + public void testPacked() throws IOException { + testPacked(0); + testPacked(1); + testPacked(5); + } + + private void testPacked(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_PACKED | ProtoStream.FIELD_TYPE_DOUBLE; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + final long fieldId7 = fieldFlags | ((long) 7 & 0x0ffffffffL); + final long fieldId8 = fieldFlags | ((long) 8 & 0x0ffffffffL); + final long fieldId9 = fieldFlags | ((long) 9 & 0x0ffffffffL); + final long fieldId10 = fieldFlags | ((long) 10 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x0a, + (byte) 0x10, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 2 -> 1 + (byte) 0x12, + (byte) 0x10, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xf0, (byte) 0x3f, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xf0, (byte) 0x3f, + // 10 -> 1 + (byte) 0x52, + (byte) 0x10, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xf0, (byte) 0x3f, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xf0, (byte) 0x3f, + // 3 -> -1234.432 + (byte) 0x1a, + (byte) 0x10, + (byte) 0x7d, (byte) 0x3f, (byte) 0x35, (byte) 0x5e, + (byte) 0xba, (byte) 0x49, (byte) 0x93, (byte) 0xc0, + (byte) 0x7d, (byte) 0x3f, (byte) 0x35, (byte) 0x5e, + (byte) 0xba, (byte) 0x49, (byte) 0x93, (byte) 0xc0, + // 4 -> 42.42 + (byte) 0x22, + (byte) 0x10, + (byte) 0xf6, (byte) 0x28, (byte) 0x5c, (byte) 0x8f, + (byte) 0xc2, (byte) 0x35, (byte) 0x45, (byte) 0x40, + (byte) 0xf6, (byte) 0x28, (byte) 0x5c, (byte) 0x8f, + (byte) 0xc2, (byte) 0x35, (byte) 0x45, (byte) 0x40, + // 5 -> Double.MIN_NORMAL + (byte) 0x2a, + (byte) 0x10, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x10, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x10, (byte) 0x00, + // 6 -> DOUBLE.MIN_VALUE + (byte) 0x32, + (byte) 0x10, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 7 -> Double.NEGATIVE_INFINITY + (byte) 0x3a, + (byte) 0x10, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xf0, (byte) 0xff, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xf0, (byte) 0xff, + // 8 -> Double.NaN + (byte) 0x42, + (byte) 0x10, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xf8, (byte) 0x7f, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xf8, (byte) 0x7f, + // 9 -> Double.POSITIVE_INFINITY + (byte) 0x4a, + (byte) 0x10, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xf0, (byte) 0x7f, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xf0, (byte) 0x7f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + double[][] results = new double[9][2]; + int[] indices = new int[9]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readDouble(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readDouble(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readDouble(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readDouble(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readDouble(fieldId5); + break; + case (int) fieldId6: + results[5][indices[5]++] = pi.readDouble(fieldId6); + break; + case (int) fieldId7: + results[6][indices[6]++] = pi.readDouble(fieldId7); + break; + case (int) fieldId8: + results[7][indices[7]++] = pi.readDouble(fieldId8); + break; + case (int) fieldId9: + results[8][indices[8]++] = pi.readDouble(fieldId9); + break; + case (int) fieldId10: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + assertEquals(0.0, results[0][0]); + assertEquals(0.0, results[0][1]); + assertEquals(1.0, results[1][0]); + assertEquals(1.0, results[1][1]); + assertEquals(-1234.432, results[2][0]); + assertEquals(-1234.432, results[2][1]); + assertEquals(42.42, results[3][0]); + assertEquals(42.42, results[3][1]); + assertEquals(Double.MIN_NORMAL, results[4][0]); + assertEquals(Double.MIN_NORMAL, results[4][1]); + assertEquals(Double.MIN_VALUE, results[5][0]); + assertEquals(Double.MIN_VALUE, results[5][1]); + assertEquals(Double.NEGATIVE_INFINITY, results[6][0]); + assertEquals(Double.NEGATIVE_INFINITY, results[6][1]); + assertEquals(Double.NaN, results[7][0]); + assertEquals(Double.NaN, results[7][1]); + assertEquals(Double.POSITIVE_INFINITY, results[8][0]); + assertEquals(Double.POSITIVE_INFINITY, results[8][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testPackedCompat() throws Exception { + testPackedCompat(new double[0]); + testPackedCompat(new double[]{0, 1, -1234.432, 42.42, + Double.MIN_NORMAL, Double.MIN_VALUE, Double.NEGATIVE_INFINITY, Double.NaN, + Double.POSITIVE_INFINITY, + }); + } + + /** + * Implementation of testPackedCompat with a given value. + */ + private void testPackedCompat(double[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_PACKED | ProtoStream.FIELD_TYPE_DOUBLE; + final long fieldId = fieldFlags | ((long) 12 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.doubleFieldPacked = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + double[] result = new double[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readDouble(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.doubleFieldPacked.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.doubleFieldPacked[i], result[i]); + } + } + + /** + * Test that using the wrong read method throws an exception + */ + public void testBadReadType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_DOUBLE; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 1 + (byte) 0x09, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + }; + + ProtoInputStream pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readFloat(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBoolean(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readInt(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readLong(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBytes(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readString(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + } + + /** + * Test that unexpected wrong wire types will throw an exception + */ + public void testBadWireType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_DOUBLE; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 : varint -> 1 + (byte) 0x08, + (byte) 0x01, + // 2 : fixed64 -> 0x1 + (byte) 0x11, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 : length delimited -> { 1 } + (byte) 0x1a, + (byte) 0x08, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 6 : fixed32 + (byte) 0x35, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream); + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + try { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + pi.readDouble(fieldId1); + fail("Should have thrown a WireTypeMismatchException"); + break; + case (int) fieldId2: + pi.readDouble(fieldId2); + // don't fail, fixed64 is ok + break; + case (int) fieldId3: + pi.readDouble(fieldId3); + // don't fail, length delimited is ok (represents packed doubles) + break; + case (int) fieldId6: + pi.readDouble(fieldId6); + fail("Should have thrown a WireTypeMismatchException"); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } catch (WireTypeMismatchException wtme) { + // good + } + } + stream.close(); + } +} diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamEnumTest.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamEnumTest.java new file mode 100644 index 000000000000..f55d95129588 --- /dev/null +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamEnumTest.java @@ -0,0 +1,570 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.protoinputstream; + +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoStream; +import android.util.proto.WireTypeMismatchException; + +import com.android.test.protoinputstream.nano.Test; + +import com.google.protobuf.nano.MessageNano; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ProtoInputStreamEnumTest extends TestCase { + + public void testRead() throws IOException { + testRead(0); + testRead(1); + testRead(5); + } + + private void testRead(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_ENUM; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, not written + // 2 -> 1 + (byte) 0x10, + (byte) 0x01, + // 6 -> MAX_VALUE + (byte) 0x30, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + // 3 -> -1 + (byte) 0x18, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 4 -> MIN_VALUE + (byte) 0x20, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 5 -> MAX_VALUE + (byte) 0x28, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + int[] results = new int[5]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + fail("Should never reach this"); + break; + case (int) fieldId2: + results[1] = pi.readInt(fieldId2); + break; + case (int) fieldId3: + results[2] = pi.readInt(fieldId3); + break; + case (int) fieldId4: + results[3] = pi.readInt(fieldId4); + break; + case (int) fieldId5: + results[4] = pi.readInt(fieldId5); + break; + case (int) fieldId6: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals(0, results[0]); + assertEquals(1, results[1]); + assertEquals(-1, results[2]); + assertEquals(Integer.MIN_VALUE, results[3]); + assertEquals(Integer.MAX_VALUE, results[4]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testReadCompat() throws Exception { + testReadCompat(0); + testReadCompat(1); + testReadCompat(-1); + testReadCompat(Integer.MIN_VALUE); + testReadCompat(Integer.MAX_VALUE); + } + + /** + * Implementation of testReadCompat with a given value. + */ + private void testReadCompat(int val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_ENUM; + final long fieldId = fieldFlags | ((long) 160 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.outsideField = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + + int result = 0; // start off with default value + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result = pi.readInt(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + // Nano proto drops values that are outside the range, so compare against val + assertEquals(val, result); + } + + public void testRepeated() throws IOException { + testRepeated(0); + testRepeated(1); + testRepeated(5); + } + + private void testRepeated(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_ENUM; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x08, + (byte) 0x00, + // 2 -> 1 + (byte) 0x10, + (byte) 0x01, + // 3 -> -1 + (byte) 0x18, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 4 -> MIN_VALUE + (byte) 0x20, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 5 -> MAX_VALUE + (byte) 0x28, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + + // 6 -> MAX_VALUE + (byte) 0x30, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + + // 1 -> 0 - default value, written when repeated + (byte) 0x08, + (byte) 0x00, + // 2 -> 1 + (byte) 0x10, + (byte) 0x01, + // 3 -> -1 + (byte) 0x18, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 4 -> MIN_VALUE + (byte) 0x20, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 5 -> MAX_VALUE + (byte) 0x28, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + int[][] results = new int[5][2]; + int[] indices = new int[5]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readInt(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readInt(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readInt(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readInt(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readInt(fieldId5); + break; + case (int) fieldId6: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + + assertEquals(0, results[0][0]); + assertEquals(0, results[0][1]); + assertEquals(1, results[1][0]); + assertEquals(1, results[1][1]); + assertEquals(-1, results[2][0]); + assertEquals(-1, results[2][1]); + assertEquals(Integer.MIN_VALUE, results[3][0]); + assertEquals(Integer.MIN_VALUE, results[3][1]); + assertEquals(Integer.MAX_VALUE, results[4][0]); + assertEquals(Integer.MAX_VALUE, results[4][1]); + } + + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testRepeatedCompat() throws Exception { + testRepeatedCompat(new int[]{}); + testRepeatedCompat(new int[]{0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE}); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testRepeatedCompat(int[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_ENUM; + final long fieldId = fieldFlags | ((long) 161 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.outsideFieldRepeated = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + + int[] result = new int[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readInt(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + // Nano proto drops values that are outside the range, so compare against val + assertEquals(val.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(val[i], result[i]); + } + } + + public void testPacked() throws IOException { + testPacked(0); + testPacked(1); + testPacked(5); + } + + private void testPacked(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_PACKED | ProtoStream.FIELD_TYPE_ENUM; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x0a, + (byte) 0x02, + (byte) 0x00, + (byte) 0x00, + // 2 -> 1 + (byte) 0x12, + (byte) 0x02, + (byte) 0x01, + (byte) 0x01, + + // 6 -> MAX_VALUE + (byte) 0x32, + (byte) 0x0a, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + + // 3 -> -1 + (byte) 0x1a, + (byte) 0x14, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + + // 4 -> MIN_VALUE + (byte) 0x22, + (byte) 0x14, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + + // 5 -> MAX_VALUE + (byte) 0x2a, + (byte) 0x0a, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + int[][] results = new int[5][2]; + int[] indices = new int[5]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readInt(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readInt(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readInt(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readInt(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readInt(fieldId5); + break; + case (int) fieldId6: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + + assertEquals(0, results[0][0]); + assertEquals(0, results[0][1]); + assertEquals(1, results[1][0]); + assertEquals(1, results[1][1]); + assertEquals(-1, results[2][0]); + assertEquals(-1, results[2][1]); + assertEquals(Integer.MIN_VALUE, results[3][0]); + assertEquals(Integer.MIN_VALUE, results[3][1]); + assertEquals(Integer.MAX_VALUE, results[4][0]); + assertEquals(Integer.MAX_VALUE, results[4][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testPackedCompat() throws Exception { + testPackedCompat(new int[]{}); + testPackedCompat(new int[]{0, 1}); + + // Nano proto has a bug. It gets the size with computeInt32SizeNoTag (correctly) + // but incorrectly uses writeRawVarint32 to write the value for negative numbers. + //testPackedCompat(new int[] { 0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE }); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testPackedCompat(int[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_PACKED | ProtoStream.FIELD_TYPE_ENUM; + final long fieldId = fieldFlags | ((long) 162 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.outsideFieldPacked = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + + int[] result = new int[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readInt(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + // Nano proto drops values that are outside the range, so compare against val + assertEquals(val.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(val[i], result[i]); + } + } + + /** + * Test that using the wrong read method throws an exception + */ + public void testBadReadType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_ENUM; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 1 + (byte) 0x08, + (byte) 0x01, + }; + + ProtoInputStream pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readFloat(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readDouble(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBoolean(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readLong(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBytes(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readString(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + } + + /** + * Test that unexpected wrong wire types will throw an exception + */ + public void testBadWireType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_ENUM; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 : varint -> 1 + (byte) 0x08, + (byte) 0x01, + // 2 : fixed64 -> 0x1 + (byte) 0x11, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 : length delimited -> { 1 } + (byte) 0x1a, + (byte) 0x01, + (byte) 0x01, + // 6 : fixed32 + (byte) 0x35, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream); + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + try { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + pi.readInt(fieldId1); + // don't fail, varint is ok + break; + case (int) fieldId2: + pi.readInt(fieldId2); + fail("Should have thrown a WireTypeMismatchException"); + break; + case (int) fieldId3: + pi.readInt(fieldId3); + // don't fail, length delimited is ok (represents packed enums) + break; + case (int) fieldId6: + pi.readInt(fieldId6); + fail("Should have thrown a WireTypeMismatchException"); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } catch (WireTypeMismatchException wtme) { + // good + } + } + stream.close(); + } +} diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamFixed32Test.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamFixed32Test.java new file mode 100644 index 000000000000..df68476f0c36 --- /dev/null +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamFixed32Test.java @@ -0,0 +1,547 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.protoinputstream; + +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoStream; +import android.util.proto.WireTypeMismatchException; + +import com.android.test.protoinputstream.nano.Test; + +import com.google.protobuf.nano.MessageNano; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ProtoInputStreamFixed32Test extends TestCase { + + public void testRead() throws IOException { + testRead(0); + testRead(1); + testRead(5); + } + + private void testRead(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_FIXED32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, not written + // 2 -> 1 + (byte) 0x15, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 6 -> 1 + (byte) 0x35, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 -> -1 + (byte) 0x1d, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // 4 -> Integer.MIN_VALUE + (byte) 0x25, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + // 5 -> Integer.MAX_VALUE + (byte) 0x2d, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + int[] results = new int[5]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + fail("Should never reach this"); + break; + case (int) fieldId2: + results[1] = pi.readInt(fieldId2); + break; + case (int) fieldId3: + results[2] = pi.readInt(fieldId3); + break; + case (int) fieldId4: + results[3] = pi.readInt(fieldId4); + break; + case (int) fieldId5: + results[4] = pi.readInt(fieldId5); + break; + case (int) fieldId6: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals(0, results[0]); + assertEquals(1, results[1]); + assertEquals(-1, results[2]); + assertEquals(Integer.MIN_VALUE, results[3]); + assertEquals(Integer.MAX_VALUE, results[4]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testReadCompat() throws Exception { + testReadCompat(0); + testReadCompat(1); + testReadCompat(-1); + testReadCompat(Integer.MIN_VALUE); + testReadCompat(Integer.MAX_VALUE); + } + + /** + * Implementation of testReadCompat with a given value. + */ + private void testReadCompat(int val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_FIXED32; + final long fieldId = fieldFlags | ((long) 90 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.fixed32Field = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + int result = 0; // start off with default value + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result = pi.readInt(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.fixed32Field, result); + } + + public void testRepeated() throws IOException { + testRepeated(0); + testRepeated(1); + testRepeated(5); + } + + private void testRepeated(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_FIXED32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x0d, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 2 -> 1 + (byte) 0x15, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 -> -1 + (byte) 0x1d, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // 4 -> Integer.MIN_VALUE + (byte) 0x25, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + // 5 -> Integer.MAX_VALUE + (byte) 0x2d, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + + // 6 -> 1 + (byte) 0x35, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + + // 1 -> 0 - default value, written when repeated + (byte) 0x0d, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 2 -> 1 + (byte) 0x15, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 -> -1 + (byte) 0x1d, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // 4 -> Integer.MIN_VALUE + (byte) 0x25, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + // 5 -> Integer.MAX_VALUE + (byte) 0x2d, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + int[][] results = new int[5][2]; + int[] indices = new int[5]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readInt(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readInt(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readInt(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readInt(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readInt(fieldId5); + break; + case (int) fieldId6: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + + assertEquals(0, results[0][0]); + assertEquals(0, results[0][1]); + assertEquals(1, results[1][0]); + assertEquals(1, results[1][1]); + assertEquals(-1, results[2][0]); + assertEquals(-1, results[2][1]); + assertEquals(Integer.MIN_VALUE, results[3][0]); + assertEquals(Integer.MIN_VALUE, results[3][1]); + assertEquals(Integer.MAX_VALUE, results[4][0]); + assertEquals(Integer.MAX_VALUE, results[4][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testRepeatedCompat() throws Exception { + testRepeatedCompat(new int[0]); + testRepeatedCompat(new int[]{0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE}); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testRepeatedCompat(int[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_FIXED32; + final long fieldId = fieldFlags | ((long) 91 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.fixed32FieldRepeated = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + int[] result = new int[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readInt(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.fixed32FieldRepeated.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.fixed32FieldRepeated[i], result[i]); + } + } + + public void testPacked() throws IOException { + testPacked(0); + testPacked(1); + testPacked(5); + } + + private void testPacked(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_PACKED | ProtoStream.FIELD_TYPE_FIXED32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x0a, + (byte) 0x08, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 6 -> 1 + (byte) 0x32, + (byte) 0x08, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 2 -> 1 + (byte) 0x12, + (byte) 0x08, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 -> -1 + (byte) 0x1a, + (byte) 0x08, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // 4 -> Integer.MIN_VALUE + (byte) 0x22, + (byte) 0x08, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + // 5 -> Integer.MAX_VALUE + (byte) 0x2a, + (byte) 0x08, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + int[][] results = new int[5][2]; + int[] indices = new int[5]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readInt(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readInt(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readInt(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readInt(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readInt(fieldId5); + break; + case (int) fieldId6: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + + assertEquals(0, results[0][0]); + assertEquals(0, results[0][1]); + assertEquals(1, results[1][0]); + assertEquals(1, results[1][1]); + assertEquals(-1, results[2][0]); + assertEquals(-1, results[2][1]); + assertEquals(Integer.MIN_VALUE, results[3][0]); + assertEquals(Integer.MIN_VALUE, results[3][1]); + assertEquals(Integer.MAX_VALUE, results[4][0]); + assertEquals(Integer.MAX_VALUE, results[4][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testPackedCompat() throws Exception { + testPackedCompat(new int[0]); + testPackedCompat(new int[]{0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE}); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testPackedCompat(int[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_FIXED32; + final long fieldId = fieldFlags | ((long) 92 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.fixed32FieldPacked = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + int[] result = new int[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readInt(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.fixed32FieldPacked.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.fixed32FieldPacked[i], result[i]); + } + } + + /** + * Test that using the wrong read method throws an exception + */ + public void testBadReadType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_FIXED32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 1 + (byte) 0x0d, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + }; + + ProtoInputStream pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readFloat(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readDouble(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBoolean(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readLong(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBytes(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readString(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + } + + /** + * Test that unexpected wrong wire types will throw an exception + */ + public void testBadWireType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_FIXED32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 : varint -> 1 + (byte) 0x08, + (byte) 0x01, + // 2 : fixed64 -> 0x1 + (byte) 0x11, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 : length delimited -> { 1 } + (byte) 0x1a, + (byte) 0x04, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 6 : fixed32 + (byte) 0x35, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream); + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + try { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + pi.readInt(fieldId1); + fail("Should have thrown a WireTypeMismatchException"); + break; + case (int) fieldId2: + pi.readInt(fieldId2); + fail("Should have thrown a WireTypeMismatchException"); + break; + case (int) fieldId3: + pi.readInt(fieldId3); + // don't fail, length delimited is ok (represents packed fixed32) + break; + case (int) fieldId6: + pi.readInt(fieldId6); + // don't fail, fixed32 is ok + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } catch (WireTypeMismatchException wtme) { + // good + } + } + stream.close(); + } +} diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamFixed64Test.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamFixed64Test.java new file mode 100644 index 000000000000..af4130b28cd8 --- /dev/null +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamFixed64Test.java @@ -0,0 +1,649 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.protoinputstream; + +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoStream; +import android.util.proto.WireTypeMismatchException; + +import com.android.test.protoinputstream.nano.Test; + +import com.google.protobuf.nano.MessageNano; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ProtoInputStreamFixed64Test extends TestCase { + + public void testRead() throws IOException { + testRead(0); + testRead(1); + testRead(5); + } + + private void testRead(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_FIXED64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + final long fieldId7 = fieldFlags | ((long) 7 & 0x0ffffffffL); + final long fieldId8 = fieldFlags | ((long) 8 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, not written + // 2 -> 1 + (byte) 0x11, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 6 -> 1 + (byte) 0x41, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 -> -1 + (byte) 0x19, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // 4 -> Integer.MIN_VALUE + (byte) 0x21, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // 5 -> Integer.MAX_VALUE + (byte) 0x29, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 6 -> Long.MIN_VALUE + (byte) 0x31, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + // 7 -> Long.MAX_VALUE + (byte) 0x39, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + long[] results = new long[7]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + fail("Should never reach this"); + break; + case (int) fieldId2: + results[1] = pi.readLong(fieldId2); + break; + case (int) fieldId3: + results[2] = pi.readLong(fieldId3); + break; + case (int) fieldId4: + results[3] = pi.readLong(fieldId4); + break; + case (int) fieldId5: + results[4] = pi.readLong(fieldId5); + break; + case (int) fieldId6: + results[5] = pi.readLong(fieldId6); + break; + case (int) fieldId7: + results[6] = pi.readLong(fieldId7); + break; + case (int) fieldId8: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals(0, results[0]); + assertEquals(1, results[1]); + assertEquals(-1, results[2]); + assertEquals(Integer.MIN_VALUE, results[3]); + assertEquals(Integer.MAX_VALUE, results[4]); + assertEquals(Long.MIN_VALUE, results[5]); + assertEquals(Long.MAX_VALUE, results[6]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testReadCompat() throws Exception { + testReadCompat(0); + testReadCompat(1); + testReadCompat(-1); + testReadCompat(Integer.MIN_VALUE); + testReadCompat(Integer.MAX_VALUE); + testReadCompat(Long.MIN_VALUE); + testReadCompat(Long.MAX_VALUE); + } + + /** + * Implementation of testReadCompat with a given value. + */ + private void testReadCompat(long val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_FIXED64; + final long fieldId = fieldFlags | ((long) 100 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.fixed64Field = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + long result = 0; // start off with default value + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result = pi.readLong(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.fixed64Field, result); + } + + public void testRepeated() throws IOException { + testRepeated(0); + testRepeated(1); + testRepeated(5); + } + + private void testRepeated(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_FIXED64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + final long fieldId7 = fieldFlags | ((long) 7 & 0x0ffffffffL); + final long fieldId8 = fieldFlags | ((long) 8 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x09, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 2 -> 1 + (byte) 0x11, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 -> -1 + (byte) 0x19, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // 4 -> Integer.MIN_VALUE + (byte) 0x21, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // 5 -> Integer.MAX_VALUE + (byte) 0x29, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 6 -> Long.MIN_VALUE + (byte) 0x31, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + // 7 -> Long.MAX_VALUE + (byte) 0x39, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + + // 8 -> 1 + (byte) 0x41, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + + // 1 -> 0 - default value, written when repeated + (byte) 0x09, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 2 -> 1 + (byte) 0x11, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 -> -1 + (byte) 0x19, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // 4 -> Integer.MIN_VALUE + (byte) 0x21, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // 5 -> Integer.MAX_VALUE + (byte) 0x29, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 6 -> Long.MIN_VALUE + (byte) 0x31, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + // 7 -> Long.MAX_VALUE + (byte) 0x39, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + long[][] results = new long[7][2]; + int[] indices = new int[7]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readLong(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readLong(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readLong(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readLong(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readLong(fieldId5); + break; + case (int) fieldId6: + results[5][indices[5]++] = pi.readLong(fieldId6); + break; + case (int) fieldId7: + results[6][indices[6]++] = pi.readLong(fieldId7); + break; + case (int) fieldId8: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals(0, results[0][0]); + assertEquals(0, results[0][1]); + assertEquals(1, results[1][0]); + assertEquals(1, results[1][1]); + assertEquals(-1, results[2][0]); + assertEquals(-1, results[2][1]); + assertEquals(Integer.MIN_VALUE, results[3][0]); + assertEquals(Integer.MIN_VALUE, results[3][1]); + assertEquals(Integer.MAX_VALUE, results[4][0]); + assertEquals(Integer.MAX_VALUE, results[4][1]); + assertEquals(Long.MIN_VALUE, results[5][0]); + assertEquals(Long.MIN_VALUE, results[5][1]); + assertEquals(Long.MAX_VALUE, results[6][0]); + assertEquals(Long.MAX_VALUE, results[6][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testRepeatedCompat() throws Exception { + testRepeatedCompat(new long[0]); + testRepeatedCompat(new long[]{0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE}); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testRepeatedCompat(long[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_FIXED64; + final long fieldId = fieldFlags | ((long) 101 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.fixed64FieldRepeated = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + long[] result = new long[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readLong(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.fixed64FieldRepeated.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.fixed64FieldRepeated[i], result[i]); + } + } + + public void testPacked() throws IOException { + testPacked(0); + testPacked(1); + testPacked(5); + } + + private void testPacked(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_PACKED | ProtoStream.FIELD_TYPE_FIXED64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + final long fieldId7 = fieldFlags | ((long) 7 & 0x0ffffffffL); + final long fieldId8 = fieldFlags | ((long) 8 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x0a, + (byte) 0x10, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 2 -> 1 + (byte) 0x12, + (byte) 0x10, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 8 -> 1 + (byte) 0x42, + (byte) 0x10, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 -> -1 + (byte) 0x1a, + (byte) 0x10, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // 4 -> Integer.MIN_VALUE + (byte) 0x22, + (byte) 0x10, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // 5 -> Integer.MAX_VALUE + (byte) 0x2a, + (byte) 0x10, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 6 -> Long.MIN_VALUE + (byte) 0x32, + (byte) 0x10, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + // 7 -> Long.MAX_VALUE + (byte) 0x3a, + (byte) 0x10, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + long[][] results = new long[7][2]; + int[] indices = new int[7]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readLong(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readLong(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readLong(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readLong(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readLong(fieldId5); + break; + case (int) fieldId6: + results[5][indices[5]++] = pi.readLong(fieldId6); + break; + case (int) fieldId7: + results[6][indices[6]++] = pi.readLong(fieldId7); + break; + case (int) fieldId8: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals(0, results[0][0]); + assertEquals(0, results[0][1]); + assertEquals(1, results[1][0]); + assertEquals(1, results[1][1]); + assertEquals(-1, results[2][0]); + assertEquals(-1, results[2][1]); + assertEquals(Integer.MIN_VALUE, results[3][0]); + assertEquals(Integer.MIN_VALUE, results[3][1]); + assertEquals(Integer.MAX_VALUE, results[4][0]); + assertEquals(Integer.MAX_VALUE, results[4][1]); + assertEquals(Long.MIN_VALUE, results[5][0]); + assertEquals(Long.MIN_VALUE, results[5][1]); + assertEquals(Long.MAX_VALUE, results[6][0]); + assertEquals(Long.MAX_VALUE, results[6][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testPackedCompat() throws Exception { + testPackedCompat(new long[0]); + testPackedCompat(new long[]{0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE}); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testPackedCompat(long[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_FIXED64; + final long fieldId = fieldFlags | ((long) 102 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.fixed64FieldPacked = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + long[] result = new long[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readLong(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.fixed64FieldPacked.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.fixed64FieldPacked[i], result[i]); + } + } + + /** + * Test that using the wrong read method throws an exception + */ + public void testBadReadType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_FIXED64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 1 + (byte) 0x09, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + }; + + ProtoInputStream pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readFloat(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readDouble(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readInt(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBoolean(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBytes(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readString(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + } + + /** + * Test that unexpected wrong wire types will throw an exception + */ + public void testBadWireType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_FIXED64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 : varint -> 1 + (byte) 0x08, + (byte) 0x01, + // 2 : fixed64 -> 0x1 + (byte) 0x11, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 : length delimited -> { 1 } + (byte) 0x1a, + (byte) 0x08, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 6 : fixed32 + (byte) 0x35, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream); + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + try { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + pi.readLong(fieldId1); + fail("Should have thrown a WireTypeMismatchException"); + break; + case (int) fieldId2: + pi.readLong(fieldId2); + // don't fail, fixed64 is ok + break; + case (int) fieldId3: + pi.readLong(fieldId3); + // don't fail, length delimited is ok (represents packed fixed64) + break; + case (int) fieldId6: + pi.readLong(fieldId6); + fail("Should have thrown a WireTypeMismatchException"); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } catch (WireTypeMismatchException wtme) { + // good + } + } + stream.close(); + } +} diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamFloatTest.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamFloatTest.java new file mode 100644 index 000000000000..9bc07dc513e1 --- /dev/null +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamFloatTest.java @@ -0,0 +1,679 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.protoinputstream; + +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoStream; +import android.util.proto.WireTypeMismatchException; + +import com.android.test.protoinputstream.nano.Test; + +import com.google.protobuf.nano.MessageNano; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ProtoInputStreamFloatTest extends TestCase { + + + public void testRead() throws IOException { + testRead(0); + testRead(1); + testRead(5); + } + + private void testRead(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_FLOAT; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + final long fieldId7 = fieldFlags | ((long) 7 & 0x0ffffffffL); + final long fieldId8 = fieldFlags | ((long) 8 & 0x0ffffffffL); + final long fieldId9 = fieldFlags | ((long) 9 & 0x0ffffffffL); + final long fieldId10 = fieldFlags | ((long) 10 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, not written + // 2 -> 1 + (byte) 0x15, + (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0x3f, + // 10 -> 1 + (byte) 0x55, + (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0x3f, + // 3 -> -1234.432 + (byte) 0x1d, + (byte) 0xd3, (byte) 0x4d, (byte) 0x9a, (byte) 0xc4, + // 4 -> 42.42 + (byte) 0x25, + (byte) 0x14, (byte) 0xae, (byte) 0x29, (byte) 0x42, + // 5 -> Float.MIN_NORMAL + (byte) 0x2d, + (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0x00, + // 6 -> DOUBLE.MIN_VALUE + (byte) 0x35, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 7 -> Float.NEGATIVE_INFINITY + (byte) 0x3d, + (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0xff, + // 8 -> Float.NaN + (byte) 0x45, + (byte) 0x00, (byte) 0x00, (byte) 0xc0, (byte) 0x7f, + // 9 -> Float.POSITIVE_INFINITY + (byte) 0x4d, + (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0x7f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + float[] results = new float[9]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + fail("Should never reach this"); + break; + case (int) fieldId2: + results[1] = pi.readFloat(fieldId2); + break; + case (int) fieldId3: + results[2] = pi.readFloat(fieldId3); + break; + case (int) fieldId4: + results[3] = pi.readFloat(fieldId4); + break; + case (int) fieldId5: + results[4] = pi.readFloat(fieldId5); + break; + case (int) fieldId6: + results[5] = pi.readFloat(fieldId6); + break; + case (int) fieldId7: + results[6] = pi.readFloat(fieldId7); + break; + case (int) fieldId8: + results[7] = pi.readFloat(fieldId8); + break; + case (int) fieldId9: + results[8] = pi.readFloat(fieldId9); + break; + case (int) fieldId10: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + assertEquals(0.0f, results[0]); + assertEquals(1.0f, results[1]); + assertEquals(-1234.432f, results[2]); + assertEquals(42.42f, results[3]); + assertEquals(Float.MIN_NORMAL, results[4]); + assertEquals(Float.MIN_VALUE, results[5]); + assertEquals(Float.NEGATIVE_INFINITY, results[6]); + assertEquals(Float.NaN, results[7]); + assertEquals(Float.POSITIVE_INFINITY, results[8]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testReadCompat() throws Exception { + testReadCompat(0); + testReadCompat(1); + testReadCompat(-1234.432f); + testReadCompat(42.42f); + testReadCompat(Float.MIN_NORMAL); + testReadCompat(Float.MIN_VALUE); + testReadCompat(Float.NEGATIVE_INFINITY); + testReadCompat(Float.NaN); + testReadCompat(Float.POSITIVE_INFINITY); + } + + /** + * Implementation of testReadCompat with a given value. + */ + private void testReadCompat(float val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_FLOAT; + final long fieldId = fieldFlags | ((long) 20 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.floatField = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + float result = 0.0f; // start off with default value + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result = pi.readFloat(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.floatField, result); + } + + + public void testRepeated() throws IOException { + testRepeated(0); + testRepeated(1); + testRepeated(5); + } + + private void testRepeated(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_FLOAT; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + final long fieldId7 = fieldFlags | ((long) 7 & 0x0ffffffffL); + final long fieldId8 = fieldFlags | ((long) 8 & 0x0ffffffffL); + final long fieldId9 = fieldFlags | ((long) 9 & 0x0ffffffffL); + final long fieldId10 = fieldFlags | ((long) 10 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x0d, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 2 -> 1 + (byte) 0x15, + (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0x3f, + // 3 -> -1234.432 + (byte) 0x1d, + (byte) 0xd3, (byte) 0x4d, (byte) 0x9a, (byte) 0xc4, + // 4 -> 42.42 + (byte) 0x25, + (byte) 0x14, (byte) 0xae, (byte) 0x29, (byte) 0x42, + // 5 -> Float.MIN_NORMAL + (byte) 0x2d, + (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0x00, + // 6 -> DOUBLE.MIN_VALUE + (byte) 0x35, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 7 -> Float.NEGATIVE_INFINITY + (byte) 0x3d, + (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0xff, + // 8 -> Float.NaN + (byte) 0x45, + (byte) 0x00, (byte) 0x00, (byte) 0xc0, (byte) 0x7f, + // 9 -> Float.POSITIVE_INFINITY + (byte) 0x4d, + (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0x7f, + + // 10 -> 1 + (byte) 0x55, + (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0x3f, + + // 1 -> 0 - default value, written when repeated + (byte) 0x0d, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 2 -> 1 + (byte) 0x15, + (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0x3f, + // 3 -> -1234.432 + (byte) 0x1d, + (byte) 0xd3, (byte) 0x4d, (byte) 0x9a, (byte) 0xc4, + // 4 -> 42.42 + (byte) 0x25, + (byte) 0x14, (byte) 0xae, (byte) 0x29, (byte) 0x42, + // 5 -> Float.MIN_NORMAL + (byte) 0x2d, + (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0x00, + // 6 -> DOUBLE.MIN_VALUE + (byte) 0x35, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 7 -> Float.NEGATIVE_INFINITY + (byte) 0x3d, + (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0xff, + // 8 -> Float.NaN + (byte) 0x45, + (byte) 0x00, (byte) 0x00, (byte) 0xc0, (byte) 0x7f, + // 9 -> Float.POSITIVE_INFINITY + (byte) 0x4d, + (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0x7f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + float[][] results = new float[9][2]; + int[] indices = new int[9]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readFloat(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readFloat(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readFloat(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readFloat(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readFloat(fieldId5); + break; + case (int) fieldId6: + results[5][indices[5]++] = pi.readFloat(fieldId6); + break; + case (int) fieldId7: + results[6][indices[6]++] = pi.readFloat(fieldId7); + break; + case (int) fieldId8: + results[7][indices[7]++] = pi.readFloat(fieldId8); + break; + case (int) fieldId9: + results[8][indices[8]++] = pi.readFloat(fieldId9); + break; + case (int) fieldId10: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + assertEquals(0.0f, results[0][0]); + assertEquals(0.0f, results[0][1]); + assertEquals(1.0f, results[1][0]); + assertEquals(1.0f, results[1][1]); + assertEquals(-1234.432f, results[2][0]); + assertEquals(-1234.432f, results[2][1]); + assertEquals(42.42f, results[3][0]); + assertEquals(42.42f, results[3][1]); + assertEquals(Float.MIN_NORMAL, results[4][0]); + assertEquals(Float.MIN_NORMAL, results[4][1]); + assertEquals(Float.MIN_VALUE, results[5][0]); + assertEquals(Float.MIN_VALUE, results[5][1]); + assertEquals(Float.NEGATIVE_INFINITY, results[6][0]); + assertEquals(Float.NEGATIVE_INFINITY, results[6][1]); + assertEquals(Float.NaN, results[7][0]); + assertEquals(Float.NaN, results[7][1]); + assertEquals(Float.POSITIVE_INFINITY, results[8][0]); + assertEquals(Float.POSITIVE_INFINITY, results[8][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testRepeatedCompat() throws Exception { + testRepeatedCompat(new float[0]); + testRepeatedCompat(new float[]{0, 1, -1234.432f, 42.42f, + Float.MIN_NORMAL, Float.MIN_VALUE, Float.NEGATIVE_INFINITY, Float.NaN, + Float.POSITIVE_INFINITY, + }); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testRepeatedCompat(float[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_FLOAT; + final long fieldId = fieldFlags | ((long) 21 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.floatFieldRepeated = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + float[] result = new float[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readFloat(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.floatFieldRepeated.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.floatFieldRepeated[i], result[i]); + } + } + + + public void testPacked() throws IOException { + testPacked(0); + testPacked(1); + testPacked(5); + } + + private void testPacked(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_PACKED | ProtoStream.FIELD_TYPE_FLOAT; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + final long fieldId7 = fieldFlags | ((long) 7 & 0x0ffffffffL); + final long fieldId8 = fieldFlags | ((long) 8 & 0x0ffffffffL); + final long fieldId9 = fieldFlags | ((long) 9 & 0x0ffffffffL); + final long fieldId10 = fieldFlags | ((long) 10 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x0a, + (byte) 0x08, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 2 -> 1 + (byte) 0x12, + (byte) 0x08, + (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0x3f, + (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0x3f, + // 10 -> 1 + (byte) 0x52, + (byte) 0x08, + (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0x3f, + (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0x3f, + // 3 -> -1234.432 + (byte) 0x1a, + (byte) 0x08, + (byte) 0xd3, (byte) 0x4d, (byte) 0x9a, (byte) 0xc4, + (byte) 0xd3, (byte) 0x4d, (byte) 0x9a, (byte) 0xc4, + // 4 -> 42.42 + (byte) 0x22, + (byte) 0x08, + (byte) 0x14, (byte) 0xae, (byte) 0x29, (byte) 0x42, + (byte) 0x14, (byte) 0xae, (byte) 0x29, (byte) 0x42, + // 5 -> Float.MIN_NORMAL + (byte) 0x2a, + (byte) 0x08, + (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0x00, + // 6 -> DOUBLE.MIN_VALUE + (byte) 0x32, + (byte) 0x08, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 7 -> Float.NEGATIVE_INFINITY + (byte) 0x3a, + (byte) 0x08, + (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0xff, + (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0xff, + // 8 -> Float.NaN + (byte) 0x42, + (byte) 0x08, + (byte) 0x00, (byte) 0x00, (byte) 0xc0, (byte) 0x7f, + (byte) 0x00, (byte) 0x00, (byte) 0xc0, (byte) 0x7f, + // 9 -> Float.POSITIVE_INFINITY + (byte) 0x4a, + (byte) 0x08, + (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0x7f, + (byte) 0x00, (byte) 0x00, (byte) 0x80, (byte) 0x7f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + float[][] results = new float[9][2]; + int[] indices = new int[9]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readFloat(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readFloat(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readFloat(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readFloat(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readFloat(fieldId5); + break; + case (int) fieldId6: + results[5][indices[5]++] = pi.readFloat(fieldId6); + break; + case (int) fieldId7: + results[6][indices[6]++] = pi.readFloat(fieldId7); + break; + case (int) fieldId8: + results[7][indices[7]++] = pi.readFloat(fieldId8); + break; + case (int) fieldId9: + results[8][indices[8]++] = pi.readFloat(fieldId9); + break; + case (int) fieldId10: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + assertEquals(0.0f, results[0][0]); + assertEquals(0.0f, results[0][1]); + assertEquals(1.0f, results[1][0]); + assertEquals(1.0f, results[1][1]); + assertEquals(-1234.432f, results[2][0]); + assertEquals(-1234.432f, results[2][1]); + assertEquals(42.42f, results[3][0]); + assertEquals(42.42f, results[3][1]); + assertEquals(Float.MIN_NORMAL, results[4][0]); + assertEquals(Float.MIN_NORMAL, results[4][1]); + assertEquals(Float.MIN_VALUE, results[5][0]); + assertEquals(Float.MIN_VALUE, results[5][1]); + assertEquals(Float.NEGATIVE_INFINITY, results[6][0]); + assertEquals(Float.NEGATIVE_INFINITY, results[6][1]); + assertEquals(Float.NaN, results[7][0]); + assertEquals(Float.NaN, results[7][1]); + assertEquals(Float.POSITIVE_INFINITY, results[8][0]); + assertEquals(Float.POSITIVE_INFINITY, results[8][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testPackedCompat() throws Exception { + testPackedCompat(new float[0]); + testPackedCompat(new float[]{0, 1, -1234.432f, 42.42f, + Float.MIN_NORMAL, Float.MIN_VALUE, Float.NEGATIVE_INFINITY, Float.NaN, + Float.POSITIVE_INFINITY, + }); + } + + /** + * Implementation of testPackedCompat with a given value. + */ + private void testPackedCompat(float[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_PACKED | ProtoStream.FIELD_TYPE_FLOAT; + final long fieldId = fieldFlags | ((long) 22 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.floatFieldPacked = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + float[] result = new float[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readFloat(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.floatFieldPacked.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.floatFieldPacked[i], result[i]); + } + } + + /** + * Test that using the wrong read method throws an exception + */ + public void testBadReadType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_FLOAT; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 1 + (byte) 0x0d, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + }; + + ProtoInputStream pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBoolean(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readDouble(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readInt(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readLong(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBytes(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readString(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + } + + /** + * Test that unexpected wrong wire types will throw an exception + */ + public void testBadWireType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_FLOAT; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 : varint -> 1 + (byte) 0x08, + (byte) 0x01, + // 2 : fixed64 -> 0x1 + (byte) 0x11, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 : length delimited -> { 1 } + (byte) 0x1a, + (byte) 0x04, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 6 : fixed32 + (byte) 0x35, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream); + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + try { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + pi.readFloat(fieldId1); + fail("Should have thrown a WireTypeMismatchException"); + break; + case (int) fieldId2: + pi.readFloat(fieldId2); + fail("Should have thrown a WireTypeMismatchException"); + break; + case (int) fieldId3: + pi.readFloat(fieldId3); + // don't fail, length delimited is ok (represents packed floats) + break; + case (int) fieldId6: + pi.readFloat(fieldId6); + // don't fail, fixed32 is ok + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } catch (WireTypeMismatchException wtme) { + // good + } + } + stream.close(); + } +} diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamInt32Test.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamInt32Test.java new file mode 100644 index 000000000000..0065870486f2 --- /dev/null +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamInt32Test.java @@ -0,0 +1,565 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.protoinputstream; + +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoStream; +import android.util.proto.WireTypeMismatchException; + +import com.android.test.protoinputstream.nano.Test; + +import com.google.protobuf.nano.MessageNano; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ProtoInputStreamInt32Test extends TestCase { + + public void testRead() throws IOException { + testRead(0); + testRead(1); + testRead(5); + } + + private void testRead(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_INT32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, not written + // 2 -> 1 + (byte) 0x10, + (byte) 0x01, + // 6 -> MAX_VALUE + (byte) 0x30, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + // 3 -> -1 + (byte) 0x18, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 4 -> MIN_VALUE + (byte) 0x20, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 5 -> MAX_VALUE + (byte) 0x28, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + int[] results = new int[5]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + fail("Should never reach this"); + break; + case (int) fieldId2: + results[1] = pi.readInt(fieldId2); + break; + case (int) fieldId3: + results[2] = pi.readInt(fieldId3); + break; + case (int) fieldId4: + results[3] = pi.readInt(fieldId4); + break; + case (int) fieldId5: + results[4] = pi.readInt(fieldId5); + break; + case (int) fieldId6: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals(0, results[0]); + assertEquals(1, results[1]); + assertEquals(-1, results[2]); + assertEquals(Integer.MIN_VALUE, results[3]); + assertEquals(Integer.MAX_VALUE, results[4]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testReadCompat() throws Exception { + testReadCompat(0); + testReadCompat(1); + testReadCompat(-1); + testReadCompat(Integer.MIN_VALUE); + testReadCompat(Integer.MAX_VALUE); + } + + /** + * Implementation of testReadCompat with a given value. + */ + private void testReadCompat(int val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_INT32; + final long fieldId = fieldFlags | ((long) 30 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.int32Field = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + int result = 0; // start off with default value + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result = pi.readInt(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.int32Field, result); + } + + public void testRepeated() throws IOException { + testRepeated(0); + testRepeated(1); + testRepeated(5); + } + + private void testRepeated(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_INT32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x08, + (byte) 0x00, + // 2 -> 1 + (byte) 0x10, + (byte) 0x01, + // 3 -> -1 + (byte) 0x18, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 4 -> MIN_VALUE + (byte) 0x20, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 5 -> MAX_VALUE + (byte) 0x28, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + + // 6 -> MAX_VALUE + (byte) 0x30, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + + // 1 -> 0 - default value, written when repeated + (byte) 0x08, + (byte) 0x00, + // 2 -> 1 + (byte) 0x10, + (byte) 0x01, + // 3 -> -1 + (byte) 0x18, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 4 -> MIN_VALUE + (byte) 0x20, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 5 -> MAX_VALUE + (byte) 0x28, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + int[][] results = new int[5][2]; + int[] indices = new int[5]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readInt(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readInt(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readInt(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readInt(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readInt(fieldId5); + break; + case (int) fieldId6: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + + assertEquals(0, results[0][0]); + assertEquals(0, results[0][1]); + assertEquals(1, results[1][0]); + assertEquals(1, results[1][1]); + assertEquals(-1, results[2][0]); + assertEquals(-1, results[2][1]); + assertEquals(Integer.MIN_VALUE, results[3][0]); + assertEquals(Integer.MIN_VALUE, results[3][1]); + assertEquals(Integer.MAX_VALUE, results[4][0]); + assertEquals(Integer.MAX_VALUE, results[4][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testRepeatedCompat() throws Exception { + testRepeatedCompat(new int[0]); + testRepeatedCompat(new int[]{0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE}); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testRepeatedCompat(int[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_INT32; + final long fieldId = fieldFlags | ((long) 31 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.int32FieldRepeated = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + int[] result = new int[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readInt(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.int32FieldRepeated.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.int32FieldRepeated[i], result[i]); + } + } + + public void testPacked() throws IOException { + testPacked(0); + testPacked(1); + testPacked(5); + } + + private void testPacked(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_PACKED | ProtoStream.FIELD_TYPE_INT32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x0a, + (byte) 0x02, + (byte) 0x00, + (byte) 0x00, + // 2 -> 1 + (byte) 0x12, + (byte) 0x02, + (byte) 0x01, + (byte) 0x01, + + // 6 -> MAX_VALUE + (byte) 0x32, + (byte) 0x0a, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + + // 3 -> -1 + (byte) 0x1a, + (byte) 0x14, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + + // 4 -> MIN_VALUE + (byte) 0x22, + (byte) 0x14, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + + // 5 -> MAX_VALUE + (byte) 0x2a, + (byte) 0x0a, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + int[][] results = new int[5][2]; + int[] indices = new int[5]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readInt(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readInt(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readInt(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readInt(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readInt(fieldId5); + break; + case (int) fieldId6: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + + assertEquals(0, results[0][0]); + assertEquals(0, results[0][1]); + assertEquals(1, results[1][0]); + assertEquals(1, results[1][1]); + assertEquals(-1, results[2][0]); + assertEquals(-1, results[2][1]); + assertEquals(Integer.MIN_VALUE, results[3][0]); + assertEquals(Integer.MIN_VALUE, results[3][1]); + assertEquals(Integer.MAX_VALUE, results[4][0]); + assertEquals(Integer.MAX_VALUE, results[4][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testPackedCompat() throws Exception { + testPackedCompat(new int[0]); + testPackedCompat(new int[]{0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE}); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testPackedCompat(int[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_INT32; + final long fieldId = fieldFlags | ((long) 32 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.int32FieldPacked = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + int[] result = new int[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readInt(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.int32FieldPacked.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.int32FieldPacked[i], result[i]); + } + } + + /** + * Test that using the wrong read method throws an exception + */ + public void testBadReadType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_INT32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 1 + (byte) 0x08, + (byte) 0x01, + }; + + ProtoInputStream pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readFloat(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readDouble(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBoolean(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readLong(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBytes(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readString(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + } + + /** + * Test that unexpected wrong wire types will throw an exception + */ + public void testBadWireType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_INT32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 : varint -> 1 + (byte) 0x08, + (byte) 0x01, + // 2 : fixed64 -> 0x1 + (byte) 0x11, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 : length delimited -> { 1 } + (byte) 0x1a, + (byte) 0x01, + (byte) 0x01, + // 6 : fixed32 + (byte) 0x35, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream); + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + try { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + pi.readInt(fieldId1); + // don't fail, varint is ok + break; + case (int) fieldId2: + pi.readInt(fieldId2); + fail("Should have thrown a WireTypeMismatchException"); + break; + case (int) fieldId3: + pi.readInt(fieldId3); + // don't fail, length delimited is ok (represents packed int32) + break; + case (int) fieldId6: + pi.readInt(fieldId6); + fail("Should have thrown a WireTypeMismatchException"); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } catch (WireTypeMismatchException wtme) { + // good + } + } + stream.close(); + } +} diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamInt64Test.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamInt64Test.java new file mode 100644 index 000000000000..4d6d105e60b0 --- /dev/null +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamInt64Test.java @@ -0,0 +1,645 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.protoinputstream; + +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoStream; +import android.util.proto.WireTypeMismatchException; + +import com.android.test.protoinputstream.nano.Test; + +import com.google.protobuf.nano.MessageNano; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ProtoInputStreamInt64Test extends TestCase { + + public void testRead() throws IOException { + testRead(0); + testRead(1); + testRead(5); + } + + private void testRead(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_INT64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + final long fieldId7 = fieldFlags | ((long) 7 & 0x0ffffffffL); + final long fieldId8 = fieldFlags | ((long) 8 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, not written + // 2 -> 1 + (byte) 0x10, + (byte) 0x01, + // 8 -> Long.MAX_VALUE + (byte) 0x40, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + // 3 -> -1 + (byte) 0x18, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 4 -> Integer.MIN_VALUE + (byte) 0x20, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 5 -> Integer.MAX_VALUE + (byte) 0x28, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + // 6 -> Long.MIN_VALUE + (byte) 0x30, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x01, + // 7 -> Long.MAX_VALUE + (byte) 0x38, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + long[] results = new long[7]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + fail("Should never reach this"); + break; + case (int) fieldId2: + results[1] = pi.readLong(fieldId2); + break; + case (int) fieldId3: + results[2] = pi.readLong(fieldId3); + break; + case (int) fieldId4: + results[3] = pi.readLong(fieldId4); + break; + case (int) fieldId5: + results[4] = pi.readLong(fieldId5); + break; + case (int) fieldId6: + results[5] = pi.readLong(fieldId6); + break; + case (int) fieldId7: + results[6] = pi.readLong(fieldId7); + break; + case (int) fieldId8: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals(0, results[0]); + assertEquals(1, results[1]); + assertEquals(-1, results[2]); + assertEquals(Integer.MIN_VALUE, results[3]); + assertEquals(Integer.MAX_VALUE, results[4]); + assertEquals(Long.MIN_VALUE, results[5]); + assertEquals(Long.MAX_VALUE, results[6]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testReadCompat() throws Exception { + testReadCompat(0); + testReadCompat(1); + testReadCompat(-1); + testReadCompat(Integer.MIN_VALUE); + testReadCompat(Integer.MAX_VALUE); + testReadCompat(Long.MIN_VALUE); + testReadCompat(Long.MAX_VALUE); + } + + /** + * Implementation of testReadCompat with a given value. + */ + private void testReadCompat(long val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_INT64; + final long fieldId = fieldFlags | ((long) 40 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.int64Field = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + long result = 0; // start off with default value + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result = pi.readLong(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.int64Field, result); + } + + public void testRepeated() throws IOException { + testRepeated(0); + testRepeated(1); + testRepeated(5); + } + + private void testRepeated(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_INT64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + final long fieldId7 = fieldFlags | ((long) 7 & 0x0ffffffffL); + final long fieldId8 = fieldFlags | ((long) 8 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x08, + (byte) 0x00, + // 2 -> 1 + (byte) 0x10, + (byte) 0x01, + // 3 -> -1 + (byte) 0x18, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 4 -> Integer.MIN_VALUE + (byte) 0x20, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 5 -> Integer.MAX_VALUE + (byte) 0x28, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + // 6 -> Long.MIN_VALUE + (byte) 0x30, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x01, + // 7 -> Long.MAX_VALUE + (byte) 0x38, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + + // 8 -> Long.MAX_VALUE + (byte) 0x40, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + + // 1 -> 0 - default value, written when repeated + (byte) 0x08, + (byte) 0x00, + // 2 -> 1 + (byte) 0x10, + (byte) 0x01, + // 3 -> -1 + (byte) 0x18, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 4 -> Integer.MIN_VALUE + (byte) 0x20, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 5 -> Integer.MAX_VALUE + (byte) 0x28, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + // 6 -> Long.MIN_VALUE + (byte) 0x30, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x01, + // 7 -> Long.MAX_VALUE + (byte) 0x38, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + long[][] results = new long[7][2]; + int[] indices = new int[7]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readLong(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readLong(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readLong(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readLong(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readLong(fieldId5); + break; + case (int) fieldId6: + results[5][indices[5]++] = pi.readLong(fieldId6); + break; + case (int) fieldId7: + results[6][indices[6]++] = pi.readLong(fieldId7); + break; + case (int) fieldId8: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals(0, results[0][0]); + assertEquals(0, results[0][1]); + assertEquals(1, results[1][0]); + assertEquals(1, results[1][1]); + assertEquals(-1, results[2][0]); + assertEquals(-1, results[2][1]); + assertEquals(Integer.MIN_VALUE, results[3][0]); + assertEquals(Integer.MIN_VALUE, results[3][1]); + assertEquals(Integer.MAX_VALUE, results[4][0]); + assertEquals(Integer.MAX_VALUE, results[4][1]); + assertEquals(Long.MIN_VALUE, results[5][0]); + assertEquals(Long.MIN_VALUE, results[5][1]); + assertEquals(Long.MAX_VALUE, results[6][0]); + assertEquals(Long.MAX_VALUE, results[6][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testRepeatedCompat() throws Exception { + testRepeatedCompat(new long[0]); + testRepeatedCompat(new long[]{0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE}); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testRepeatedCompat(long[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_INT64; + final long fieldId = fieldFlags | ((long) 41 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.int64FieldRepeated = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + long[] result = new long[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readLong(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.int64FieldRepeated.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.int64FieldRepeated[i], result[i]); + } + } + + public void testPacked() throws IOException { + testPacked(0); + testPacked(1); + testPacked(5); + } + + private void testPacked(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_PACKED | ProtoStream.FIELD_TYPE_INT64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + final long fieldId7 = fieldFlags | ((long) 7 & 0x0ffffffffL); + final long fieldId8 = fieldFlags | ((long) 8 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x0a, + (byte) 0x02, + (byte) 0x00, + (byte) 0x00, + // 2 -> 1 + (byte) 0x12, + (byte) 0x02, + (byte) 0x01, + (byte) 0x01, + + // 8 -> Long.MAX_VALUE + (byte) 0x42, + (byte) 0x12, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + + // 3 -> -1 + (byte) 0x1a, + (byte) 0x14, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + + // 4 -> Integer.MIN_VALUE + (byte) 0x22, + (byte) 0x14, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + + // 5 -> Integer.MAX_VALUE + (byte) 0x2a, + (byte) 0x0a, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + + // 6 -> Long.MIN_VALUE + (byte) 0x32, + (byte) 0x14, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x01, + + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x01, + + // 7 -> Long.MAX_VALUE + (byte) 0x3a, + (byte) 0x12, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + long[][] results = new long[7][2]; + int[] indices = new int[7]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readLong(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readLong(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readLong(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readLong(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readLong(fieldId5); + break; + case (int) fieldId6: + results[5][indices[5]++] = pi.readLong(fieldId6); + break; + case (int) fieldId7: + results[6][indices[6]++] = pi.readLong(fieldId7); + break; + case (int) fieldId8: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals(0, results[0][0]); + assertEquals(0, results[0][1]); + assertEquals(1, results[1][0]); + assertEquals(1, results[1][1]); + assertEquals(-1, results[2][0]); + assertEquals(-1, results[2][1]); + assertEquals(Integer.MIN_VALUE, results[3][0]); + assertEquals(Integer.MIN_VALUE, results[3][1]); + assertEquals(Integer.MAX_VALUE, results[4][0]); + assertEquals(Integer.MAX_VALUE, results[4][1]); + assertEquals(Long.MIN_VALUE, results[5][0]); + assertEquals(Long.MIN_VALUE, results[5][1]); + assertEquals(Long.MAX_VALUE, results[6][0]); + assertEquals(Long.MAX_VALUE, results[6][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testPackedCompat() throws Exception { + testPackedCompat(new long[0]); + testPackedCompat(new long[]{0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE}); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testPackedCompat(long[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_INT64; + final long fieldId = fieldFlags | ((long) 42 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.int64FieldPacked = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + long[] result = new long[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readLong(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.int64FieldPacked.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.int64FieldPacked[i], result[i]); + } + } + + /** + * Test that using the wrong read method throws an exception + */ + public void testBadReadType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_INT64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 1 + (byte) 0x08, + (byte) 0x01, + }; + + ProtoInputStream pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readFloat(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readDouble(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readInt(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBoolean(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBytes(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readString(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + } + + /** + * Test that unexpected wrong wire types will throw an exception + */ + public void testBadWireType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_INT64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 : varint -> 1 + (byte) 0x08, + (byte) 0x01, + // 2 : fixed64 -> 0x1 + (byte) 0x11, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 : length delimited -> { 1 } + (byte) 0x1a, + (byte) 0x01, + (byte) 0x01, + // 6 : fixed32 + (byte) 0x35, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream); + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + try { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + pi.readLong(fieldId1); + // don't fail, varint is ok + break; + case (int) fieldId2: + pi.readLong(fieldId2); + fail("Should have thrown a WireTypeMismatchException"); + break; + case (int) fieldId3: + pi.readLong(fieldId3); + // don't fail, length delimited is ok (represents packed int64) + break; + case (int) fieldId6: + pi.readLong(fieldId6); + fail("Should have thrown a WireTypeMismatchException"); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } catch (WireTypeMismatchException wtme) { + // good + } + } + stream.close(); + } +} diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamObjectTest.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamObjectTest.java new file mode 100644 index 000000000000..5e49eeafb8af --- /dev/null +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamObjectTest.java @@ -0,0 +1,507 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.protoinputstream; + +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoStream; +import android.util.proto.WireTypeMismatchException; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ProtoInputStreamObjectTest extends TestCase { + + + class SimpleObject { + public char mChar; + public char mLargeChar; + public String mString; + public SimpleObject mNested; + + void parseProto(ProtoInputStream pi) throws IOException { + final long uintFieldFlags = + ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_UINT32; + final long stringFieldFlags = + ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_STRING; + final long messageFieldFlags = + ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_MESSAGE; + final long charId = uintFieldFlags | ((long) 2 & 0x0ffffffffL); + final long largeCharId = uintFieldFlags | ((long) 5000 & 0x0ffffffffL); + final long stringId = stringFieldFlags | ((long) 4 & 0x0ffffffffL); + final long nestedId = messageFieldFlags | ((long) 5 & 0x0ffffffffL); + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) charId: + mChar = (char) pi.readInt(charId); + break; + case (int) largeCharId: + mLargeChar = (char) pi.readInt(largeCharId); + break; + case (int) stringId: + mString = pi.readString(stringId); + break; + case (int) nestedId: + long token = pi.start(nestedId); + mNested = new SimpleObject(); + mNested.parseProto(pi); + pi.end(token); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + } + + } + + /** + * Test reading an object with one char in it. + */ + public void testObjectOneChar() throws IOException { + testObjectOneChar(0); + testObjectOneChar(1); + testObjectOneChar(5); + } + + /** + * Implementation of testObjectOneChar for a given chunkSize. + */ + private void testObjectOneChar(int chunkSize) throws IOException { + final long messageFieldFlags = + ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_MESSAGE; + + final long messageId1 = messageFieldFlags | ((long) 1 & 0x0ffffffffL); + final long messageId2 = messageFieldFlags | ((long) 2 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // Message 2 : { char 2 : 'c' } + (byte) 0x12, (byte) 0x02, (byte) 0x10, (byte) 0x63, + // Message 1 : { char 2 : 'b' } + (byte) 0x0a, (byte) 0x02, (byte) 0x10, (byte) 0x62, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + + SimpleObject result = null; + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) messageId1: + final long token = pi.start(messageId1); + result = new SimpleObject(); + result.parseProto(pi); + pi.end(token); + break; + case (int) messageId2: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertNotNull(result); + assertEquals('b', result.mChar); + } + + /** + * Test reading an object with one multibyte unicode char in it. + */ + public void testObjectOneLargeChar() throws IOException { + testObjectOneLargeChar(0); + testObjectOneLargeChar(1); + testObjectOneLargeChar(5); + } + + /** + * Implementation of testObjectOneLargeChar for a given chunkSize. + */ + private void testObjectOneLargeChar(int chunkSize) throws IOException { + final long messageFieldFlags = + ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_MESSAGE; + + final long messageId1 = messageFieldFlags | ((long) 1 & 0x0ffffffffL); + final long messageId2 = messageFieldFlags | ((long) 2 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // Message 2 : { char 5000 : '\u3110' } + (byte) 0x12, (byte) 0x05, (byte) 0xc0, (byte) 0xb8, + (byte) 0x02, (byte) 0x90, (byte) 0x62, + // Message 1 : { char 5000 : '\u3110' } + (byte) 0x0a, (byte) 0x05, (byte) 0xc0, (byte) 0xb8, + (byte) 0x02, (byte) 0x90, (byte) 0x62, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + + SimpleObject result = null; + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) messageId1: + final long token = pi.start(messageId1); + result = new SimpleObject(); + result.parseProto(pi); + pi.end(token); + break; + case (int) messageId2: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertNotNull(result); + assertEquals('\u3110', result.mLargeChar); + } + + /** + * Test reading a char, then an object, then a char. + */ + public void testObjectAndTwoChars() throws IOException { + testObjectAndTwoChars(0); + testObjectAndTwoChars(1); + testObjectAndTwoChars(5); + } + + /** + * Implementation of testObjectAndTwoChars for a given chunkSize. + */ + private void testObjectAndTwoChars(int chunkSize) throws IOException { + final long uintFieldFlags = + ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_UINT32; + final long messageFieldFlags = + ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_MESSAGE; + + final long charId1 = uintFieldFlags | ((long) 1 & 0x0ffffffffL); + final long messageId2 = messageFieldFlags | ((long) 2 & 0x0ffffffffL); + final long charId4 = uintFieldFlags | ((long) 4 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 'a' + (byte) 0x08, (byte) 0x61, + // Message 1 : { char 2 : 'b' } + (byte) 0x12, (byte) 0x02, (byte) 0x10, (byte) 0x62, + // 4 -> 'c' + (byte) 0x20, (byte) 0x63, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + + SimpleObject obj = null; + char char1 = '\0'; + char char4 = '\0'; + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) charId1: + char1 = (char) pi.readInt(charId1); + break; + case (int) messageId2: + final long token = pi.start(messageId2); + obj = new SimpleObject(); + obj.parseProto(pi); + pi.end(token); + break; + case (int) charId4: + char4 = (char) pi.readInt(charId4); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals('a', char1); + assertNotNull(obj); + assertEquals('b', obj.mChar); + assertEquals('c', char4); + } + + /** + * Test reading a char, then an object with an int and a string in it, then a char. + */ + public void testComplexObject() throws IOException { + testComplexObject(0); + testComplexObject(1); + testComplexObject(5); + } + + /** + * Implementation of testComplexObject for a given chunkSize. + */ + private void testComplexObject(int chunkSize) throws IOException { + final long uintFieldFlags = + ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_UINT32; + final long messageFieldFlags = + ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_MESSAGE; + + final long charId1 = uintFieldFlags | ((long) 1 & 0x0ffffffffL); + final long messageId2 = messageFieldFlags | ((long) 2 & 0x0ffffffffL); + final long charId4 = uintFieldFlags | ((long) 4 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 'x' + (byte) 0x08, (byte) 0x78, + // begin object 2 + (byte) 0x12, (byte) 0x10, + // 2 -> 'y' + (byte) 0x10, (byte) 0x79, + // 4 -> "abcdefghijkl" + (byte) 0x22, (byte) 0x0c, + (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, + (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6a, (byte) 0x6b, (byte) 0x6c, + // 4 -> 'z' + (byte) 0x20, (byte) 0x7a, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + + SimpleObject obj = null; + char char1 = '\0'; + char char4 = '\0'; + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) charId1: + char1 = (char) pi.readInt(charId1); + break; + case (int) messageId2: + final long token = pi.start(messageId2); + obj = new SimpleObject(); + obj.parseProto(pi); + pi.end(token); + break; + case (int) charId4: + char4 = (char) pi.readInt(charId4); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals('x', char1); + assertNotNull(obj); + assertEquals('y', obj.mChar); + assertEquals("abcdefghijkl", obj.mString); + assertEquals('z', char4); + } + + /** + * Test reading 3 levels deep of objects. + */ + public void testDeepObjects() throws IOException { + testDeepObjects(0); + testDeepObjects(1); + testDeepObjects(5); + } + + /** + * Implementation of testDeepObjects for a given chunkSize. + */ + private void testDeepObjects(int chunkSize) throws IOException { + final long messageFieldFlags = + ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_MESSAGE; + final long messageId2 = messageFieldFlags | ((long) 2 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // begin object id 2 + (byte) 0x12, (byte) 0x1a, + // 2 -> 'a' + (byte) 0x10, (byte) 0x61, + // begin nested object id 5 + (byte) 0x2a, (byte) 0x15, + // 5000 -> '\u3110' + (byte) 0xc0, (byte) 0xb8, + (byte) 0x02, (byte) 0x90, (byte) 0x62, + // begin nested object id 5 + (byte) 0x2a, (byte) 0x0e, + // 4 -> "abcdefghijkl" + (byte) 0x22, (byte) 0x0c, + (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, + (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6a, (byte) 0x6b, (byte) 0x6c, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + + SimpleObject obj = null; + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) messageId2: + final long token = pi.start(messageId2); + obj = new SimpleObject(); + obj.parseProto(pi); + pi.end(token); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertNotNull(obj); + assertEquals('a', obj.mChar); + assertNotNull(obj.mNested); + assertEquals('\u3110', obj.mNested.mLargeChar); + assertNotNull(obj.mNested.mNested); + assertEquals("abcdefghijkl", obj.mNested.mNested.mString); + } + + /** + * Test that using the wrong read method throws an exception + */ + public void testBadReadType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_MESSAGE; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> {1} + (byte) 0x0a, + (byte) 0x01, + (byte) 0x01, + }; + + ProtoInputStream pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readFloat(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readDouble(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readInt(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readLong(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBoolean(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readString(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + } + + /** + * Test that unexpected wrong wire types will throw an exception + */ + public void testBadWireType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_MESSAGE; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 : varint -> 1 + (byte) 0x08, + (byte) 0x01, + // 2 : fixed64 -> 0x1 + (byte) 0x11, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 : length delimited -> { 1 } + (byte) 0x1a, + (byte) 0x01, + (byte) 0x01, + // 6 : fixed32 + (byte) 0x35, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream); + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + try { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + pi.readBytes(fieldId1); + fail("Should have thrown a WireTypeMismatchException"); + break; + case (int) fieldId2: + pi.readBytes(fieldId2); + fail("Should have thrown a WireTypeMismatchException"); + break; + case (int) fieldId3: + pi.readBytes(fieldId3); + // don't fail, length delimited is ok + break; + case (int) fieldId6: + pi.readBytes(fieldId6); + fail("Should have thrown a WireTypeMismatchException"); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } catch (WireTypeMismatchException wtme) { + // good + } + } + stream.close(); + } +} diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSFixed32Test.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSFixed32Test.java new file mode 100644 index 000000000000..75c88a44614b --- /dev/null +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSFixed32Test.java @@ -0,0 +1,547 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.protoinputstream; + +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoStream; +import android.util.proto.WireTypeMismatchException; + +import com.android.test.protoinputstream.nano.Test; + +import com.google.protobuf.nano.MessageNano; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ProtoInputStreamSFixed32Test extends TestCase { + + public void testRead() throws IOException { + testRead(0); + testRead(1); + testRead(5); + } + + private void testRead(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_SFIXED32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, not written + // 2 -> 1 + (byte) 0x15, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 6 -> 1 + (byte) 0x35, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 -> -1 + (byte) 0x1d, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // 4 -> Integer.MIN_VALUE + (byte) 0x25, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + // 5 -> Integer.MAX_VALUE + (byte) 0x2d, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + int[] results = new int[5]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + fail("Should never reach this"); + break; + case (int) fieldId2: + results[1] = pi.readInt(fieldId2); + break; + case (int) fieldId3: + results[2] = pi.readInt(fieldId3); + break; + case (int) fieldId4: + results[3] = pi.readInt(fieldId4); + break; + case (int) fieldId5: + results[4] = pi.readInt(fieldId5); + break; + case (int) fieldId6: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals(0, results[0]); + assertEquals(1, results[1]); + assertEquals(-1, results[2]); + assertEquals(Integer.MIN_VALUE, results[3]); + assertEquals(Integer.MAX_VALUE, results[4]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testReadCompat() throws Exception { + testReadCompat(0); + testReadCompat(1); + testReadCompat(-1); + testReadCompat(Integer.MIN_VALUE); + testReadCompat(Integer.MAX_VALUE); + } + + /** + * Implementation of testReadCompat with a given value. + */ + private void testReadCompat(int val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_SFIXED32; + final long fieldId = fieldFlags | ((long) 110 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.sfixed32Field = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + int result = 0; // start off with default value + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result = pi.readInt(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.sfixed32Field, result); + } + + public void testRepeated() throws IOException { + testRepeated(0); + testRepeated(1); + testRepeated(5); + } + + private void testRepeated(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_SFIXED32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x0d, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 2 -> 1 + (byte) 0x15, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 -> -1 + (byte) 0x1d, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // 4 -> Integer.MIN_VALUE + (byte) 0x25, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + // 5 -> Integer.MAX_VALUE + (byte) 0x2d, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + + // 6 -> 1 + (byte) 0x35, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + + // 1 -> 0 - default value, written when repeated + (byte) 0x0d, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 2 -> 1 + (byte) 0x15, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 -> -1 + (byte) 0x1d, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // 4 -> Integer.MIN_VALUE + (byte) 0x25, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + // 5 -> Integer.MAX_VALUE + (byte) 0x2d, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + int[][] results = new int[5][2]; + int[] indices = new int[5]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readInt(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readInt(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readInt(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readInt(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readInt(fieldId5); + break; + case (int) fieldId6: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + + assertEquals(0, results[0][0]); + assertEquals(0, results[0][1]); + assertEquals(1, results[1][0]); + assertEquals(1, results[1][1]); + assertEquals(-1, results[2][0]); + assertEquals(-1, results[2][1]); + assertEquals(Integer.MIN_VALUE, results[3][0]); + assertEquals(Integer.MIN_VALUE, results[3][1]); + assertEquals(Integer.MAX_VALUE, results[4][0]); + assertEquals(Integer.MAX_VALUE, results[4][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testRepeatedCompat() throws Exception { + testRepeatedCompat(new int[0]); + testRepeatedCompat(new int[]{0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE}); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testRepeatedCompat(int[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_SFIXED32; + final long fieldId = fieldFlags | ((long) 111 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.sfixed32FieldRepeated = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + int[] result = new int[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readInt(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.sfixed32FieldRepeated.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.sfixed32FieldRepeated[i], result[i]); + } + } + + public void testPacked() throws IOException { + testPacked(0); + testPacked(1); + testPacked(5); + } + + private void testPacked(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_PACKED | ProtoStream.FIELD_TYPE_SFIXED32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x0a, + (byte) 0x08, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 2 -> 1 + (byte) 0x12, + (byte) 0x08, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 6 -> 1 + (byte) 0x32, + (byte) 0x08, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 -> -1 + (byte) 0x1a, + (byte) 0x08, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // 4 -> Integer.MIN_VALUE + (byte) 0x22, + (byte) 0x08, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + // 5 -> Integer.MAX_VALUE + (byte) 0x2a, + (byte) 0x08, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + int[][] results = new int[5][2]; + int[] indices = new int[5]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readInt(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readInt(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readInt(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readInt(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readInt(fieldId5); + break; + case (int) fieldId6: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + + assertEquals(0, results[0][0]); + assertEquals(0, results[0][1]); + assertEquals(1, results[1][0]); + assertEquals(1, results[1][1]); + assertEquals(-1, results[2][0]); + assertEquals(-1, results[2][1]); + assertEquals(Integer.MIN_VALUE, results[3][0]); + assertEquals(Integer.MIN_VALUE, results[3][1]); + assertEquals(Integer.MAX_VALUE, results[4][0]); + assertEquals(Integer.MAX_VALUE, results[4][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testPackedCompat() throws Exception { + testPackedCompat(new int[0]); + testPackedCompat(new int[]{0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE}); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testPackedCompat(int[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_SFIXED32; + final long fieldId = fieldFlags | ((long) 112 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.sfixed32FieldPacked = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + int[] result = new int[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readInt(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.sfixed32FieldPacked.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.sfixed32FieldPacked[i], result[i]); + } + } + + /** + * Test that using the wrong read method throws an exception + */ + public void testBadReadType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_SFIXED32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 1 + (byte) 0x08, + (byte) 0x01, + }; + + ProtoInputStream pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readFloat(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readDouble(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBoolean(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readLong(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBytes(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readString(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + } + + /** + * Test that unexpected wrong wire types will throw an exception + */ + public void testBadWireType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_SFIXED32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 : varint -> 1 + (byte) 0x08, + (byte) 0x01, + // 2 : fixed64 -> 0x1 + (byte) 0x11, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 : length delimited -> { 1 } + (byte) 0x1a, + (byte) 0x04, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 6 : fixed32 + (byte) 0x35, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream); + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + try { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + pi.readInt(fieldId1); + fail("Should have thrown a WireTypeMismatchException"); + break; + case (int) fieldId2: + pi.readInt(fieldId2); + fail("Should have thrown a WireTypeMismatchException"); + break; + case (int) fieldId3: + pi.readInt(fieldId3); + // don't fail, length delimited is ok (represents packed sfixed32) + break; + case (int) fieldId6: + pi.readInt(fieldId6); + // don't fail, fixed32 is ok + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } catch (WireTypeMismatchException wtme) { + // good + } + } + stream.close(); + } +} diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSFixed64Test.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSFixed64Test.java new file mode 100644 index 000000000000..4c65cf49318d --- /dev/null +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSFixed64Test.java @@ -0,0 +1,648 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.protoinputstream; + +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoStream; +import android.util.proto.WireTypeMismatchException; + +import com.android.test.protoinputstream.nano.Test; + +import com.google.protobuf.nano.MessageNano; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ProtoInputStreamSFixed64Test extends TestCase { + + public void testRead() throws IOException { + testRead(0); + testRead(1); + testRead(5); + } + + private void testRead(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_SFIXED64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + final long fieldId7 = fieldFlags | ((long) 7 & 0x0ffffffffL); + final long fieldId8 = fieldFlags | ((long) 8 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, not written + // 2 -> 1 + (byte) 0x11, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 8 -> 1 + (byte) 0x41, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 -> -1 + (byte) 0x19, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // 4 -> Integer.MIN_VALUE + (byte) 0x21, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // 5 -> Integer.MAX_VALUE + (byte) 0x29, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 6 -> Long.MIN_VALUE + (byte) 0x31, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + // 7 -> Long.MAX_VALUE + (byte) 0x39, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + long[] results = new long[7]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + fail("Should never reach this"); + break; + case (int) fieldId2: + results[1] = pi.readLong(fieldId2); + break; + case (int) fieldId3: + results[2] = pi.readLong(fieldId3); + break; + case (int) fieldId4: + results[3] = pi.readLong(fieldId4); + break; + case (int) fieldId5: + results[4] = pi.readLong(fieldId5); + break; + case (int) fieldId6: + results[5] = pi.readLong(fieldId6); + break; + case (int) fieldId7: + results[6] = pi.readLong(fieldId7); + break; + case (int) fieldId8: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals(0, results[0]); + assertEquals(1, results[1]); + assertEquals(-1, results[2]); + assertEquals(Integer.MIN_VALUE, results[3]); + assertEquals(Integer.MAX_VALUE, results[4]); + assertEquals(Long.MIN_VALUE, results[5]); + assertEquals(Long.MAX_VALUE, results[6]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testReadCompat() throws Exception { + testReadCompat(0); + testReadCompat(1); + testReadCompat(-1); + testReadCompat(Integer.MIN_VALUE); + testReadCompat(Integer.MAX_VALUE); + testReadCompat(Long.MIN_VALUE); + testReadCompat(Long.MAX_VALUE); + } + + /** + * Implementation of testReadCompat with a given value. + */ + private void testReadCompat(long val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_SFIXED64; + final long fieldId = fieldFlags | ((long) 120 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.sfixed64Field = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + long result = 0; // start off with default value + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result = pi.readLong(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.sfixed64Field, result); + } + + public void testRepeated() throws IOException { + testRepeated(0); + testRepeated(1); + testRepeated(5); + } + + private void testRepeated(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_SFIXED64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + final long fieldId7 = fieldFlags | ((long) 7 & 0x0ffffffffL); + final long fieldId8 = fieldFlags | ((long) 8 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x09, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 2 -> 1 + (byte) 0x11, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 -> -1 + (byte) 0x19, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // 4 -> Integer.MIN_VALUE + (byte) 0x21, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // 5 -> Integer.MAX_VALUE + (byte) 0x29, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 6 -> Long.MIN_VALUE + (byte) 0x31, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + // 7 -> Long.MAX_VALUE + (byte) 0x39, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + + // 8 -> 1 + (byte) 0x41, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + + // 1 -> 0 - default value, written when repeated + (byte) 0x09, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 2 -> 1 + (byte) 0x11, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 -> -1 + (byte) 0x19, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // 4 -> Integer.MIN_VALUE + (byte) 0x21, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // 5 -> Integer.MAX_VALUE + (byte) 0x29, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 6 -> Long.MIN_VALUE + (byte) 0x31, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + // 7 -> Long.MAX_VALUE + (byte) 0x39, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + long[][] results = new long[7][2]; + int[] indices = new int[7]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readLong(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readLong(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readLong(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readLong(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readLong(fieldId5); + break; + case (int) fieldId6: + results[5][indices[5]++] = pi.readLong(fieldId6); + break; + case (int) fieldId7: + results[6][indices[6]++] = pi.readLong(fieldId7); + break; + case (int) fieldId8: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals(0, results[0][0]); + assertEquals(0, results[0][1]); + assertEquals(1, results[1][0]); + assertEquals(1, results[1][1]); + assertEquals(-1, results[2][0]); + assertEquals(-1, results[2][1]); + assertEquals(Integer.MIN_VALUE, results[3][0]); + assertEquals(Integer.MIN_VALUE, results[3][1]); + assertEquals(Integer.MAX_VALUE, results[4][0]); + assertEquals(Integer.MAX_VALUE, results[4][1]); + assertEquals(Long.MIN_VALUE, results[5][0]); + assertEquals(Long.MIN_VALUE, results[5][1]); + assertEquals(Long.MAX_VALUE, results[6][0]); + assertEquals(Long.MAX_VALUE, results[6][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testRepeatedCompat() throws Exception { + testRepeatedCompat(new long[0]); + testRepeatedCompat(new long[]{0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE}); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testRepeatedCompat(long[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_SFIXED64; + final long fieldId = fieldFlags | ((long) 121 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.sfixed64FieldRepeated = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + long[] result = new long[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readLong(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.sfixed64FieldRepeated.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.sfixed64FieldRepeated[i], result[i]); + } + } + + public void testPacked() throws IOException { + testPacked(0); + testPacked(1); + testPacked(5); + } + + private void testPacked(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_PACKED | ProtoStream.FIELD_TYPE_SFIXED64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + final long fieldId7 = fieldFlags | ((long) 7 & 0x0ffffffffL); + final long fieldId8 = fieldFlags | ((long) 8 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x0a, + (byte) 0x10, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 2 -> 1 + (byte) 0x12, + (byte) 0x10, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 8 -> 1 + (byte) 0x42, + (byte) 0x10, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 -> -1 + (byte) 0x1a, + (byte) 0x10, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // 4 -> Integer.MIN_VALUE + (byte) 0x22, + (byte) 0x10, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // 5 -> Integer.MAX_VALUE + (byte) 0x2a, + (byte) 0x10, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 6 -> Long.MIN_VALUE + (byte) 0x32, + (byte) 0x10, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + // 7 -> Long.MAX_VALUE + (byte) 0x3a, + (byte) 0x10, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + long[][] results = new long[7][2]; + int[] indices = new int[7]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readLong(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readLong(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readLong(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readLong(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readLong(fieldId5); + break; + case (int) fieldId6: + results[5][indices[5]++] = pi.readLong(fieldId6); + break; + case (int) fieldId7: + results[6][indices[6]++] = pi.readLong(fieldId7); + break; + case (int) fieldId8: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals(0, results[0][0]); + assertEquals(0, results[0][1]); + assertEquals(1, results[1][0]); + assertEquals(1, results[1][1]); + assertEquals(-1, results[2][0]); + assertEquals(-1, results[2][1]); + assertEquals(Integer.MIN_VALUE, results[3][0]); + assertEquals(Integer.MIN_VALUE, results[3][1]); + assertEquals(Integer.MAX_VALUE, results[4][0]); + assertEquals(Integer.MAX_VALUE, results[4][1]); + assertEquals(Long.MIN_VALUE, results[5][0]); + assertEquals(Long.MIN_VALUE, results[5][1]); + assertEquals(Long.MAX_VALUE, results[6][0]); + assertEquals(Long.MAX_VALUE, results[6][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testPackedCompat() throws Exception { + testPackedCompat(new long[0]); + testPackedCompat(new long[]{0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE}); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testPackedCompat(long[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_SFIXED64; + final long fieldId = fieldFlags | ((long) 122 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.sfixed64FieldPacked = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + long[] result = new long[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readLong(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.sfixed64FieldPacked.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.sfixed64FieldPacked[i], result[i]); + } + } + + /** + * Test that using the wrong read method throws an exception + */ + public void testBadReadType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_SFIXED64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 1 + (byte) 0x08, + (byte) 0x01, + }; + + ProtoInputStream pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readFloat(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readDouble(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readInt(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBoolean(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBytes(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readString(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + } + + /** + * Test that unexpected wrong wire types will throw an exception + */ + public void testBadWireType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_SFIXED64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 : varint -> 1 + (byte) 0x08, + (byte) 0x01, + // 2 : fixed64 -> 0x1 + (byte) 0x11, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 : length delimited -> { 1 } + (byte) 0x1a, + (byte) 0x08, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 6 : fixed32 + (byte) 0x35, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream); + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + try { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + pi.readLong(fieldId1); + fail("Should have thrown a WireTypeMismatchException"); + break; + case (int) fieldId2: + pi.readLong(fieldId2); + // don't fail, fixed32 is ok + break; + case (int) fieldId3: + pi.readLong(fieldId3); + // don't fail, length delimited is ok (represents packed sfixed64) + break; + case (int) fieldId6: + pi.readLong(fieldId6); + fail("Should have thrown a WireTypeMismatchException"); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } catch (WireTypeMismatchException wtme) { + // good + } + } + stream.close(); + } +} diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSInt32Test.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSInt32Test.java new file mode 100644 index 000000000000..6854cd8aad28 --- /dev/null +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSInt32Test.java @@ -0,0 +1,547 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.protoinputstream; + +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoStream; +import android.util.proto.WireTypeMismatchException; + +import com.android.test.protoinputstream.nano.Test; + +import com.google.protobuf.nano.MessageNano; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ProtoInputStreamSInt32Test extends TestCase { + + public void testRead() throws IOException { + testRead(0); + testRead(1); + testRead(5); + } + + private void testRead(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_SINT32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, not written + // 2 -> 1 + (byte) 0x10, + (byte) 0x02, + // 6 -> MAX_VALUE + (byte) 0x30, + (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + // 3 -> -1 + (byte) 0x18, + (byte) 0x01, + // 4 -> MIN_VALUE + (byte) 0x20, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + // 5 -> MAX_VALUE + (byte) 0x28, + (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + int[] results = new int[5]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + fail("Should never reach this"); + break; + case (int) fieldId2: + results[1] = pi.readInt(fieldId2); + break; + case (int) fieldId3: + results[2] = pi.readInt(fieldId3); + break; + case (int) fieldId4: + results[3] = pi.readInt(fieldId4); + break; + case (int) fieldId5: + results[4] = pi.readInt(fieldId5); + break; + case (int) fieldId6: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals(0, results[0]); + assertEquals(1, results[1]); + assertEquals(-1, results[2]); + assertEquals(Integer.MIN_VALUE, results[3]); + assertEquals(Integer.MAX_VALUE, results[4]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testReadCompat() throws Exception { + testReadCompat(0); + testReadCompat(1); + testReadCompat(-1); + testReadCompat(Integer.MIN_VALUE); + testReadCompat(Integer.MAX_VALUE); + } + + /** + * Implementation of testReadCompat with a given value. + */ + private void testReadCompat(int val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_SINT32; + final long fieldId = fieldFlags | ((long) 70 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.sint32Field = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + int result = 0; // start off with default value + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result = pi.readInt(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.sint32Field, result); + } + + public void testRepeated() throws IOException { + testRepeated(0); + testRepeated(1); + testRepeated(5); + } + + private void testRepeated(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_SINT32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x08, + (byte) 0x00, + // 2 -> 1 + (byte) 0x10, + (byte) 0x02, + // 3 -> -1 + (byte) 0x18, + (byte) 0x01, + // 4 -> MIN_VALUE + (byte) 0x20, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + // 5 -> MAX_VALUE + (byte) 0x28, + (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + + // 6 -> MAX_VALUE + (byte) 0x30, + (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + + // 1 -> 0 - default value, written when repeated + (byte) 0x08, + (byte) 0x00, + // 2 -> 1 + (byte) 0x10, + (byte) 0x02, + // 3 -> -1 + (byte) 0x18, + (byte) 0x01, + // 4 -> MIN_VALUE + (byte) 0x20, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + // 5 -> MAX_VALUE + (byte) 0x28, + (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + int[][] results = new int[5][2]; + int[] indices = new int[5]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readInt(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readInt(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readInt(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readInt(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readInt(fieldId5); + break; + case (int) fieldId6: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + + assertEquals(0, results[0][0]); + assertEquals(0, results[0][1]); + assertEquals(1, results[1][0]); + assertEquals(1, results[1][1]); + assertEquals(-1, results[2][0]); + assertEquals(-1, results[2][1]); + assertEquals(Integer.MIN_VALUE, results[3][0]); + assertEquals(Integer.MIN_VALUE, results[3][1]); + assertEquals(Integer.MAX_VALUE, results[4][0]); + assertEquals(Integer.MAX_VALUE, results[4][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testRepeatedCompat() throws Exception { + testRepeatedCompat(new int[0]); + testRepeatedCompat(new int[]{0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE}); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testRepeatedCompat(int[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_SINT32; + final long fieldId = fieldFlags | ((long) 71 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.sint32FieldRepeated = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + int[] result = new int[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readInt(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.sint32FieldRepeated.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.sint32FieldRepeated[i], result[i]); + } + } + + public void testPacked() throws IOException { + testPacked(0); + testPacked(1); + testPacked(5); + } + + private void testPacked(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_PACKED | ProtoStream.FIELD_TYPE_SINT32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x0a, + (byte) 0x02, + (byte) 0x00, + (byte) 0x00, + // 2 -> 1 + (byte) 0x12, + (byte) 0x02, + (byte) 0x02, + (byte) 0x02, + // 6 -> MAX_VALUE + (byte) 0x32, + (byte) 0x0a, + (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + // 3 -> -1 + (byte) 0x1a, + (byte) 0x02, + (byte) 0x01, + (byte) 0x01, + // 4 -> MIN_VALUE + (byte) 0x22, + (byte) 0x0a, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + // 5 -> MAX_VALUE + (byte) 0x2a, + (byte) 0x0a, + (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + int[][] results = new int[5][2]; + int[] indices = new int[5]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readInt(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readInt(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readInt(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readInt(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readInt(fieldId5); + break; + case (int) fieldId6: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + + assertEquals(0, results[0][0]); + assertEquals(0, results[0][1]); + assertEquals(1, results[1][0]); + assertEquals(1, results[1][1]); + assertEquals(-1, results[2][0]); + assertEquals(-1, results[2][1]); + assertEquals(Integer.MIN_VALUE, results[3][0]); + assertEquals(Integer.MIN_VALUE, results[3][1]); + assertEquals(Integer.MAX_VALUE, results[4][0]); + assertEquals(Integer.MAX_VALUE, results[4][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testPackedCompat() throws Exception { + testPackedCompat(new int[0]); + testPackedCompat(new int[]{0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE}); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testPackedCompat(int[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_SINT32; + final long fieldId = fieldFlags | ((long) 72 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.sint32FieldPacked = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + int[] result = new int[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readInt(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.sint32FieldPacked.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.sint32FieldPacked[i], result[i]); + } + } + + /** + * Test that using the wrong read method throws an exception + */ + public void testBadReadType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_SINT32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 1 + (byte) 0x08, + (byte) 0x01, + }; + + ProtoInputStream pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readFloat(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readDouble(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBoolean(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readLong(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBytes(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readString(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + } + + /** + * Test that unexpected wrong wire types will throw an exception + */ + public void testBadWireType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_SINT32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 : varint -> 1 + (byte) 0x08, + (byte) 0x01, + // 2 : fixed64 -> 0x1 + (byte) 0x11, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 : length delimited -> { 1 } + (byte) 0x1a, + (byte) 0x01, + (byte) 0x01, + // 6 : fixed32 + (byte) 0x35, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream); + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + try { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + pi.readInt(fieldId1); + // don't fail, varint is ok + break; + case (int) fieldId2: + pi.readInt(fieldId2); + fail("Should have thrown a WireTypeMismatchException"); + break; + case (int) fieldId3: + pi.readInt(fieldId3); + // don't fail, length delimited is ok (represents packed sint32) + break; + case (int) fieldId6: + pi.readInt(fieldId6); + fail("Should have thrown a WireTypeMismatchException"); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } catch (WireTypeMismatchException wtme) { + // good + } + } + stream.close(); + } +} diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSInt64Test.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSInt64Test.java new file mode 100644 index 000000000000..c53e9d72562a --- /dev/null +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSInt64Test.java @@ -0,0 +1,622 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.protoinputstream; + +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoStream; +import android.util.proto.WireTypeMismatchException; + +import com.android.test.protoinputstream.nano.Test; + +import com.google.protobuf.nano.MessageNano; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ProtoInputStreamSInt64Test extends TestCase { + + public void testRead() throws IOException { + testRead(0); + testRead(1); + testRead(5); + } + + private void testRead(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_SINT64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + final long fieldId7 = fieldFlags | ((long) 7 & 0x0ffffffffL); + final long fieldId8 = fieldFlags | ((long) 8 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, not written + // 2 -> 1 + (byte) 0x10, + (byte) 0x02, + // 8 -> Integer.MAX_VALUE + (byte) 0x40, + (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + // 3 -> -1 + (byte) 0x18, + (byte) 0x01, + // 4 -> Integer.MIN_VALUE + (byte) 0x20, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + // 5 -> Integer.MAX_VALUE + (byte) 0x28, + (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + // 6 -> Long.MIN_VALUE + (byte) 0x30, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 7 -> Long.MAX_VALUE + (byte) 0x38, + (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + long[] results = new long[7]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + fail("Should never reach this"); + break; + case (int) fieldId2: + results[1] = pi.readLong(fieldId2); + break; + case (int) fieldId3: + results[2] = pi.readLong(fieldId3); + break; + case (int) fieldId4: + results[3] = pi.readLong(fieldId4); + break; + case (int) fieldId5: + results[4] = pi.readLong(fieldId5); + break; + case (int) fieldId6: + results[5] = pi.readLong(fieldId6); + break; + case (int) fieldId7: + results[6] = pi.readLong(fieldId7); + break; + case (int) fieldId8: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals(0, results[0]); + assertEquals(1, results[1]); + assertEquals(-1, results[2]); + assertEquals(Integer.MIN_VALUE, results[3]); + assertEquals(Integer.MAX_VALUE, results[4]); + assertEquals(Long.MIN_VALUE, results[5]); + assertEquals(Long.MAX_VALUE, results[6]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testReadCompat() throws Exception { + testReadCompat(0); + testReadCompat(1); + testReadCompat(-1); + testReadCompat(Integer.MIN_VALUE); + testReadCompat(Integer.MAX_VALUE); + testReadCompat(Long.MIN_VALUE); + testReadCompat(Long.MAX_VALUE); + } + + /** + * Implementation of testReadCompat with a given value. + */ + private void testReadCompat(long val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_SINT64; + final long fieldId = fieldFlags | ((long) 80 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.sint64Field = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + long result = 0; // start off with default value + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result = pi.readLong(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.sint64Field, result); + } + + public void testRepeated() throws IOException { + testRepeated(0); + testRepeated(1); + testRepeated(5); + } + + private void testRepeated(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_SINT64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + final long fieldId7 = fieldFlags | ((long) 7 & 0x0ffffffffL); + final long fieldId8 = fieldFlags | ((long) 8 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x08, + (byte) 0x00, + // 2 -> 1 + (byte) 0x10, + (byte) 0x02, + // 3 -> -1 + (byte) 0x18, + (byte) 0x01, + // 4 -> Integer.MIN_VALUE + (byte) 0x20, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + // 5 -> Integer.MAX_VALUE + (byte) 0x28, + (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + // 6 -> Long.MIN_VALUE + (byte) 0x30, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 7 -> Long.MAX_VALUE + (byte) 0x38, + (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + + // 8 -> Integer.MAX_VALUE + (byte) 0x40, + (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + + + // 1 -> 0 - default value, written when repeated + (byte) 0x08, + (byte) 0x00, + // 2 -> 1 + (byte) 0x10, + (byte) 0x02, + // 3 -> -1 + (byte) 0x18, + (byte) 0x01, + // 4 -> Integer.MIN_VALUE + (byte) 0x20, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + // 5 -> Integer.MAX_VALUE + (byte) 0x28, + (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + // 6 -> Long.MIN_VALUE + (byte) 0x30, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 7 -> Long.MAX_VALUE + (byte) 0x38, + (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + long[][] results = new long[7][2]; + int[] indices = new int[7]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readLong(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readLong(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readLong(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readLong(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readLong(fieldId5); + break; + case (int) fieldId6: + results[5][indices[5]++] = pi.readLong(fieldId6); + break; + case (int) fieldId7: + results[6][indices[6]++] = pi.readLong(fieldId7); + break; + case (int) fieldId8: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals(0, results[0][0]); + assertEquals(0, results[0][1]); + assertEquals(1, results[1][0]); + assertEquals(1, results[1][1]); + assertEquals(-1, results[2][0]); + assertEquals(-1, results[2][1]); + assertEquals(Integer.MIN_VALUE, results[3][0]); + assertEquals(Integer.MIN_VALUE, results[3][1]); + assertEquals(Integer.MAX_VALUE, results[4][0]); + assertEquals(Integer.MAX_VALUE, results[4][1]); + assertEquals(Long.MIN_VALUE, results[5][0]); + assertEquals(Long.MIN_VALUE, results[5][1]); + assertEquals(Long.MAX_VALUE, results[6][0]); + assertEquals(Long.MAX_VALUE, results[6][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testRepeatedCompat() throws Exception { + testRepeatedCompat(new long[0]); + testRepeatedCompat(new long[]{0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE}); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testRepeatedCompat(long[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_SINT64; + final long fieldId = fieldFlags | ((long) 81 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.sint64FieldRepeated = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + long[] result = new long[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readLong(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.sint64FieldRepeated.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.sint64FieldRepeated[i], result[i]); + } + } + + public void testPacked() throws IOException { + testPacked(0); + testPacked(1); + testPacked(5); + } + + private void testPacked(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_PACKED | ProtoStream.FIELD_TYPE_SINT64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + final long fieldId7 = fieldFlags | ((long) 7 & 0x0ffffffffL); + final long fieldId8 = fieldFlags | ((long) 8 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x0a, + (byte) 0x02, + (byte) 0x00, + (byte) 0x00, + // 2 -> 1 + (byte) 0x12, + (byte) 0x02, + (byte) 0x02, + (byte) 0x02, + // 8 -> Integer.MAX_VALUE + (byte) 0x42, + (byte) 0x0a, + (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + // 3 -> -1 + (byte) 0x1a, + (byte) 0x02, + (byte) 0x01, + (byte) 0x01, + // 4 -> Integer.MIN_VALUE + (byte) 0x22, + (byte) 0x0a, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + // 5 -> Integer.MAX_VALUE + (byte) 0x2a, + (byte) 0x0a, + (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x0f, + // 6 -> Long.MIN_VALUE + (byte) 0x32, + (byte) 0x14, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 7 -> Long.MAX_VALUE + (byte) 0x3a, + (byte) 0x14, + (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + + (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01 + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + long[][] results = new long[7][2]; + int[] indices = new int[7]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readLong(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readLong(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readLong(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readLong(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readLong(fieldId5); + break; + case (int) fieldId6: + results[5][indices[5]++] = pi.readLong(fieldId6); + break; + case (int) fieldId7: + results[6][indices[6]++] = pi.readLong(fieldId7); + break; + case (int) fieldId8: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals(0, results[0][0]); + assertEquals(0, results[0][1]); + assertEquals(1, results[1][0]); + assertEquals(1, results[1][1]); + assertEquals(-1, results[2][0]); + assertEquals(-1, results[2][1]); + assertEquals(Integer.MIN_VALUE, results[3][0]); + assertEquals(Integer.MIN_VALUE, results[3][1]); + assertEquals(Integer.MAX_VALUE, results[4][0]); + assertEquals(Integer.MAX_VALUE, results[4][1]); + assertEquals(Long.MIN_VALUE, results[5][0]); + assertEquals(Long.MIN_VALUE, results[5][1]); + assertEquals(Long.MAX_VALUE, results[6][0]); + assertEquals(Long.MAX_VALUE, results[6][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testPackedCompat() throws Exception { + testPackedCompat(new long[0]); + testPackedCompat(new long[]{0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE}); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testPackedCompat(long[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_SINT64; + final long fieldId = fieldFlags | ((long) 82 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.sint64FieldPacked = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + long[] result = new long[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readLong(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.sint64FieldPacked.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.sint64FieldPacked[i], result[i]); + } + } + + /** + * Test that using the wrong read method throws an exception + */ + public void testBadReadType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_SINT64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 1 + (byte) 0x08, + (byte) 0x01, + }; + + ProtoInputStream pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readFloat(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readDouble(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readInt(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBoolean(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBytes(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readString(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + } + + /** + * Test that unexpected wrong wire types will throw an exception + */ + public void testBadWireType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_SINT64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 : varint -> 1 + (byte) 0x08, + (byte) 0x01, + // 2 : fixed64 -> 0x1 + (byte) 0x11, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 : length delimited -> { 1 } + (byte) 0x1a, + (byte) 0x01, + (byte) 0x01, + // 6 : fixed32 + (byte) 0x35, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream); + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + try { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + pi.readLong(fieldId1); + // don't fail, varint is ok + break; + case (int) fieldId2: + pi.readLong(fieldId2); + fail("Should have thrown a WireTypeMismatchException"); + break; + case (int) fieldId3: + pi.readLong(fieldId3); + // don't fail, length delimited is ok (represents packed sint64) + break; + case (int) fieldId6: + pi.readLong(fieldId6); + fail("Should have thrown a WireTypeMismatchException"); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } catch (WireTypeMismatchException wtme) { + // good + } + } + stream.close(); + } +} diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamStringTest.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamStringTest.java new file mode 100644 index 000000000000..816d5f900a3d --- /dev/null +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamStringTest.java @@ -0,0 +1,404 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.protoinputstream; + +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoStream; +import android.util.proto.WireTypeMismatchException; + +import com.android.test.protoinputstream.nano.Test; + +import com.google.protobuf.nano.MessageNano; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ProtoInputStreamStringTest extends TestCase { + + public void testRead() throws IOException { + testRead(0); + testRead(1); + testRead(5); + } + + private void testRead(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_STRING; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> null - default value, not written + // 2 -> "" - default value, not written + // 3 -> "abcd\u3110!" + (byte) 0x1a, + (byte) 0x08, + (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, + (byte) 0xe3, (byte) 0x84, (byte) 0x90, (byte) 0x21, + // 5 -> "Hi" + (byte) 0x2a, + (byte) 0x02, + (byte) 0x48, (byte) 0x69, + // 4 -> "Hi" + (byte) 0x22, + (byte) 0x02, + (byte) 0x48, (byte) 0x69, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + String[] results = new String[4]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0] = pi.readString(fieldId1); + break; + case (int) fieldId2: + results[1] = pi.readString(fieldId2); + break; + case (int) fieldId3: + results[2] = pi.readString(fieldId3); + break; + case (int) fieldId4: + results[3] = pi.readString(fieldId4); + break; + case (int) fieldId5: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertNull(results[0]); + assertNull(results[1]); + assertEquals("abcd\u3110!", results[2]); + assertEquals("Hi", results[3]); + } + + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testReadCompat() throws Exception { + testReadCompat(""); + testReadCompat("abcd\u3110!"); + } + + /** + * Implementation of testReadCompat with a given value. + */ + private void testReadCompat(String val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_STRING; + final long fieldId = fieldFlags | ((long) 140 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.stringField = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + String result = ""; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result = pi.readString(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.stringField, result); + } + + + public void testRepeated() throws IOException { + testRepeated(0); + testRepeated(1); + testRepeated(5); + } + + private void testRepeated(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_STRING; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> null - default value, written when repeated + (byte) 0x0a, + (byte) 0x00, + // 2 -> "" - default value, written when repeated + (byte) 0x12, + (byte) 0x00, + // 3 -> "abcd\u3110!" + (byte) 0x1a, + (byte) 0x08, + (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, + (byte) 0xe3, (byte) 0x84, (byte) 0x90, (byte) 0x21, + // 4 -> "Hi" + (byte) 0x22, + (byte) 0x02, + (byte) 0x48, (byte) 0x69, + + // 5 -> "Hi" + (byte) 0x2a, + (byte) 0x02, + (byte) 0x48, (byte) 0x69, + + // 1 -> null - default value, written when repeated + (byte) 0x0a, + (byte) 0x00, + // 2 -> "" - default value, written when repeated + (byte) 0x12, + (byte) 0x00, + // 3 -> "abcd\u3110!" + (byte) 0x1a, + (byte) 0x08, + (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, + (byte) 0xe3, (byte) 0x84, (byte) 0x90, (byte) 0x21, + // 4 -> "Hi" + (byte) 0x22, + (byte) 0x02, + (byte) 0x48, (byte) 0x69, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + String[][] results = new String[4][2]; + int[] indices = new int[4]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readString(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readString(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readString(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readString(fieldId4); + break; + case (int) fieldId5: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + + assertEquals("", results[0][0]); + assertEquals("", results[0][1]); + assertEquals("", results[1][0]); + assertEquals("", results[1][1]); + assertEquals("abcd\u3110!", results[2][0]); + assertEquals("abcd\u3110!", results[2][1]); + assertEquals("Hi", results[3][0]); + assertEquals("Hi", results[3][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testRepeatedCompat() throws Exception { + testRepeatedCompat(new String[0]); + testRepeatedCompat(new String[]{"", "abcd\u3110!", "Hi"}); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testRepeatedCompat(String[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_STRING; + final long fieldId = fieldFlags | ((long) 141 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.stringFieldRepeated = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + String[] result = new String[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readString(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.stringFieldRepeated.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.stringFieldRepeated[i], result[i]); + } + } + + /** + * Test that using the wrong read method throws an exception + */ + public void testBadReadType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_STRING; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> {1} + (byte) 0x0a, + (byte) 0x01, + (byte) 0x01, + }; + + ProtoInputStream pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readFloat(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readDouble(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readInt(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readLong(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBytes(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBoolean(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + } + + /** + * Test that unexpected wrong wire types will throw an exception + */ + public void testBadWireType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_STRING; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 : varint -> 1 + (byte) 0x08, + (byte) 0x01, + // 2 : fixed64 -> 0x1 + (byte) 0x11, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 : length delimited -> { 1 } + (byte) 0x1a, + (byte) 0x01, + (byte) 0x01, + // 6 : fixed32 + (byte) 0x35, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream); + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + try { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + pi.readString(fieldId1); + fail("Should have thrown a WireTypeMismatchException"); + break; + case (int) fieldId2: + pi.readString(fieldId2); + fail("Should have thrown a WireTypeMismatchException"); + break; + case (int) fieldId3: + pi.readString(fieldId3); + // don't fail, length delimited is ok (represents packed booleans) + break; + case (int) fieldId6: + pi.readString(fieldId6); + fail("Should have thrown a WireTypeMismatchException"); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } catch (WireTypeMismatchException wtme) { + // good + } + } + stream.close(); + } + +} diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamUInt32Test.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamUInt32Test.java new file mode 100644 index 000000000000..50fc537767a4 --- /dev/null +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamUInt32Test.java @@ -0,0 +1,564 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.protoinputstream; + +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoStream; +import android.util.proto.WireTypeMismatchException; + +import com.android.test.protoinputstream.nano.Test; + +import com.google.protobuf.nano.MessageNano; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ProtoInputStreamUInt32Test extends TestCase { + + public void testRead() throws IOException { + testRead(0); + testRead(1); + testRead(5); + } + + private void testRead(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_UINT32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, not written + // 2 -> 1 + (byte) 0x10, + (byte) 0x01, + // 6 -> MAX_VALUE + (byte) 0x30, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + // 3 -> -1 + (byte) 0x18, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 4 -> MIN_VALUE + (byte) 0x20, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 5 -> MAX_VALUE + (byte) 0x28, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + int[] results = new int[5]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + fail("Should never reach this"); + break; + case (int) fieldId2: + results[1] = pi.readInt(fieldId2); + break; + case (int) fieldId3: + results[2] = pi.readInt(fieldId3); + break; + case (int) fieldId4: + results[3] = pi.readInt(fieldId4); + break; + case (int) fieldId5: + results[4] = pi.readInt(fieldId5); + break; + case (int) fieldId6: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals(0, results[0]); + assertEquals(1, results[1]); + assertEquals(-1, results[2]); + assertEquals(Integer.MIN_VALUE, results[3]); + assertEquals(Integer.MAX_VALUE, results[4]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testReadCompat() throws Exception { + testReadCompat(0); + testReadCompat(1); + testReadCompat(-1); + testReadCompat(Integer.MIN_VALUE); + testReadCompat(Integer.MAX_VALUE); + } + + /** + * Implementation of testReadCompat with a given value. + */ + private void testReadCompat(int val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_UINT32; + final long fieldId = fieldFlags | ((long) 50 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.uint32Field = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + int result = 0; // start off with default value + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result = pi.readInt(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.uint32Field, result); + } + + public void testRepeated() throws IOException { + testRepeated(0); + testRepeated(1); + testRepeated(5); + } + + private void testRepeated(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_UINT32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x08, + (byte) 0x00, + // 2 -> 1 + (byte) 0x10, + (byte) 0x01, + // 6 -> MAX_VALUE + (byte) 0x30, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + // 3 -> -1 + (byte) 0x18, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 4 -> MIN_VALUE + (byte) 0x20, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 5 -> MAX_VALUE + (byte) 0x28, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + + // 1 -> 0 - default value, written when repeated + (byte) 0x08, + (byte) 0x00, + // 2 -> 1 + (byte) 0x10, + (byte) 0x01, + // 3 -> -1 + (byte) 0x18, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 4 -> MIN_VALUE + (byte) 0x20, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 5 -> MAX_VALUE + (byte) 0x28, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + int[][] results = new int[5][2]; + int[] indices = new int[5]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readInt(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readInt(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readInt(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readInt(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readInt(fieldId5); + break; + case (int) fieldId6: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + + assertEquals(0, results[0][0]); + assertEquals(0, results[0][1]); + assertEquals(1, results[1][0]); + assertEquals(1, results[1][1]); + assertEquals(-1, results[2][0]); + assertEquals(-1, results[2][1]); + assertEquals(Integer.MIN_VALUE, results[3][0]); + assertEquals(Integer.MIN_VALUE, results[3][1]); + assertEquals(Integer.MAX_VALUE, results[4][0]); + assertEquals(Integer.MAX_VALUE, results[4][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testRepeatedCompat() throws Exception { + testRepeatedCompat(new int[0]); + testRepeatedCompat(new int[]{0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE}); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testRepeatedCompat(int[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_UINT32; + final long fieldId = fieldFlags | ((long) 51 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.uint32FieldRepeated = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + int[] result = new int[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readInt(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.uint32FieldRepeated.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.uint32FieldRepeated[i], result[i]); + } + } + + public void testPacked() throws IOException { + testPacked(0); + testPacked(1); + testPacked(5); + } + + private void testPacked(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_PACKED | ProtoStream.FIELD_TYPE_UINT32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x0a, + (byte) 0x02, + (byte) 0x00, + (byte) 0x00, + // 2 -> 1 + (byte) 0x12, + (byte) 0x02, + (byte) 0x01, + (byte) 0x01, + + // 6 -> MAX_VALUE + (byte) 0x32, + (byte) 0x0a, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + + // 3 -> -1 + (byte) 0x1a, + (byte) 0x14, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + + // 4 -> MIN_VALUE + (byte) 0x22, + (byte) 0x14, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + + // 5 -> MAX_VALUE + (byte) 0x2a, + (byte) 0x0a, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + int[][] results = new int[5][2]; + int[] indices = new int[5]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readInt(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readInt(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readInt(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readInt(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readInt(fieldId5); + break; + case (int) fieldId6: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + + assertEquals(0, results[0][0]); + assertEquals(0, results[0][1]); + assertEquals(1, results[1][0]); + assertEquals(1, results[1][1]); + assertEquals(-1, results[2][0]); + assertEquals(-1, results[2][1]); + assertEquals(Integer.MIN_VALUE, results[3][0]); + assertEquals(Integer.MIN_VALUE, results[3][1]); + assertEquals(Integer.MAX_VALUE, results[4][0]); + assertEquals(Integer.MAX_VALUE, results[4][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testPackedCompat() throws Exception { + testPackedCompat(new int[0]); + testPackedCompat(new int[]{0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE}); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testPackedCompat(int[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_UINT32; + final long fieldId = fieldFlags | ((long) 52 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.uint32FieldPacked = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + int[] result = new int[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readInt(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.uint32FieldPacked.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.uint32FieldPacked[i], result[i]); + } + } + + /** + * Test that using the wrong read method throws an exception + */ + public void testBadReadType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_UINT32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 1 + (byte) 0x08, + (byte) 0x01, + }; + + ProtoInputStream pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readFloat(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readDouble(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBoolean(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readLong(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBytes(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readString(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + } + + /** + * Test that unexpected wrong wire types will throw an exception + */ + public void testBadWireType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_UINT32; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 : varint -> 1 + (byte) 0x08, + (byte) 0x01, + // 2 : fixed64 -> 0x1 + (byte) 0x11, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 : length delimited -> { 1 } + (byte) 0x1a, + (byte) 0x01, + (byte) 0x01, + // 6 : fixed32 + (byte) 0x35, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream); + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + try { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + pi.readInt(fieldId1); + // don't fail, varint is ok + break; + case (int) fieldId2: + pi.readInt(fieldId2); + fail("Should have thrown a WireTypeMismatchException"); + break; + case (int) fieldId3: + pi.readInt(fieldId3); + // don't fail, length delimited is ok (represents packed uint32) + break; + case (int) fieldId6: + pi.readInt(fieldId6); + fail("Should have thrown a WireTypeMismatchException"); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } catch (WireTypeMismatchException wtme) { + // good + } + } + stream.close(); + } +} diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamUInt64Test.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamUInt64Test.java new file mode 100644 index 000000000000..20969e9056a9 --- /dev/null +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamUInt64Test.java @@ -0,0 +1,641 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.protoinputstream; + +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoStream; +import android.util.proto.WireTypeMismatchException; + +import com.android.test.protoinputstream.nano.Test; + +import com.google.protobuf.nano.MessageNano; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ProtoInputStreamUInt64Test extends TestCase { + + public void testRead() throws IOException { + testRead(0); + testRead(1); + testRead(5); + } + + private void testRead(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_UINT64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + final long fieldId7 = fieldFlags | ((long) 7 & 0x0ffffffffL); + final long fieldId8 = fieldFlags | ((long) 8 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, not written + // 2 -> 1 + (byte) 0x10, + (byte) 0x01, + // 8 -> Integer.MAX_VALUE + (byte) 0x40, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + // 3 -> -1 + (byte) 0x18, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 4 -> Integer.MIN_VALUE + (byte) 0x20, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 5 -> Integer.MAX_VALUE + (byte) 0x28, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + // 6 -> Long.MIN_VALUE + (byte) 0x30, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x01, + // 7 -> Long.MAX_VALUE + (byte) 0x38, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + long[] results = new long[7]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + fail("Should never reach this"); + break; + case (int) fieldId2: + results[1] = pi.readLong(fieldId2); + break; + case (int) fieldId3: + results[2] = pi.readLong(fieldId3); + break; + case (int) fieldId4: + results[3] = pi.readLong(fieldId4); + break; + case (int) fieldId5: + results[4] = pi.readLong(fieldId5); + break; + case (int) fieldId6: + results[5] = pi.readLong(fieldId6); + break; + case (int) fieldId7: + results[6] = pi.readLong(fieldId7); + break; + case (int) fieldId8: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals(0, results[0]); + assertEquals(1, results[1]); + assertEquals(-1, results[2]); + assertEquals(Integer.MIN_VALUE, results[3]); + assertEquals(Integer.MAX_VALUE, results[4]); + assertEquals(Long.MIN_VALUE, results[5]); + assertEquals(Long.MAX_VALUE, results[6]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testReadCompat() throws Exception { + testReadCompat(0); + testReadCompat(1); + testReadCompat(-1); + testReadCompat(Integer.MIN_VALUE); + testReadCompat(Integer.MAX_VALUE); + testReadCompat(Long.MIN_VALUE); + testReadCompat(Long.MAX_VALUE); + } + + /** + * Implementation of testReadCompat with a given value. + */ + private void testReadCompat(long val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_UINT64; + final long fieldId = fieldFlags | ((long) 60 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.uint64Field = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + long result = 0; // start off with default value + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result = pi.readLong(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.uint64Field, result); + } + + public void testRepeated() throws IOException { + testRepeated(0); + testRepeated(1); + testRepeated(5); + } + + private void testRepeated(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_UINT64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + final long fieldId7 = fieldFlags | ((long) 7 & 0x0ffffffffL); + final long fieldId8 = fieldFlags | ((long) 8 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x08, + (byte) 0x00, + // 2 -> 1 + (byte) 0x10, + (byte) 0x01, + // 3 -> -1 + (byte) 0x18, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 4 -> Integer.MIN_VALUE + (byte) 0x20, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 5 -> Integer.MAX_VALUE + (byte) 0x28, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + // 6 -> Long.MIN_VALUE + (byte) 0x30, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x01, + // 7 -> Long.MAX_VALUE + (byte) 0x38, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + + // 8 -> Integer.MAX_VALUE + (byte) 0x40, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + + // 1 -> 0 - default value, written when repeated + (byte) 0x08, + (byte) 0x00, + // 2 -> 1 + (byte) 0x10, + (byte) 0x01, + // 3 -> -1 + (byte) 0x18, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 4 -> Integer.MIN_VALUE + (byte) 0x20, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + // 5 -> Integer.MAX_VALUE + (byte) 0x28, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + // 6 -> Long.MIN_VALUE + (byte) 0x30, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x01, + // 7 -> Long.MAX_VALUE + (byte) 0x38, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + long[][] results = new long[7][2]; + int[] indices = new int[7]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readLong(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readLong(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readLong(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readLong(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readLong(fieldId5); + break; + case (int) fieldId6: + results[5][indices[5]++] = pi.readLong(fieldId6); + break; + case (int) fieldId7: + results[6][indices[6]++] = pi.readLong(fieldId7); + break; + case (int) fieldId8: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals(0, results[0][0]); + assertEquals(0, results[0][1]); + assertEquals(1, results[1][0]); + assertEquals(1, results[1][1]); + assertEquals(-1, results[2][0]); + assertEquals(-1, results[2][1]); + assertEquals(Integer.MIN_VALUE, results[3][0]); + assertEquals(Integer.MIN_VALUE, results[3][1]); + assertEquals(Integer.MAX_VALUE, results[4][0]); + assertEquals(Integer.MAX_VALUE, results[4][1]); + assertEquals(Long.MIN_VALUE, results[5][0]); + assertEquals(Long.MIN_VALUE, results[5][1]); + assertEquals(Long.MAX_VALUE, results[6][0]); + assertEquals(Long.MAX_VALUE, results[6][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testRepeatedCompat() throws Exception { + testRepeatedCompat(new long[0]); + testRepeatedCompat(new long[]{0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE}); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testRepeatedCompat(long[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_UINT64; + final long fieldId = fieldFlags | ((long) 61 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.uint64FieldRepeated = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + long[] result = new long[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readLong(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.uint64FieldRepeated.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.uint64FieldRepeated[i], result[i]); + } + } + + public void testPacked() throws IOException { + testPacked(0); + testPacked(1); + testPacked(5); + } + + private void testPacked(int chunkSize) throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_PACKED | ProtoStream.FIELD_TYPE_UINT64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL); + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + final long fieldId7 = fieldFlags | ((long) 7 & 0x0ffffffffL); + final long fieldId8 = fieldFlags | ((long) 8 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 0 - default value, written when repeated + (byte) 0x0a, + (byte) 0x02, + (byte) 0x00, + (byte) 0x00, + // 2 -> 1 + (byte) 0x12, + (byte) 0x02, + (byte) 0x01, + (byte) 0x01, + + // 8 -> Integer.MAX_VALUE + (byte) 0x42, + (byte) 0x0a, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + + // 3 -> -1 + (byte) 0x1a, + (byte) 0x14, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + + // 4 -> Integer.MIN_VALUE + (byte) 0x22, + (byte) 0x14, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, + + // 5 -> Integer.MAX_VALUE + (byte) 0x2a, + (byte) 0x0a, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07, + + // 6 -> Long.MIN_VALUE + (byte) 0x32, + (byte) 0x14, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x01, + + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, + (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x01, + + // 7 -> Long.MAX_VALUE + (byte) 0x3a, + (byte) 0x12, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize); + long[][] results = new long[7][2]; + int[] indices = new int[7]; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + + switch (pi.getFieldNumber()) { + case (int) fieldId1: + results[0][indices[0]++] = pi.readLong(fieldId1); + break; + case (int) fieldId2: + results[1][indices[1]++] = pi.readLong(fieldId2); + break; + case (int) fieldId3: + results[2][indices[2]++] = pi.readLong(fieldId3); + break; + case (int) fieldId4: + results[3][indices[3]++] = pi.readLong(fieldId4); + break; + case (int) fieldId5: + results[4][indices[4]++] = pi.readLong(fieldId5); + break; + case (int) fieldId6: + results[5][indices[5]++] = pi.readLong(fieldId6); + break; + case (int) fieldId7: + results[6][indices[6]++] = pi.readLong(fieldId7); + break; + case (int) fieldId8: + // Intentionally don't read the data. Parse should continue normally + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + stream.close(); + + assertEquals(0, results[0][0]); + assertEquals(0, results[0][1]); + assertEquals(1, results[1][0]); + assertEquals(1, results[1][1]); + assertEquals(-1, results[2][0]); + assertEquals(-1, results[2][1]); + assertEquals(Integer.MIN_VALUE, results[3][0]); + assertEquals(Integer.MIN_VALUE, results[3][1]); + assertEquals(Integer.MAX_VALUE, results[4][0]); + assertEquals(Integer.MAX_VALUE, results[4][1]); + assertEquals(Long.MIN_VALUE, results[5][0]); + assertEquals(Long.MIN_VALUE, results[5][1]); + assertEquals(Long.MAX_VALUE, results[6][0]); + assertEquals(Long.MAX_VALUE, results[6][1]); + } + + /** + * Test that reading with ProtoInputStream matches, and can read the output of standard proto. + */ + public void testPackedCompat() throws Exception { + testPackedCompat(new long[0]); + testPackedCompat(new long[]{0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE}); + } + + /** + * Implementation of testRepeatedCompat with a given value. + */ + private void testPackedCompat(long[] val) throws Exception { + final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_UINT64; + final long fieldId = fieldFlags | ((long) 62 & 0x0ffffffffL); + + final Test.All all = new Test.All(); + all.uint64FieldPacked = val; + + final byte[] proto = MessageNano.toByteArray(all); + + final ProtoInputStream pi = new ProtoInputStream(proto); + final Test.All readback = Test.All.parseFrom(proto); + + long[] result = new long[val.length]; + int index = 0; + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pi.getFieldNumber()) { + case (int) fieldId: + result[index++] = pi.readLong(fieldId); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } + + assertEquals(readback.uint64FieldPacked.length, result.length); + for (int i = 0; i < result.length; i++) { + assertEquals(readback.uint64FieldPacked[i], result[i]); + } + } + + /** + * Test that using the wrong read method throws an exception + */ + public void testBadReadType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_UINT64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 -> 1 + (byte) 0x08, + (byte) 0x01, + }; + + ProtoInputStream pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readFloat(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readDouble(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readInt(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBoolean(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readBytes(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + + pi = new ProtoInputStream(protobuf); + pi.isNextField(fieldId1); + try { + pi.readString(fieldId1); + fail("Should have throw IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // good + } + } + + /** + * Test that unexpected wrong wire types will throw an exception + */ + public void testBadWireType() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_UINT64; + + final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL); + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL); + final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 1 : varint -> 1 + (byte) 0x08, + (byte) 0x01, + // 2 : fixed64 -> 0x1 + (byte) 0x11, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // 3 : length delimited -> { 1 } + (byte) 0x1a, + (byte) 0x01, + (byte) 0x01, + // 6 : fixed32 + (byte) 0x35, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream); + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + try { + switch (pi.getFieldNumber()) { + case (int) fieldId1: + pi.readLong(fieldId1); + // don't fail, varint is ok + break; + case (int) fieldId2: + pi.readLong(fieldId2); + fail("Should have thrown a WireTypeMismatchException"); + break; + case (int) fieldId3: + pi.readLong(fieldId3); + // don't fail, length delimited is ok (represents packed uint64) + break; + case (int) fieldId6: + pi.readLong(fieldId6); + fail("Should have thrown a WireTypeMismatchException"); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } catch (WireTypeMismatchException wtme) { + // good + } + } + stream.close(); + } +} diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoTests.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoTests.java new file mode 100644 index 000000000000..cdf6ae20f370 --- /dev/null +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoTests.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.protoinputstream; + +import junit.framework.TestSuite; + +public class ProtoTests { + public static TestSuite suite() { + TestSuite suite = new TestSuite(ProtoTests.class.getName()); + + suite.addTestSuite(ProtoInputStreamDoubleTest.class); + suite.addTestSuite(ProtoInputStreamFloatTest.class); + suite.addTestSuite(ProtoInputStreamInt32Test.class); + suite.addTestSuite(ProtoInputStreamInt64Test.class); + suite.addTestSuite(ProtoInputStreamUInt32Test.class); + suite.addTestSuite(ProtoInputStreamUInt64Test.class); + suite.addTestSuite(ProtoInputStreamSInt32Test.class); + suite.addTestSuite(ProtoInputStreamSInt64Test.class); + suite.addTestSuite(ProtoInputStreamFixed32Test.class); + suite.addTestSuite(ProtoInputStreamFixed64Test.class); + suite.addTestSuite(ProtoInputStreamSFixed32Test.class); + suite.addTestSuite(ProtoInputStreamSFixed64Test.class); + suite.addTestSuite(ProtoInputStreamBoolTest.class); + suite.addTestSuite(ProtoInputStreamStringTest.class); + suite.addTestSuite(ProtoInputStreamBytesTest.class); + suite.addTestSuite(ProtoInputStreamEnumTest.class); + suite.addTestSuite(ProtoInputStreamObjectTest.class); + + return suite; + } +} diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/test.proto b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/test.proto new file mode 100644 index 000000000000..9ff1d7e2d19a --- /dev/null +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/test.proto @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +syntax = "proto2"; + +package com.android.test.protoinputstream; + +/** + * Enum that outside the scope of any classes. + */ +enum Outside { + OUTSIDE_0 = 0; + OUTSIDE_1 = 1; +}; + +/** + * Message that is recursive. + */ +message Nested { + optional int32 data = 10001; + optional Nested nested = 10002; +}; + +/** + * Message with all of the field types. + */ +message All { + /** + * Enum that is inside the scope of a class. + */ + enum Inside { + option allow_alias = true; + INSIDE_0 = 0; + INSIDE_1 = 1; + INSIDE_1A = 1; + }; + + optional double double_field = 10; + repeated double double_field_repeated = 11; + repeated double double_field_packed = 12 [packed=true]; + + optional float float_field = 20; + repeated float float_field_repeated = 21; + repeated float float_field_packed = 22 [packed=true]; + + optional int32 int32_field = 30; + repeated int32 int32_field_repeated = 31; + repeated int32 int32_field_packed = 32 [packed=true]; + + optional int64 int64_field = 40; + repeated int64 int64_field_repeated = 41; + repeated int64 int64_field_packed = 42 [packed=true]; + + optional uint32 uint32_field = 50; + repeated uint32 uint32_field_repeated = 51; + repeated uint32 uint32_field_packed = 52 [packed=true]; + + optional uint64 uint64_field = 60; + repeated uint64 uint64_field_repeated = 61; + repeated uint64 uint64_field_packed = 62 [packed=true]; + + optional sint32 sint32_field = 70; + repeated sint32 sint32_field_repeated = 71; + repeated sint32 sint32_field_packed = 72 [packed=true]; + + optional sint64 sint64_field = 80; + repeated sint64 sint64_field_repeated = 81; + repeated sint64 sint64_field_packed = 82 [packed=true]; + + optional fixed32 fixed32_field = 90; + repeated fixed32 fixed32_field_repeated = 91; + repeated fixed32 fixed32_field_packed = 92 [packed=true]; + + optional fixed64 fixed64_field = 100; + repeated fixed64 fixed64_field_repeated = 101; + repeated fixed64 fixed64_field_packed = 102 [packed=true]; + + optional sfixed32 sfixed32_field = 110; + repeated sfixed32 sfixed32_field_repeated = 111; + repeated sfixed32 sfixed32_field_packed = 112 [packed=true]; + + optional sfixed64 sfixed64_field = 120; + repeated sfixed64 sfixed64_field_repeated = 121; + repeated sfixed64 sfixed64_field_packed = 122 [packed=true]; + + optional bool bool_field = 130; + repeated bool bool_field_repeated = 131; + repeated bool bool_field_packed = 132 [packed=true]; + + optional string string_field = 140; + repeated string string_field_repeated = 141; + + optional bytes bytes_field = 150; + repeated bytes bytes_field_repeated = 151; + + optional Outside outside_field = 160; + repeated Outside outside_field_repeated = 161; + repeated Outside outside_field_packed = 162 [packed=true]; + + optional Nested nested_field = 170; + repeated Nested nested_field_repeated = 171; +}; diff --git a/tests/RcsTests/Android.bp b/tests/RcsTests/Android.bp index 81c6df070881..8ee496066bb3 100644 --- a/tests/RcsTests/Android.bp +++ b/tests/RcsTests/Android.bp @@ -10,7 +10,7 @@ android_test { ], static_libs: [ "junit", - "android-support-test", + "androidx.test.rules", "mockito-target-minus-junit4", "truth-prebuilt", ], diff --git a/tests/RcsTests/AndroidManifest.xml b/tests/RcsTests/AndroidManifest.xml index a7e7d479a4d9..b1706a0a3629 100644 --- a/tests/RcsTests/AndroidManifest.xml +++ b/tests/RcsTests/AndroidManifest.xml @@ -6,6 +6,6 @@ <uses-library android:name="android.test.runner" /> </application> - <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner" + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" android:targetPackage="com.android.tests.rcs"/> </manifest> diff --git a/tests/RcsTests/src/com/android/tests/ims/RcsGroupThreadIconChangedEventTest.java b/tests/RcsTests/src/com/android/tests/ims/RcsGroupThreadIconChangedEventTest.java index 658c3edb4642..6c311f99f695 100644 --- a/tests/RcsTests/src/com/android/tests/ims/RcsGroupThreadIconChangedEventTest.java +++ b/tests/RcsTests/src/com/android/tests/ims/RcsGroupThreadIconChangedEventTest.java @@ -19,10 +19,11 @@ import static com.google.common.truth.Truth.assertThat; import android.net.Uri; import android.os.Parcel; -import android.support.test.runner.AndroidJUnit4; import android.telephony.ims.RcsGroupThreadIconChangedEvent; import android.telephony.ims.RcsGroupThreadIconChangedEventDescriptor; +import androidx.test.runner.AndroidJUnit4; + import org.junit.Test; import org.junit.runner.RunWith; diff --git a/tests/RcsTests/src/com/android/tests/ims/RcsGroupThreadNameChangedEventTest.java b/tests/RcsTests/src/com/android/tests/ims/RcsGroupThreadNameChangedEventTest.java index 9fe67ad62b7c..a60dabb023f8 100644 --- a/tests/RcsTests/src/com/android/tests/ims/RcsGroupThreadNameChangedEventTest.java +++ b/tests/RcsTests/src/com/android/tests/ims/RcsGroupThreadNameChangedEventTest.java @@ -18,10 +18,11 @@ package com.android.tests.ims; import static com.google.common.truth.Truth.assertThat; import android.os.Parcel; -import android.support.test.runner.AndroidJUnit4; import android.telephony.ims.RcsGroupThreadNameChangedEvent; import android.telephony.ims.RcsGroupThreadNameChangedEventDescriptor; +import androidx.test.runner.AndroidJUnit4; + import org.junit.Test; import org.junit.runner.RunWith; diff --git a/tests/RcsTests/src/com/android/tests/ims/RcsGroupThreadParticipantJoinedEventTest.java b/tests/RcsTests/src/com/android/tests/ims/RcsGroupThreadParticipantJoinedEventTest.java index 18d5621f8e20..7b02cb1eaafa 100644 --- a/tests/RcsTests/src/com/android/tests/ims/RcsGroupThreadParticipantJoinedEventTest.java +++ b/tests/RcsTests/src/com/android/tests/ims/RcsGroupThreadParticipantJoinedEventTest.java @@ -18,10 +18,11 @@ package com.android.tests.ims; import static com.google.common.truth.Truth.assertThat; import android.os.Parcel; -import android.support.test.runner.AndroidJUnit4; import android.telephony.ims.RcsGroupThreadParticipantJoinedEvent; import android.telephony.ims.RcsGroupThreadParticipantJoinedEventDescriptor; +import androidx.test.runner.AndroidJUnit4; + import org.junit.Test; import org.junit.runner.RunWith; diff --git a/tests/RcsTests/src/com/android/tests/ims/RcsGroupThreadParticipantLeftEventTest.java b/tests/RcsTests/src/com/android/tests/ims/RcsGroupThreadParticipantLeftEventTest.java index 53a6bba52a3e..51466bd98d60 100644 --- a/tests/RcsTests/src/com/android/tests/ims/RcsGroupThreadParticipantLeftEventTest.java +++ b/tests/RcsTests/src/com/android/tests/ims/RcsGroupThreadParticipantLeftEventTest.java @@ -18,10 +18,11 @@ package com.android.tests.ims; import static com.google.common.truth.Truth.assertThat; import android.os.Parcel; -import android.support.test.runner.AndroidJUnit4; import android.telephony.ims.RcsGroupThreadParticipantLeftEvent; import android.telephony.ims.RcsGroupThreadParticipantLeftEventDescriptor; +import androidx.test.runner.AndroidJUnit4; + import org.junit.Test; import org.junit.runner.RunWith; diff --git a/tests/RcsTests/src/com/android/tests/ims/RcsParticipantAliasChangedEventTest.java b/tests/RcsTests/src/com/android/tests/ims/RcsParticipantAliasChangedEventTest.java index dcf68ffa3324..56830dff1ce5 100644 --- a/tests/RcsTests/src/com/android/tests/ims/RcsParticipantAliasChangedEventTest.java +++ b/tests/RcsTests/src/com/android/tests/ims/RcsParticipantAliasChangedEventTest.java @@ -18,10 +18,11 @@ package com.android.tests.ims; import static com.google.common.truth.Truth.assertThat; import android.os.Parcel; -import android.support.test.runner.AndroidJUnit4; import android.telephony.ims.RcsParticipantAliasChangedEvent; import android.telephony.ims.RcsParticipantAliasChangedEventDescriptor; +import androidx.test.runner.AndroidJUnit4; + import org.junit.Test; import org.junit.runner.RunWith; diff --git a/tests/RcsTests/src/com/android/tests/ims/RcsParticipantQueryParamsTest.java b/tests/RcsTests/src/com/android/tests/ims/RcsParticipantQueryParamsTest.java index 6361a393187e..2d95513be069 100644 --- a/tests/RcsTests/src/com/android/tests/ims/RcsParticipantQueryParamsTest.java +++ b/tests/RcsTests/src/com/android/tests/ims/RcsParticipantQueryParamsTest.java @@ -18,9 +18,10 @@ package com.android.tests.ims; import static com.google.common.truth.Truth.assertThat; import android.os.Parcel; -import android.support.test.runner.AndroidJUnit4; import android.telephony.ims.RcsParticipantQueryParams; +import androidx.test.runner.AndroidJUnit4; + import org.junit.Test; import org.junit.runner.RunWith; diff --git a/tests/RcsTests/src/com/android/tests/ims/RcsThreadQueryParamsTest.java b/tests/RcsTests/src/com/android/tests/ims/RcsThreadQueryParamsTest.java index 551a228282cd..7a3e384020a1 100644 --- a/tests/RcsTests/src/com/android/tests/ims/RcsThreadQueryParamsTest.java +++ b/tests/RcsTests/src/com/android/tests/ims/RcsThreadQueryParamsTest.java @@ -21,10 +21,11 @@ import static android.telephony.ims.RcsThreadQueryParams.THREAD_TYPE_GROUP; import static com.google.common.truth.Truth.assertThat; import android.os.Parcel; -import android.support.test.runner.AndroidJUnit4; import android.telephony.ims.RcsParticipant; import android.telephony.ims.RcsThreadQueryParams; +import androidx.test.runner.AndroidJUnit4; + import org.junit.Test; import org.junit.runner.RunWith; diff --git a/tests/RollbackTest/Android.bp b/tests/RollbackTest/Android.bp new file mode 100644 index 000000000000..aec40558ad51 --- /dev/null +++ b/tests/RollbackTest/Android.bp @@ -0,0 +1,107 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +android_test_helper_app { + name: "RollbackTestAppAv1", + manifest: "TestApp/Av1.xml", + sdk_version: "current", + srcs: ["TestApp/src/**/*.java"], + resource_dirs: ["TestApp/res_v1"], +} + +android_test_helper_app { + name: "RollbackTestAppAv2", + manifest: "TestApp/Av2.xml", + sdk_version: "current", + srcs: ["TestApp/src/**/*.java"], + resource_dirs: ["TestApp/res_v2"], +} + +android_test_helper_app { + name: "RollbackTestAppAv3", + manifest: "TestApp/Av3.xml", + sdk_version: "current", + srcs: ["TestApp/src/**/*.java"], + resource_dirs: ["TestApp/res_v3"], +} + +android_test_helper_app { + name: "RollbackTestAppACrashingV2", + manifest: "TestApp/ACrashingV2.xml", + sdk_version: "current", + srcs: ["TestApp/src/**/*.java"], + resource_dirs: ["TestApp/res_v2"], +} + +android_test_helper_app { + name: "RollbackTestAppBv1", + manifest: "TestApp/Bv1.xml", + sdk_version: "current", + srcs: ["TestApp/src/**/*.java"], + resource_dirs: ["TestApp/res_v1"], +} + +android_test_helper_app { + name: "RollbackTestAppBv2", + manifest: "TestApp/Bv2.xml", + sdk_version: "current", + srcs: ["TestApp/src/**/*.java"], + resource_dirs: ["TestApp/res_v2"], +} + +android_test_helper_app { + name: "RollbackTestAppASplitV1", + manifest: "TestApp/Av1.xml", + sdk_version: "current", + srcs: ["TestApp/src/**/*.java"], + resource_dirs: ["TestApp/res_v1"], + package_splits: ["anydpi"], +} + +android_test_helper_app { + name: "RollbackTestAppASplitV2", + manifest: "TestApp/Av2.xml", + sdk_version: "current", + srcs: ["TestApp/src/**/*.java"], + resource_dirs: ["TestApp/res_v2"], + package_splits: ["anydpi"], +} + +android_test { + name: "RollbackTest", + manifest: "RollbackTest/AndroidManifest.xml", + srcs: ["RollbackTest/src/**/*.java"], + static_libs: ["androidx.test.rules"], + test_suites: ["general-tests"], + java_resources: [ + ":RollbackTestAppAv1", + ":RollbackTestAppAv2", + ":RollbackTestAppAv3", + ":RollbackTestAppACrashingV2", + ":RollbackTestAppBv1", + ":RollbackTestAppBv2", + ":RollbackTestAppASplitV1", + ":RollbackTestAppASplitV2", + ], + test_config: "RollbackTest.xml", + // TODO: sdk_version: "test_current" when Intent#resolveSystemservice is TestApi +} + +java_test_host { + name: "StagedRollbackTest", + srcs: ["StagedRollbackTest/src/**/*.java"], + libs: ["tradefed"], + test_suites: ["general-tests"], + test_config: "StagedRollbackTest.xml", +} diff --git a/tests/RollbackTest/README.txt b/tests/RollbackTest/README.txt new file mode 100644 index 000000000000..c0b718a3e2c1 --- /dev/null +++ b/tests/RollbackTest/README.txt @@ -0,0 +1,23 @@ +This directory contains a test for the rollback manager service. + +Directory structure +=================== +RollbackTest + - device driven test for rollbacks not involving staged rollbacks. + +StagedRollbackTest + - device driven test for staged rollbacks. + +TestApp + - source for dummy apks used in testing. + +TestApex + - source for dummy apex modules used in testing. + +Running the tests +================= + +You can manually run the tests as follows: + + atest RollbackTest + atest StagedRollbackTest diff --git a/tests/RollbackTest/RollbackTest.xml b/tests/RollbackTest/RollbackTest.xml new file mode 100644 index 000000000000..70cd86783d6d --- /dev/null +++ b/tests/RollbackTest/RollbackTest.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<configuration description="Runs the rollback tests"> + <option name="test-suite-tag" value="RollbackTest" /> + <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <option name="test-file-name" value="RollbackTest.apk" /> + </target_preparer> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.tests.rollback" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + + <!-- Exclude the StagedRollbackTest tests, which needs to be specially + driven from the StagedRollbackTest host test --> + <option name="exclude-filter" value="com.android.tests.rollback.StagedRollbackTest" /> + </test> +</configuration> diff --git a/tests/ImfTest/tests/AndroidManifest.xml b/tests/RollbackTest/RollbackTest/AndroidManifest.xml index c02fa0b212a5..5380dc9fc8cd 100644 --- a/tests/ImfTest/tests/AndroidManifest.xml +++ b/tests/RollbackTest/RollbackTest/AndroidManifest.xml @@ -1,12 +1,12 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2007 The Android Open Source Project +<!-- Copyright (C) 2018 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - + http://www.apache.org/licenses/LICENSE-2.0 - + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -14,23 +14,18 @@ limitations under the License. --> -<!-- package name must be unique so suffix with "tests" so package loader doesn't ignore us --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.imftest.tests"> - - <uses-permission android:name="android.permission.DISABLE_KEYGUARD" /> + package="com.android.tests.rollback" > <application> + <receiver android:name="com.android.tests.rollback.LocalIntentSender" + android:exported="true" /> <uses-library android:name="android.test.runner" /> </application> - <!-- - This declares that this app uses the instrumentation test runner targeting - the package of com.android.imftest. To run the tests use the command: - "adb shell am instrument -w com.android.imftest.tests/android.test.InstrumentationTestRunner" - --> - <instrumentation android:name="android.test.InstrumentationTestRunner" - android:targetPackage="com.android.imftest" - android:label="imf tests"/> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.tests.rollback" + android:label="Rollback Test"/> </manifest> diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/LocalIntentSender.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/LocalIntentSender.java new file mode 100644 index 000000000000..267ef7377b36 --- /dev/null +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/LocalIntentSender.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tests.rollback; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentSender; + +import androidx.test.InstrumentationRegistry; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * Make IntentSender that sends intent locally. + */ +public class LocalIntentSender extends BroadcastReceiver { + + private static final String TAG = "RollbackTest"; + + private static final BlockingQueue<Intent> sIntentSenderResults = new LinkedBlockingQueue<>(); + + @Override + public void onReceive(Context context, Intent intent) { + sIntentSenderResults.add(intent); + } + + /** + * Get a LocalIntentSender. + */ + static IntentSender getIntentSender() { + Context context = InstrumentationRegistry.getContext(); + Intent intent = new Intent(context, LocalIntentSender.class); + PendingIntent pending = PendingIntent.getBroadcast(context, 0, intent, 0); + return pending.getIntentSender(); + } + + /** + * Returns the most recent Intent sent by a LocalIntentSender. + */ + static Intent getIntentSenderResult() throws InterruptedException { + return sIntentSenderResults.take(); + } +} diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackBroadcastReceiver.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackBroadcastReceiver.java new file mode 100644 index 000000000000..ebe54187ddb6 --- /dev/null +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackBroadcastReceiver.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tests.rollback; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.util.Log; + +import androidx.test.InstrumentationRegistry; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * A broadcast receiver that can be used to get + * ACTION_ROLLBACK_COMMITTED broadcasts. + */ +class RollbackBroadcastReceiver extends BroadcastReceiver { + + private static final String TAG = "RollbackTest"; + + private final BlockingQueue<Intent> mRollbackBroadcasts = new LinkedBlockingQueue<>(); + + /** + * Creates a RollbackBroadcastReceiver and registers it with the given + * context. + */ + RollbackBroadcastReceiver() { + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_ROLLBACK_COMMITTED); + InstrumentationRegistry.getContext().registerReceiver(this, filter); + } + + @Override + public void onReceive(Context context, Intent intent) { + Log.i(TAG, "Received rollback broadcast intent"); + mRollbackBroadcasts.add(intent); + } + + /** + * Polls for at most the given amount of time for the next rollback + * broadcast. + */ + Intent poll(long timeout, TimeUnit unit) throws InterruptedException { + return mRollbackBroadcasts.poll(timeout, unit); + } + + /** + * Waits forever for the next rollback broadcast. + */ + Intent take() throws InterruptedException { + return mRollbackBroadcasts.take(); + } + + /** + * Unregisters this broadcast receiver. + */ + void unregister() { + InstrumentationRegistry.getContext().unregisterReceiver(this); + } +} diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java new file mode 100644 index 000000000000..1b002cadb07f --- /dev/null +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java @@ -0,0 +1,1008 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tests.rollback; + +import static com.android.tests.rollback.RollbackTestUtils.assertPackageRollbackInfoEquals; +import static com.android.tests.rollback.RollbackTestUtils.assertRollbackInfoEquals; +import static com.android.tests.rollback.RollbackTestUtils.getUniqueRollbackInfoForPackage; +import static com.android.tests.rollback.RollbackTestUtils.processUserData; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import android.Manifest; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.VersionedPackage; +import android.content.rollback.RollbackInfo; +import android.content.rollback.RollbackManager; +import android.provider.DeviceConfig; +import android.provider.Settings; +import android.util.Log; + +import androidx.test.InstrumentationRegistry; + +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.Collections; +import java.util.concurrent.TimeUnit; + +/** + * Test system Rollback APIs. + * TODO: Should this be a cts test instead? Where should it live? + */ +@RunWith(JUnit4.class) +public class RollbackTest { + + private static final String TAG = "RollbackTest"; + + private static final String TEST_APP_A = "com.android.tests.rollback.testapp.A"; + private static final String TEST_APP_B = "com.android.tests.rollback.testapp.B"; + private static final String INSTRUMENTED_APP = "com.android.tests.rollback"; + + // copied from PackageManagerService#PROPERTY_ENABLE_ROLLBACK_TIMEOUT_MILLIS + // TODO: find a better place for the property so that it can be imported in tests + // maybe android.content.pm.PackageManager? + private static final String PROPERTY_ENABLE_ROLLBACK_TIMEOUT_MILLIS = + "enable_rollback_timeout"; + + /** + * Test basic rollbacks. + */ + @Test + public void testBasic() throws Exception { + // Make sure an app can't listen to or disturb the internal + // ACTION_PACKAGE_ENABLE_ROLLBACK broadcast. + Context context = InstrumentationRegistry.getContext(); + IntentFilter enableRollbackFilter = new IntentFilter(); + enableRollbackFilter.addAction("android.intent.action.PACKAGE_ENABLE_ROLLBACK"); + enableRollbackFilter.addDataType("application/vnd.android.package-archive"); + enableRollbackFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); + BroadcastReceiver enableRollbackReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + abortBroadcast(); + } + }; + context.registerReceiver(enableRollbackReceiver, enableRollbackFilter); + + try { + RollbackTestUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.TEST_MANAGE_ROLLBACKS, + Manifest.permission.MANAGE_ROLLBACKS); + + // Register a broadcast receiver for notification when the + // rollback has been committed. + RollbackBroadcastReceiver broadcastReceiver = new RollbackBroadcastReceiver(); + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + + // Uninstall TEST_APP_A + RollbackTestUtils.uninstall(TEST_APP_A); + assertEquals(-1, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + // TODO: There is currently a race condition between when the app is + // uninstalled and when rollback manager deletes the rollback. Fix it + // so that's not the case! + for (int i = 0; i < 5; ++i) { + RollbackInfo rollback = getUniqueRollbackInfoForPackage( + rm.getRecentlyCommittedRollbacks(), TEST_APP_A); + if (rollback != null) { + Log.i(TAG, "Sleeping 1 second to wait for uninstall to take effect."); + Thread.sleep(1000); + } + } + + // The app should not be available for rollback. + assertNull(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), TEST_APP_A)); + + // There should be no recently committed rollbacks for this package. + assertNull(getUniqueRollbackInfoForPackage( + rm.getRecentlyCommittedRollbacks(), TEST_APP_A)); + + // Install v1 of the app (without rollbacks enabled). + RollbackTestUtils.install("RollbackTestAppAv1.apk", false); + assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + // Upgrade from v1 to v2, with rollbacks enabled. + RollbackTestUtils.install("RollbackTestAppAv2.apk", true); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + // The app should now be available for rollback. + RollbackInfo rollback = getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_A); + assertRollbackInfoEquals(TEST_APP_A, 2, 1, rollback); + + // We should not have received any rollback requests yet. + // TODO: Possibly flaky if, by chance, some other app on device + // happens to be rolled back at the same time? + assertNull(broadcastReceiver.poll(0, TimeUnit.SECONDS)); + + // Roll back the app. + RollbackTestUtils.rollback(rollback.getRollbackId()); + assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + // Verify we received a broadcast for the rollback. + // TODO: Race condition between the timeout and when the broadcast is + // received could lead to test flakiness. + Intent broadcast = broadcastReceiver.poll(5, TimeUnit.SECONDS); + assertNotNull(broadcast); + assertNull(broadcastReceiver.poll(0, TimeUnit.SECONDS)); + + // Verify the recent rollback has been recorded. + rollback = getUniqueRollbackInfoForPackage( + rm.getRecentlyCommittedRollbacks(), TEST_APP_A); + assertRollbackInfoEquals(TEST_APP_A, 2, 1, rollback); + + broadcastReceiver.unregister(); + context.unregisterReceiver(enableRollbackReceiver); + } finally { + RollbackTestUtils.dropShellPermissionIdentity(); + } + } + + /** + * Test that multiple available rollbacks are properly persisted. + */ + @Test + public void testAvailableRollbackPersistence() throws Exception { + try { + RollbackTestUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.TEST_MANAGE_ROLLBACKS); + + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + + RollbackTestUtils.uninstall(TEST_APP_A); + RollbackTestUtils.install("RollbackTestAppAv1.apk", false); + RollbackTestUtils.install("RollbackTestAppAv2.apk", true); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + RollbackTestUtils.uninstall(TEST_APP_B); + RollbackTestUtils.install("RollbackTestAppBv1.apk", false); + RollbackTestUtils.install("RollbackTestAppBv2.apk", true); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_B)); + + // Both test apps should now be available for rollback. + RollbackInfo rollbackA = getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_A); + assertRollbackInfoEquals(TEST_APP_A, 2, 1, rollbackA); + + RollbackInfo rollbackB = getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_B); + assertRollbackInfoEquals(TEST_APP_B, 2, 1, rollbackB); + + // Reload the persisted data. + rm.reloadPersistedData(); + + // The apps should still be available for rollback. + rollbackA = getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_A); + assertRollbackInfoEquals(TEST_APP_A, 2, 1, rollbackA); + + rollbackB = getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_B); + assertRollbackInfoEquals(TEST_APP_B, 2, 1, rollbackB); + + // Rollback of B should not rollback A + RollbackTestUtils.rollback(rollbackB.getRollbackId()); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_B)); + } finally { + RollbackTestUtils.dropShellPermissionIdentity(); + } + } + + /** + * Test that available multi-package rollbacks are properly persisted. + */ + @Test + public void testAvailableMultiPackageRollbackPersistence() throws Exception { + try { + RollbackTestUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.TEST_MANAGE_ROLLBACKS); + + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + + RollbackTestUtils.uninstall(TEST_APP_A); + RollbackTestUtils.uninstall(TEST_APP_B); + RollbackTestUtils.installMultiPackage(false, + "RollbackTestAppAv1.apk", + "RollbackTestAppBv1.apk"); + RollbackTestUtils.installMultiPackage(true, + "RollbackTestAppAv2.apk", + "RollbackTestAppBv2.apk"); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_B)); + + // The app should now be available for rollback. + RollbackInfo rollbackA = getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_A); + assertRollbackInfoForAandB(rollbackA); + + RollbackInfo rollbackB = getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_B); + assertRollbackInfoForAandB(rollbackB); + + // Reload the persisted data. + rm.reloadPersistedData(); + + // The apps should still be available for rollback. + rollbackA = getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), TEST_APP_A); + assertRollbackInfoForAandB(rollbackA); + + rollbackB = getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), TEST_APP_B); + assertRollbackInfoForAandB(rollbackB); + + // Rollback of B should rollback A as well + RollbackTestUtils.rollback(rollbackB.getRollbackId()); + assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_B)); + } finally { + RollbackTestUtils.dropShellPermissionIdentity(); + } + } + + /** + * Test that recently committed rollback data is properly persisted. + */ + @Test + public void testRecentlyCommittedRollbackPersistence() throws Exception { + try { + RollbackTestUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.TEST_MANAGE_ROLLBACKS); + + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + + RollbackTestUtils.uninstall(TEST_APP_A); + RollbackTestUtils.install("RollbackTestAppAv1.apk", false); + RollbackTestUtils.install("RollbackTestAppAv2.apk", true); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + // The app should now be available for rollback. + RollbackInfo rollback = getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_A); + + // Roll back the app. + VersionedPackage cause = new VersionedPackage( + "com.android.tests.rollback.testapp.Foo", 42); + RollbackTestUtils.rollback(rollback.getRollbackId(), cause); + assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + // Verify the recent rollback has been recorded. + rollback = getUniqueRollbackInfoForPackage( + rm.getRecentlyCommittedRollbacks(), TEST_APP_A); + assertRollbackInfoEquals(TEST_APP_A, 2, 1, rollback, cause); + + // Reload the persisted data. + rm.reloadPersistedData(); + + // Verify the recent rollback is still recorded. + rollback = getUniqueRollbackInfoForPackage( + rm.getRecentlyCommittedRollbacks(), TEST_APP_A); + assertRollbackInfoEquals(TEST_APP_A, 2, 1, rollback, cause); + } finally { + RollbackTestUtils.dropShellPermissionIdentity(); + } + } + + /** + * Test the scheduling aspect of rollback expiration. + */ + @Test + public void testRollbackExpiresAfterLifetime() throws Exception { + long expirationTime = TimeUnit.SECONDS.toMillis(30); + long defaultExpirationTime = TimeUnit.HOURS.toMillis(48); + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + + try { + RollbackTestUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.TEST_MANAGE_ROLLBACKS, + Manifest.permission.WRITE_DEVICE_CONFIG); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK_BOOT, + RollbackManager.PROPERTY_ROLLBACK_LIFETIME_MILLIS, + Long.toString(expirationTime), false /* makeDefault*/); + + // Pull the new expiration time from DeviceConfig + rm.reloadPersistedData(); + + // Uninstall TEST_APP_A + RollbackTestUtils.uninstall(TEST_APP_A); + assertEquals(-1, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + // Install v1 of the app (without rollbacks enabled). + RollbackTestUtils.install("RollbackTestAppAv1.apk", false); + assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + // Upgrade from v1 to v2, with rollbacks enabled. + RollbackTestUtils.install("RollbackTestAppAv2.apk", true); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + // Check that the rollback data has not expired + Thread.sleep(1000); + RollbackInfo rollback = getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_A); + assertRollbackInfoEquals(TEST_APP_A, 2, 1, rollback); + + // Give it a little more time, but still not the long enough to expire + Thread.sleep(expirationTime / 2); + rollback = getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_A); + assertRollbackInfoEquals(TEST_APP_A, 2, 1, rollback); + + // Check that the data has expired after the expiration time (with a buffer of 1 second) + Thread.sleep(expirationTime / 2); + assertNull(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), TEST_APP_A)); + + } finally { + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK_BOOT, + RollbackManager.PROPERTY_ROLLBACK_LIFETIME_MILLIS, + Long.toString(defaultExpirationTime), false /* makeDefault*/); + RollbackTestUtils.dropShellPermissionIdentity(); + } + } + + /** + * Test that changing time on device does not affect the duration of time that we keep + * rollback available + */ + @Test + public void testTimeChangeDoesNotAffectLifetime() throws Exception { + long expirationTime = TimeUnit.SECONDS.toMillis(30); + long defaultExpirationTime = TimeUnit.HOURS.toMillis(48); + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + + try { + RollbackTestUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.TEST_MANAGE_ROLLBACKS, + Manifest.permission.WRITE_DEVICE_CONFIG, + Manifest.permission.SET_TIME); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK_BOOT, + RollbackManager.PROPERTY_ROLLBACK_LIFETIME_MILLIS, + Long.toString(expirationTime), false /* makeDefault*/); + + // Pull the new expiration time from DeviceConfig + rm.reloadPersistedData(); + + // Install app A with rollback enabled + RollbackTestUtils.uninstall(TEST_APP_A); + RollbackTestUtils.install("RollbackTestAppAv1.apk", false); + RollbackTestUtils.install("RollbackTestAppAv2.apk", true); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + Thread.sleep(expirationTime / 2); + + // Install app B with rollback enabled + RollbackTestUtils.uninstall(TEST_APP_B); + RollbackTestUtils.install("RollbackTestAppBv1.apk", false); + RollbackTestUtils.install("RollbackTestAppBv2.apk", true); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_B)); + // 1 second buffer + Thread.sleep(1000); + + try { + // Change the time + RollbackTestUtils.forwardTimeBy(expirationTime); + + // 1 second buffer to allow Rollback Manager to handle time change before loading + // persisted data + Thread.sleep(1000); + + // Load timestamps from storage + rm.reloadPersistedData(); + + // Wait until rollback for app A has expired + // This will trigger an expiration run that should expire app A but not B + Thread.sleep(expirationTime / 2); + assertNull(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), TEST_APP_A)); + + // Rollback for app B should not be expired + RollbackInfo rollback = getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_B); + assertRollbackInfoEquals(TEST_APP_B, 2, 1, rollback); + + // Wait until rollback for app B has expired + Thread.sleep(expirationTime / 2); + assertNull(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), TEST_APP_B)); + } finally { + RollbackTestUtils.forwardTimeBy(-expirationTime); + } + } finally { + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK_BOOT, + RollbackManager.PROPERTY_ROLLBACK_LIFETIME_MILLIS, + Long.toString(defaultExpirationTime), false /* makeDefault*/); + RollbackTestUtils.dropShellPermissionIdentity(); + } + } + + /** + * Test explicit expiration of rollbacks. + * Does not test the scheduling aspects of rollback expiration. + */ + @Test + public void testRollbackExpiration() throws Exception { + try { + RollbackTestUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.TEST_MANAGE_ROLLBACKS); + + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + RollbackTestUtils.uninstall(TEST_APP_A); + RollbackTestUtils.install("RollbackTestAppAv1.apk", false); + RollbackTestUtils.install("RollbackTestAppAv2.apk", true); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + // The app should now be available for rollback. + RollbackInfo rollback = getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_A); + assertRollbackInfoEquals(TEST_APP_A, 2, 1, rollback); + + // Expire the rollback. + rm.expireRollbackForPackage(TEST_APP_A); + + // The rollback should no longer be available. + assertNull(getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_A)); + } finally { + RollbackTestUtils.dropShellPermissionIdentity(); + } + } + + /** + * Test that app user data is rolled back. + */ + @Test + public void testUserDataRollback() throws Exception { + try { + RollbackTestUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.TEST_MANAGE_ROLLBACKS); + + RollbackTestUtils.uninstall(TEST_APP_A); + RollbackTestUtils.install("RollbackTestAppAv1.apk", false); + processUserData(TEST_APP_A); + RollbackTestUtils.install("RollbackTestAppAv2.apk", true); + processUserData(TEST_APP_A); + + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + RollbackInfo rollback = getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_A); + RollbackTestUtils.rollback(rollback.getRollbackId()); + processUserData(TEST_APP_A); + } finally { + RollbackTestUtils.dropShellPermissionIdentity(); + } + } + + /** + * Test rollback of apks involving splits. + */ + @Test + public void testRollbackWithSplits() throws Exception { + try { + RollbackTestUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.TEST_MANAGE_ROLLBACKS); + + RollbackTestUtils.uninstall(TEST_APP_A); + RollbackTestUtils.installSplit(false, + "RollbackTestAppASplitV1.apk", + "RollbackTestAppASplitV1_anydpi.apk"); + processUserData(TEST_APP_A); + + RollbackTestUtils.installSplit(true, + "RollbackTestAppASplitV2.apk", + "RollbackTestAppASplitV2_anydpi.apk"); + processUserData(TEST_APP_A); + + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + RollbackInfo rollback = getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_A); + assertNotNull(rollback); + RollbackTestUtils.rollback(rollback.getRollbackId()); + processUserData(TEST_APP_A); + } finally { + RollbackTestUtils.dropShellPermissionIdentity(); + } + } + + /** + * Test restrictions on rollback broadcast sender. + * A random app should not be able to send a ROLLBACK_COMMITTED broadcast. + */ + @Test + public void testRollbackBroadcastRestrictions() throws Exception { + RollbackBroadcastReceiver broadcastReceiver = new RollbackBroadcastReceiver(); + Intent broadcast = new Intent(Intent.ACTION_ROLLBACK_COMMITTED); + try { + InstrumentationRegistry.getContext().sendBroadcast(broadcast); + fail("Succeeded in sending restricted broadcast from app context."); + } catch (SecurityException se) { + // Expected behavior. + } + + // Confirm that we really haven't received the broadcast. + // TODO: How long to wait for the expected timeout? + assertNull(broadcastReceiver.poll(5, TimeUnit.SECONDS)); + + // TODO: Do we need to do this? Do we need to ensure this is always + // called, even when the test fails? + broadcastReceiver.unregister(); + } + + /** + * Regression test for rollback in the case when multiple apps are + * available for rollback at the same time. + */ + @Test + public void testMultipleRollbackAvailable() throws Exception { + try { + RollbackTestUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.TEST_MANAGE_ROLLBACKS); + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + + // Prep installation of the test apps. + RollbackTestUtils.uninstall(TEST_APP_A); + RollbackTestUtils.install("RollbackTestAppAv1.apk", false); + RollbackTestUtils.install("RollbackTestAppAv2.apk", true); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + RollbackTestUtils.uninstall(TEST_APP_B); + RollbackTestUtils.install("RollbackTestAppBv1.apk", false); + RollbackTestUtils.install("RollbackTestAppBv2.apk", true); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_B)); + + // Both test apps should now be available for rollback, and the + // RollbackInfo returned for the rollbacks should be correct. + RollbackInfo rollbackA = getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_A); + assertRollbackInfoEquals(TEST_APP_A, 2, 1, rollbackA); + + RollbackInfo rollbackB = getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_B); + assertRollbackInfoEquals(TEST_APP_B, 2, 1, rollbackB); + + // Executing rollback should roll back the correct package. + RollbackTestUtils.rollback(rollbackA.getRollbackId()); + assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_B)); + + RollbackTestUtils.uninstall(TEST_APP_A); + RollbackTestUtils.install("RollbackTestAppAv1.apk", false); + RollbackTestUtils.install("RollbackTestAppAv2.apk", true); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + RollbackTestUtils.rollback(rollbackB.getRollbackId()); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_B)); + } finally { + RollbackTestUtils.dropShellPermissionIdentity(); + } + } + + /** + * Test that the MANAGE_ROLLBACKS permission is required to call + * RollbackManager APIs. + */ + @Test + public void testManageRollbacksPermission() throws Exception { + // We shouldn't be allowed to call any of the RollbackManager APIs + // without the MANAGE_ROLLBACKS permission. + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + + try { + rm.getAvailableRollbacks(); + fail("expected SecurityException"); + } catch (SecurityException e) { + // Expected. + } + + try { + rm.getRecentlyCommittedRollbacks(); + fail("expected SecurityException"); + } catch (SecurityException e) { + // Expected. + } + + try { + // TODO: What if the implementation checks arguments for non-null + // first? Then this test isn't valid. + rm.commitRollback(0, Collections.emptyList(), null); + fail("expected SecurityException"); + } catch (SecurityException e) { + // Expected. + } + + try { + rm.reloadPersistedData(); + fail("expected SecurityException"); + } catch (SecurityException e) { + // Expected. + } + + try { + rm.expireRollbackForPackage(TEST_APP_A); + fail("expected SecurityException"); + } catch (SecurityException e) { + // Expected. + } + } + + /** + * Test that you cannot enable rollback for a package without the + * MANAGE_ROLLBACKS permission. + */ + @Test + public void testEnableRollbackPermission() throws Exception { + try { + RollbackTestUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES); + + RollbackTestUtils.uninstall(TEST_APP_A); + RollbackTestUtils.install("RollbackTestAppAv1.apk", /* enableRollback */ false); + assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + RollbackTestUtils.install("RollbackTestAppAv2.apk", /* enableRollback */ true); + + // We expect v2 of the app was installed, but rollback has not + // been enabled. + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + RollbackTestUtils.adoptShellPermissionIdentity( + Manifest.permission.TEST_MANAGE_ROLLBACKS); + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + assertNull(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), TEST_APP_A)); + } finally { + RollbackTestUtils.dropShellPermissionIdentity(); + } + } + + /** + * Test that you cannot enable rollback for a non-module package when + * holding the MANAGE_ROLLBACKS permission. + */ + @Test + public void testNonModuleEnableRollback() throws Exception { + try { + RollbackTestUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.MANAGE_ROLLBACKS); + + RollbackTestUtils.uninstall(TEST_APP_A); + RollbackTestUtils.install("RollbackTestAppAv1.apk", /* enableRollback */ false); + assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + RollbackTestUtils.install("RollbackTestAppAv2.apk", /* enableRollback */ true); + + // We expect v2 of the app was installed, but rollback has not + // been enabled because the test app is not a module. + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + assertNull(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), TEST_APP_A)); + } finally { + RollbackTestUtils.dropShellPermissionIdentity(); + } + } + + /** + * Test rollback of multi-package installs is implemented. + */ + @Test + public void testMultiPackage() throws Exception { + try { + RollbackTestUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.TEST_MANAGE_ROLLBACKS); + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + + // Prep installation of the test apps. + RollbackTestUtils.uninstall(TEST_APP_A); + RollbackTestUtils.uninstall(TEST_APP_B); + RollbackTestUtils.installMultiPackage(false, + "RollbackTestAppAv1.apk", + "RollbackTestAppBv1.apk"); + processUserData(TEST_APP_A); + processUserData(TEST_APP_B); + RollbackTestUtils.installMultiPackage(true, + "RollbackTestAppAv2.apk", + "RollbackTestAppBv2.apk"); + processUserData(TEST_APP_A); + processUserData(TEST_APP_B); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_B)); + + // TEST_APP_A should now be available for rollback. + RollbackInfo rollback = getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_A); + assertRollbackInfoForAandB(rollback); + + // Rollback the app. It should cause both test apps to be rolled + // back. + RollbackTestUtils.rollback(rollback.getRollbackId()); + assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_B)); + + // We should see recent rollbacks listed for both A and B. + Thread.sleep(1000); + RollbackInfo rollbackA = getUniqueRollbackInfoForPackage( + rm.getRecentlyCommittedRollbacks(), TEST_APP_A); + + RollbackInfo rollbackB = getUniqueRollbackInfoForPackage( + rm.getRecentlyCommittedRollbacks(), TEST_APP_B); + assertRollbackInfoForAandB(rollbackB); + + assertEquals(rollbackA.getRollbackId(), rollbackB.getRollbackId()); + + processUserData(TEST_APP_A); + processUserData(TEST_APP_B); + } finally { + RollbackTestUtils.dropShellPermissionIdentity(); + } + } + + /** + * Test failure to enable rollback for multi-package installs. + * If any one of the packages fail to enable rollback, we shouldn't enable + * rollback for any package. + */ + @Test + public void testMultiPackageEnableFail() throws Exception { + try { + RollbackTestUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.TEST_MANAGE_ROLLBACKS); + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + + RollbackTestUtils.uninstall(TEST_APP_A); + RollbackTestUtils.uninstall(TEST_APP_B); + RollbackTestUtils.install("RollbackTestAppAv1.apk", false); + + // We should fail to enable rollback here because TestApp B is not + // already installed. + RollbackTestUtils.installMultiPackage(true, + "RollbackTestAppAv2.apk", + "RollbackTestAppBv2.apk"); + + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_B)); + + assertNull(getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_A)); + assertNull(getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_B)); + } finally { + RollbackTestUtils.dropShellPermissionIdentity(); + } + } + + @Test + @Ignore("b/120200473") + /** + * Test rollback when app is updated to its same version. + */ + public void testSameVersionUpdate() throws Exception { + try { + RollbackTestUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.TEST_MANAGE_ROLLBACKS); + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + + RollbackTestUtils.uninstall(TEST_APP_A); + RollbackTestUtils.install("RollbackTestAppAv1.apk", false); + RollbackTestUtils.install("RollbackTestAppAv2.apk", true); + RollbackTestUtils.install("RollbackTestAppACrashingV2.apk", true); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + RollbackInfo rollback = getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_A); + assertRollbackInfoEquals(TEST_APP_A, 2, 2, rollback); + + RollbackTestUtils.rollback(rollback.getRollbackId()); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + rollback = getUniqueRollbackInfoForPackage( + rm.getRecentlyCommittedRollbacks(), TEST_APP_A); + assertRollbackInfoEquals(TEST_APP_A, 2, 2, rollback); + } finally { + RollbackTestUtils.dropShellPermissionIdentity(); + } + } + + /** + * Test bad update automatic rollback. + */ + @Test + public void testBadUpdateRollback() throws Exception { + BroadcastReceiver crashCountReceiver = null; + Context context = InstrumentationRegistry.getContext(); + try { + RollbackTestUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.MANAGE_ROLLBACKS, + Manifest.permission.TEST_MANAGE_ROLLBACKS, + Manifest.permission.KILL_BACKGROUND_PROCESSES, + Manifest.permission.RESTART_PACKAGES); + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + + // Prep installation of the test apps. + RollbackTestUtils.uninstall(TEST_APP_A); + RollbackTestUtils.install("RollbackTestAppAv1.apk", false); + RollbackTestUtils.install("RollbackTestAppACrashingV2.apk", true); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + RollbackTestUtils.uninstall(TEST_APP_B); + RollbackTestUtils.install("RollbackTestAppBv1.apk", false); + RollbackTestUtils.install("RollbackTestAppBv2.apk", true); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_B)); + + // Both test apps should now be available for rollback, and the + // targetPackage returned for rollback should be correct. + RollbackInfo rollbackA = getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_A); + assertRollbackInfoEquals(TEST_APP_A, 2, 1, rollbackA); + + RollbackInfo rollbackB = getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_B); + assertRollbackInfoEquals(TEST_APP_B, 2, 1, rollbackB); + + // Register rollback committed receiver + RollbackBroadcastReceiver rollbackReceiver = new RollbackBroadcastReceiver(); + + // Crash TEST_APP_A PackageWatchdog#TRIGGER_FAILURE_COUNT times to trigger rollback + crashCountReceiver = RollbackTestUtils.sendCrashBroadcast(context, TEST_APP_A, 5); + + // Verify we received a broadcast for the rollback. + rollbackReceiver.take(); + + // TEST_APP_A is automatically rolled back by the RollbackPackageHealthObserver + assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + // Instrumented app is still the package installer + String installer = context.getPackageManager().getInstallerPackageName(TEST_APP_A); + assertEquals(INSTRUMENTED_APP, installer); + // TEST_APP_B is untouched + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_B)); + } finally { + RollbackTestUtils.dropShellPermissionIdentity(); + if (crashCountReceiver != null) { + context.unregisterReceiver(crashCountReceiver); + } + } + } + + /** + * Test race between roll back and roll forward. + */ + @Test + public void testRollForwardRace() throws Exception { + try { + RollbackTestUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.TEST_MANAGE_ROLLBACKS, + Manifest.permission.MANAGE_ROLLBACKS); + + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + + RollbackTestUtils.uninstall(TEST_APP_A); + RollbackTestUtils.install("RollbackTestAppAv1.apk", false); + RollbackTestUtils.install("RollbackTestAppAv2.apk", true); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + RollbackInfo rollback = getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_A); + assertRollbackInfoEquals(TEST_APP_A, 2, 1, rollback); + + // Install a new version of package A, then immediately rollback + // the previous version. We expect the rollback to fail, because + // it is no longer available. + // There are a couple different ways this could fail depending on + // thread interleaving, so don't ignore flaky failures. + RollbackTestUtils.install("RollbackTestAppAv3.apk", false); + try { + RollbackTestUtils.rollback(rollback.getRollbackId()); + // Note: Don't ignore flaky failures here. + fail("Expected rollback to fail, but it did not."); + } catch (AssertionError e) { + Log.i(TAG, "Note expected failure: ", e); + // Expected + } + + // Note: Don't ignore flaky failures here. + assertEquals(3, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + } finally { + RollbackTestUtils.dropShellPermissionIdentity(); + } + } + + @Test + @Ignore("b/136605788") + public void testEnableRollbackTimeoutFailsRollback() throws Exception { + try { + RollbackTestUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.TEST_MANAGE_ROLLBACKS, + Manifest.permission.MANAGE_ROLLBACKS, + Manifest.permission.WRITE_DEVICE_CONFIG); + + //setting the timeout to a very short amount that will definitely be triggered + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK, + PROPERTY_ENABLE_ROLLBACK_TIMEOUT_MILLIS, + Long.toString(1), false /* makeDefault*/); + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + + RollbackTestUtils.uninstall(TEST_APP_A); + RollbackTestUtils.install("RollbackTestAppAv1.apk", false); + RollbackTestUtils.install("RollbackTestAppAv2.apk", true); + + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + assertNull(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), TEST_APP_A)); + } finally { + //setting the timeout back to default + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK, + PROPERTY_ENABLE_ROLLBACK_TIMEOUT_MILLIS, + null, false /* makeDefault*/); + RollbackTestUtils.dropShellPermissionIdentity(); + } + } + + // Helper function to test that the given rollback info is a rollback for + // the atomic set {A2, B2} -> {A1, B1}. + private void assertRollbackInfoForAandB(RollbackInfo rollback) { + assertNotNull(rollback); + assertEquals(2, rollback.getPackages().size()); + if (TEST_APP_A.equals(rollback.getPackages().get(0).getPackageName())) { + assertPackageRollbackInfoEquals(TEST_APP_A, 2, 1, rollback.getPackages().get(0)); + assertPackageRollbackInfoEquals(TEST_APP_B, 2, 1, rollback.getPackages().get(1)); + } else { + assertPackageRollbackInfoEquals(TEST_APP_B, 2, 1, rollback.getPackages().get(0)); + assertPackageRollbackInfoEquals(TEST_APP_A, 2, 1, rollback.getPackages().get(1)); + } + } +} diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTestUtils.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTestUtils.java new file mode 100644 index 000000000000..a9e20cdb191b --- /dev/null +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTestUtils.java @@ -0,0 +1,547 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tests.rollback; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import android.app.ActivityManager; +import android.app.AlarmManager; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageInstaller; +import android.content.pm.PackageManager; +import android.content.pm.VersionedPackage; +import android.content.rollback.PackageRollbackInfo; +import android.content.rollback.RollbackInfo; +import android.content.rollback.RollbackManager; +import android.os.Handler; +import android.os.HandlerThread; +import android.util.Log; + +import androidx.test.InstrumentationRegistry; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.SynchronousQueue; + +/** + * Utilities to facilitate testing rollbacks. + */ +class RollbackTestUtils { + + private static final String TAG = "RollbackTest"; + + static RollbackManager getRollbackManager() { + Context context = InstrumentationRegistry.getContext(); + RollbackManager rm = (RollbackManager) context.getSystemService(Context.ROLLBACK_SERVICE); + if (rm == null) { + throw new AssertionError("Failed to get RollbackManager"); + } + return rm; + } + + private static void setTime(long millis) { + Context context = InstrumentationRegistry.getContext(); + AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + am.setTime(millis); + } + + static void forwardTimeBy(long offsetMillis) { + setTime(System.currentTimeMillis() + offsetMillis); + Log.i(TAG, "Forwarded time on device by " + offsetMillis + " millis"); + } + + /** + * Returns the version of the given package installed on device. + * Returns -1 if the package is not currently installed. + */ + static long getInstalledVersion(String packageName) { + PackageInfo pi = getPackageInfo(packageName); + if (pi == null) { + return -1; + } else { + return pi.getLongVersionCode(); + } + } + + private static boolean isSystemAppWithoutUpdate(String packageName) { + PackageInfo pi = getPackageInfo(packageName); + if (pi == null) { + return false; + } else { + return ((pi.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) + && ((pi.applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) == 0); + } + } + + private static PackageInfo getPackageInfo(String packageName) { + Context context = InstrumentationRegistry.getContext(); + PackageManager pm = context.getPackageManager(); + try { + return pm.getPackageInfo(packageName, PackageManager.MATCH_APEX); + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } + + private static void assertStatusSuccess(Intent result) { + int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS, + PackageInstaller.STATUS_FAILURE); + if (status == -1) { + throw new AssertionError("PENDING USER ACTION"); + } else if (status > 0) { + String message = result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE); + throw new AssertionError(message == null ? "UNKNOWN FAILURE" : message); + } + } + + /** + * Uninstalls the given package. + * Does nothing if the package is not installed. + * @throws AssertionError if package can't be uninstalled. + */ + static void uninstall(String packageName) throws InterruptedException, IOException { + // No need to uninstall if the package isn't installed or is installed on /system. + if (getInstalledVersion(packageName) == -1 || isSystemAppWithoutUpdate(packageName)) { + return; + } + + Context context = InstrumentationRegistry.getContext(); + PackageManager packageManager = context.getPackageManager(); + PackageInstaller packageInstaller = packageManager.getPackageInstaller(); + packageInstaller.uninstall(packageName, LocalIntentSender.getIntentSender()); + assertStatusSuccess(LocalIntentSender.getIntentSenderResult()); + } + + /** + * Commit the given rollback. + * @throws AssertionError if the rollback fails. + */ + static void rollback(int rollbackId, VersionedPackage... causePackages) + throws InterruptedException { + RollbackManager rm = getRollbackManager(); + rm.commitRollback(rollbackId, Arrays.asList(causePackages), + LocalIntentSender.getIntentSender()); + Intent result = LocalIntentSender.getIntentSenderResult(); + int status = result.getIntExtra(RollbackManager.EXTRA_STATUS, + RollbackManager.STATUS_FAILURE); + if (status != RollbackManager.STATUS_SUCCESS) { + String message = result.getStringExtra(RollbackManager.EXTRA_STATUS_MESSAGE); + throw new AssertionError(message); + } + } + + /** + * Installs the apk with the given name. + * + * @param resourceName name of class loader resource for the apk to + * install. + * @param enableRollback if rollback should be enabled. + * @throws AssertionError if the installation fails. + */ + static void install(String resourceName, boolean enableRollback) + throws InterruptedException, IOException { + installSplit(enableRollback, resourceName); + } + + /** + * Installs the apk with the given name and its splits. + * + * @param enableRollback if rollback should be enabled. + * @param resourceNames names of class loader resources for the apk and + * its splits to install. + * @throws AssertionError if the installation fails. + */ + static void installSplit(boolean enableRollback, String... resourceNames) + throws InterruptedException, IOException { + Context context = InstrumentationRegistry.getContext(); + PackageInstaller.Session session = null; + PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller(); + PackageInstaller.SessionParams params = new PackageInstaller.SessionParams( + PackageInstaller.SessionParams.MODE_FULL_INSTALL); + params.setEnableRollback(enableRollback); + int sessionId = packageInstaller.createSession(params); + session = packageInstaller.openSession(sessionId); + + ClassLoader loader = RollbackTest.class.getClassLoader(); + for (String resourceName : resourceNames) { + try (OutputStream packageInSession = session.openWrite(resourceName, 0, -1); + InputStream is = loader.getResourceAsStream(resourceName);) { + byte[] buffer = new byte[4096]; + int n; + while ((n = is.read(buffer)) >= 0) { + packageInSession.write(buffer, 0, n); + } + } + } + + // Commit the session (this will start the installation workflow). + session.commit(LocalIntentSender.getIntentSender()); + assertStatusSuccess(LocalIntentSender.getIntentSenderResult()); + } + + /** Launches {@code packageName} with {@link Intent#ACTION_MAIN}. */ + private static void launchPackage(String packageName) + throws InterruptedException, IOException { + Context context = InstrumentationRegistry.getContext(); + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setPackage(packageName); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + context.startActivity(intent); + } + + /** + * Installs the APKs or APEXs with the given resource names as an atomic + * set. A resource is assumed to be an APEX if it has the .apex extension. + * <p> + * In case of staged installs, this function will return succesfully after + * the staged install has been committed and is ready for the device to + * reboot. + * + * @param staged if the rollback should be staged. + * @param enableRollback if rollback should be enabled. + * @param resourceNames names of the class loader resource for the apks to + * install. + * @throws AssertionError if the installation fails. + */ + private static void install(boolean staged, boolean enableRollback, + String... resourceNames) throws InterruptedException, IOException { + Context context = InstrumentationRegistry.getContext(); + PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller(); + + PackageInstaller.SessionParams multiPackageParams = new PackageInstaller.SessionParams( + PackageInstaller.SessionParams.MODE_FULL_INSTALL); + multiPackageParams.setMultiPackage(); + if (staged) { + multiPackageParams.setStaged(); + } + // TODO: Do we set this on the parent params, the child params, or + // both? + multiPackageParams.setEnableRollback(enableRollback); + int multiPackageId = packageInstaller.createSession(multiPackageParams); + PackageInstaller.Session multiPackage = packageInstaller.openSession(multiPackageId); + + for (String resourceName : resourceNames) { + PackageInstaller.Session session = null; + PackageInstaller.SessionParams params = new PackageInstaller.SessionParams( + PackageInstaller.SessionParams.MODE_FULL_INSTALL); + if (staged) { + params.setStaged(); + } + if (resourceName.endsWith(".apex")) { + params.setInstallAsApex(); + } + params.setEnableRollback(enableRollback); + int sessionId = packageInstaller.createSession(params); + session = packageInstaller.openSession(sessionId); + + ClassLoader loader = RollbackTest.class.getClassLoader(); + try (OutputStream packageInSession = session.openWrite(resourceName, 0, -1); + InputStream is = loader.getResourceAsStream(resourceName);) { + byte[] buffer = new byte[4096]; + int n; + while ((n = is.read(buffer)) >= 0) { + packageInSession.write(buffer, 0, n); + } + } + multiPackage.addChildSessionId(sessionId); + } + + // Commit the session (this will start the installation workflow). + multiPackage.commit(LocalIntentSender.getIntentSender()); + assertStatusSuccess(LocalIntentSender.getIntentSenderResult()); + + if (staged) { + waitForSessionReady(multiPackageId); + } + } + + /** + * Installs the apks with the given resource names as an atomic set. + * + * @param enableRollback if rollback should be enabled. + * @param resourceNames names of the class loader resource for the apks to + * install. + * @throws AssertionError if the installation fails. + */ + static void installMultiPackage(boolean enableRollback, String... resourceNames) + throws InterruptedException, IOException { + install(false, enableRollback, resourceNames); + } + + /** + * Installs the APKs or APEXs with the given resource names as a staged + * atomic set. A resource is assumed to be an APEX if it has the .apex + * extension. + * + * @param enableRollback if rollback should be enabled. + * @param resourceNames names of the class loader resource for the apks to + * install. + * @throws AssertionError if the installation fails. + */ + static void installStaged(boolean enableRollback, String... resourceNames) + throws InterruptedException, IOException { + install(true, enableRollback, resourceNames); + } + + static void adoptShellPermissionIdentity(String... permissions) { + InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity(permissions); + } + + static void dropShellPermissionIdentity() { + InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .dropShellPermissionIdentity(); + } + + /** + * Returns the RollbackInfo with a given package in the list of rollbacks. + * Throws an assertion failure if there is more than one such rollback + * info. Returns null if there are no such rollback infos. + */ + static RollbackInfo getUniqueRollbackInfoForPackage(List<RollbackInfo> rollbacks, + String packageName) { + RollbackInfo found = null; + for (RollbackInfo rollback : rollbacks) { + for (PackageRollbackInfo info : rollback.getPackages()) { + if (packageName.equals(info.getPackageName())) { + assertNull(found); + found = rollback; + break; + } + } + } + return found; + } + + /** + * Asserts that the given PackageRollbackInfo has the expected package + * name and versions. + */ + static void assertPackageRollbackInfoEquals(String packageName, + long versionRolledBackFrom, long versionRolledBackTo, + PackageRollbackInfo info) { + assertEquals(packageName, info.getPackageName()); + assertEquals(packageName, info.getVersionRolledBackFrom().getPackageName()); + assertEquals(versionRolledBackFrom, info.getVersionRolledBackFrom().getLongVersionCode()); + assertEquals(packageName, info.getVersionRolledBackTo().getPackageName()); + assertEquals(versionRolledBackTo, info.getVersionRolledBackTo().getLongVersionCode()); + } + + /** + * Asserts that the given RollbackInfo has the given packages with expected + * package names and all are rolled to and from the same given versions. + */ + static void assertRollbackInfoEquals(String[] packageNames, + long versionRolledBackFrom, long versionRolledBackTo, + RollbackInfo info, VersionedPackage... causePackages) { + assertNotNull(info); + assertEquals(packageNames.length, info.getPackages().size()); + int foundPackages = 0; + for (String packageName : packageNames) { + for (PackageRollbackInfo pkgRollbackInfo : info.getPackages()) { + if (packageName.equals(pkgRollbackInfo.getPackageName())) { + foundPackages++; + assertPackageRollbackInfoEquals(packageName, versionRolledBackFrom, + versionRolledBackTo, pkgRollbackInfo); + break; + } + } + } + assertEquals(packageNames.length, foundPackages); + assertEquals(causePackages.length, info.getCausePackages().size()); + for (int i = 0; i < causePackages.length; ++i) { + assertEquals(causePackages[i].getPackageName(), + info.getCausePackages().get(i).getPackageName()); + assertEquals(causePackages[i].getLongVersionCode(), + info.getCausePackages().get(i).getLongVersionCode()); + } + } + + /** + * Asserts that the given RollbackInfo has a single package with expected + * package name and versions. + */ + static void assertRollbackInfoEquals(String packageName, + long versionRolledBackFrom, long versionRolledBackTo, + RollbackInfo info, VersionedPackage... causePackages) { + String[] packageNames = {packageName}; + assertRollbackInfoEquals(packageNames, versionRolledBackFrom, versionRolledBackTo, info, + causePackages); + } + + /** + * Waits for the given session to be marked as ready. + * Throws an assertion if the session fails. + */ + static void waitForSessionReady(int sessionId) { + BlockingQueue<PackageInstaller.SessionInfo> sessionStatus = new LinkedBlockingQueue<>(); + BroadcastReceiver sessionUpdatedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + PackageInstaller.SessionInfo info = + intent.getParcelableExtra(PackageInstaller.EXTRA_SESSION); + if (info != null && info.getSessionId() == sessionId) { + if (info.isStagedSessionReady() || info.isStagedSessionFailed()) { + try { + sessionStatus.put(info); + } catch (InterruptedException e) { + Log.e(TAG, "Failed to put session info.", e); + } + } + } + } + }; + IntentFilter sessionUpdatedFilter = + new IntentFilter(PackageInstaller.ACTION_SESSION_UPDATED); + + Context context = InstrumentationRegistry.getContext(); + context.registerReceiver(sessionUpdatedReceiver, sessionUpdatedFilter); + + PackageInstaller installer = context.getPackageManager().getPackageInstaller(); + PackageInstaller.SessionInfo info = installer.getSessionInfo(sessionId); + + try { + if (info.isStagedSessionReady() || info.isStagedSessionFailed()) { + sessionStatus.put(info); + } + + info = sessionStatus.take(); + context.unregisterReceiver(sessionUpdatedReceiver); + if (info.isStagedSessionFailed()) { + throw new AssertionError(info.getStagedSessionErrorMessage()); + } + } catch (InterruptedException e) { + throw new AssertionError(e); + } + } + + private static final String NO_RESPONSE = "NO RESPONSE"; + + /** + * Calls into the test app to process user data. + * Asserts if the user data could not be processed or was version + * incompatible with the previously processed user data. + */ + static void processUserData(String packageName) { + Intent intent = new Intent(); + intent.setComponent(new ComponentName(packageName, + "com.android.tests.rollback.testapp.ProcessUserData")); + Context context = InstrumentationRegistry.getContext(); + + HandlerThread handlerThread = new HandlerThread("RollbackTestHandlerThread"); + handlerThread.start(); + + // It can sometimes take a while after rollback before the app will + // receive this broadcast, so try a few times in a loop. + String result = NO_RESPONSE; + for (int i = 0; result.equals(NO_RESPONSE) && i < 5; ++i) { + BlockingQueue<String> resultQueue = new LinkedBlockingQueue<>(); + context.sendOrderedBroadcast(intent, null, new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (getResultCode() == 1) { + resultQueue.add("OK"); + } else { + // If the test app doesn't receive the broadcast or + // fails to set the result data, then getResultData + // here returns the initial NO_RESPONSE data passed to + // the sendOrderedBroadcast call. + resultQueue.add(getResultData()); + } + } + }, new Handler(handlerThread.getLooper()), 0, NO_RESPONSE, null); + + try { + result = resultQueue.take(); + } catch (InterruptedException e) { + throw new AssertionError(e); + } + } + + handlerThread.quit(); + if (!"OK".equals(result)) { + fail(result); + } + } + + /** + * Return the rollback info for a recently committed rollback, by matching the rollback id, or + * return null if no matching rollback is found. + */ + static RollbackInfo getRecentlyCommittedRollbackInfoById(int getRollbackId) { + for (RollbackInfo info : getRollbackManager().getRecentlyCommittedRollbacks()) { + if (info.getRollbackId() == getRollbackId) { + return info; + } + } + return null; + } + + /** + * Send broadcast to crash {@code packageName} {@code count} times. If {@code count} is at least + * {@link PackageWatchdog#TRIGGER_FAILURE_COUNT}, watchdog crash detection will be triggered. + */ + static BroadcastReceiver sendCrashBroadcast(Context context, String packageName, int count) + throws InterruptedException, IOException { + BlockingQueue<Integer> crashQueue = new SynchronousQueue<>(); + IntentFilter crashCountFilter = new IntentFilter(); + crashCountFilter.addAction("com.android.tests.rollback.CRASH"); + crashCountFilter.addCategory(Intent.CATEGORY_DEFAULT); + + BroadcastReceiver crashCountReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + try { + // Sleep long enough for packagewatchdog to be notified of crash + Thread.sleep(1000); + // Kill app and close AppErrorDialog + ActivityManager am = context.getSystemService(ActivityManager.class); + am.killBackgroundProcesses(packageName); + // Allow another package launch + crashQueue.put(intent.getIntExtra("count", 0)); + } catch (InterruptedException e) { + fail("Failed to communicate with test app"); + } + } + }; + context.registerReceiver(crashCountReceiver, crashCountFilter); + + do { + launchPackage(packageName); + } while(crashQueue.take() < count); + return crashCountReceiver; + } +} diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java new file mode 100644 index 000000000000..1a29c4c11457 --- /dev/null +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tests.rollback; + +import static com.android.tests.rollback.RollbackTestUtils.assertRollbackInfoEquals; +import static com.android.tests.rollback.RollbackTestUtils.getUniqueRollbackInfoForPackage; + +import android.Manifest; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.VersionedPackage; +import android.content.rollback.RollbackInfo; +import android.content.rollback.RollbackManager; + +import androidx.test.InstrumentationRegistry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Tests for rollback of staged installs. + * <p> + * Note: These tests require reboot in between test phases. They are run + * specially so that the testFooEnableRollback, testFooCommitRollback, and + * testFooConfirmRollback phases of each test are run in order with reboots in + * between them. + */ +@RunWith(JUnit4.class) +public class StagedRollbackTest { + + private static final String TAG = "RollbackTest"; + private static final String TEST_APP_A = "com.android.tests.rollback.testapp.A"; + private static final String TEST_APP_A_V1 = "RollbackTestAppAv1.apk"; + private static final String TEST_APP_A_CRASHING_V2 = "RollbackTestAppACrashingV2.apk"; + private static final String NETWORK_STACK_CONNECTOR_CLASS = + "android.net.INetworkStackConnector"; + + /** + * Adopts common shell permissions needed for rollback tests. + */ + @Before + public void adoptShellPermissions() { + RollbackTestUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.TEST_MANAGE_ROLLBACKS, + Manifest.permission.KILL_BACKGROUND_PROCESSES); + } + + /** + * Drops shell permissions needed for rollback tests. + */ + @After + public void dropShellPermissions() { + RollbackTestUtils.dropShellPermissionIdentity(); + } + + /** + * Test rollbacks of staged installs involving only apks with bad update. + * Enable rollback phase. + */ + @Test + public void testBadApkOnlyEnableRollback() throws Exception { + RollbackTestUtils.uninstall(TEST_APP_A); + assertEquals(-1, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + RollbackTestUtils.install(TEST_APP_A_V1, false); + assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + RollbackTestUtils.processUserData(TEST_APP_A); + + RollbackTestUtils.installStaged(true, TEST_APP_A_CRASHING_V2); + + // At this point, the host test driver will reboot the device and run + // testBadApkOnlyConfirmEnableRollback(). + } + + /** + * Test rollbacks of staged installs involving only apks with bad update. + * Confirm that rollback was successfully enabled. + */ + @Test + public void testBadApkOnlyConfirmEnableRollback() throws Exception { + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + RollbackTestUtils.processUserData(TEST_APP_A); + + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + RollbackInfo rollback = getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_A); + assertRollbackInfoEquals(TEST_APP_A, 2, 1, rollback); + assertTrue(rollback.isStaged()); + + // At this point, the host test driver will run + // testBadApkOnlyTriggerRollback(). + } + + /** + * Test rollbacks of staged installs involving only apks with bad update. + * Trigger rollback phase. This is expected to fail due to watchdog + * rebooting the test out from under it. + */ + @Test + public void testBadApkOnlyTriggerRollback() throws Exception { + BroadcastReceiver crashCountReceiver = null; + Context context = InstrumentationRegistry.getContext(); + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + + try { + // Crash TEST_APP_A PackageWatchdog#TRIGGER_FAILURE_COUNT times to trigger rollback + crashCountReceiver = RollbackTestUtils.sendCrashBroadcast(context, TEST_APP_A, 5); + } finally { + if (crashCountReceiver != null) { + context.unregisterReceiver(crashCountReceiver); + } + } + + // We expect the device to be rebooted automatically. Wait for that to + // happen. At that point, the host test driver will wait for the + // device to come back up and run testApkOnlyConfirmRollback(). + Thread.sleep(30 * 1000); + + fail("watchdog did not trigger reboot"); + } + + /** + * Test rollbacks of staged installs involving only apks. + * Confirm rollback phase. + */ + @Test + public void testBadApkOnlyConfirmRollback() throws Exception { + assertEquals(1, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + RollbackTestUtils.processUserData(TEST_APP_A); + + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + RollbackInfo rollback = getUniqueRollbackInfoForPackage( + rm.getRecentlyCommittedRollbacks(), TEST_APP_A); + assertRollbackInfoEquals(TEST_APP_A, 2, 1, rollback, new VersionedPackage(TEST_APP_A, 2)); + assertTrue(rollback.isStaged()); + assertNotEquals(-1, rollback.getCommittedSessionId()); + } + + @Test + public void resetNetworkStack() throws Exception { + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + String networkStack = getNetworkStackPackageName(); + + rm.expireRollbackForPackage(networkStack); + RollbackTestUtils.uninstall(networkStack); + + assertNull(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), + networkStack)); + } + + @Test + public void assertNetworkStackRollbackAvailable() throws Exception { + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + assertNotNull(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), + getNetworkStackPackageName())); + } + + @Test + public void assertNetworkStackRollbackCommitted() throws Exception { + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + assertNotNull(getUniqueRollbackInfoForPackage(rm.getRecentlyCommittedRollbacks(), + getNetworkStackPackageName())); + } + + @Test + public void assertNoNetworkStackRollbackCommitted() throws Exception { + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + assertNull(getUniqueRollbackInfoForPackage(rm.getRecentlyCommittedRollbacks(), + getNetworkStackPackageName())); + } + + private String getNetworkStackPackageName() { + Intent intent = new Intent(NETWORK_STACK_CONNECTOR_CLASS); + ComponentName comp = intent.resolveSystemService( + InstrumentationRegistry.getContext().getPackageManager(), 0); + return comp.getPackageName(); + } +} diff --git a/tests/RollbackTest/StagedRollbackTest.xml b/tests/RollbackTest/StagedRollbackTest.xml new file mode 100644 index 000000000000..2750d3765c20 --- /dev/null +++ b/tests/RollbackTest/StagedRollbackTest.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<configuration description="Runs the staged rollback tests"> + <option name="test-suite-tag" value="StagedRollbackTest" /> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="test-file-name" value="RollbackTest.apk" /> + </target_preparer> + <test class="com.android.tradefed.testtype.HostTest" > + <option name="class" value="com.android.tests.rollback.host.StagedRollbackTest" /> + </test> +</configuration> diff --git a/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java b/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java new file mode 100644 index 000000000000..bad294794337 --- /dev/null +++ b/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tests.rollback.host; + +import static org.junit.Assert.assertTrue; + +import com.android.ddmlib.Log.LogLevel; +import com.android.tradefed.log.LogUtil.CLog; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; +import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Runs the staged rollback tests. + */ +@RunWith(DeviceJUnit4ClassRunner.class) +public class StagedRollbackTest extends BaseHostJUnit4Test { + /** + * Runs the given phase of a test by calling into the device. + * Throws an exception if the test phase fails. + * <p> + * For example, <code>runPhase("testApkOnlyEnableRollback");</code> + */ + private void runPhase(String phase) throws Exception { + assertTrue(runDeviceTests("com.android.tests.rollback", + "com.android.tests.rollback.StagedRollbackTest", + phase)); + } + + @Before + public void setUp() throws Exception { + // Disconnect internet so we can test network health triggered rollbacks + getDevice().executeShellCommand("svc wifi disable"); + getDevice().executeShellCommand("svc data disable"); + } + + @After + public void tearDown() throws Exception { + // Reconnect internet after testing network health triggered rollbacks + getDevice().executeShellCommand("svc wifi enable"); + getDevice().executeShellCommand("svc data enable"); + } + + /** + * Tests watchdog triggered staged rollbacks involving only apks. + */ + @Test + public void testBadApkOnly() throws Exception { + runPhase("testBadApkOnlyEnableRollback"); + getDevice().reboot(); + runPhase("testBadApkOnlyConfirmEnableRollback"); + try { + // This is expected to fail due to the device being rebooted out + // from underneath the test. If this fails for reasons other than + // the device reboot, those failures should result in failure of + // the testApkOnlyConfirmRollback phase. + CLog.logAndDisplay(LogLevel.INFO, "testBadApkOnlyTriggerRollback is expected to fail"); + runPhase("testBadApkOnlyTriggerRollback"); + } catch (AssertionError e) { + // AssertionError is expected. + } + + getDevice().waitForDeviceAvailable(); + + runPhase("testBadApkOnlyConfirmRollback"); + } + + /** + * Tests failed network health check triggers watchdog staged rollbacks. + */ + @Test + public void testNetworkFailedRollback() throws Exception { + // Remove available rollbacks and uninstall NetworkStack on /data/ + runPhase("resetNetworkStack"); + // Reduce health check deadline + getDevice().executeShellCommand("device_config put rollback " + + "watchdog_request_timeout_millis 300000"); + // Simulate re-installation of new NetworkStack with rollbacks enabled + getDevice().executeShellCommand("pm install -r --staged --enable-rollback " + + "/system/priv-app/NetworkStack/NetworkStack.apk"); + + // Sleep to allow writes to disk before reboot + Thread.sleep(5000); + // Reboot device to activate staged package + getDevice().reboot(); + getDevice().waitForDeviceAvailable(); + + // Verify rollback was enabled + runPhase("assertNetworkStackRollbackAvailable"); + + // Sleep for < health check deadline + Thread.sleep(5000); + // Verify rollback was not executed before health check deadline + runPhase("assertNoNetworkStackRollbackCommitted"); + try { + // This is expected to fail due to the device being rebooted out + // from underneath the test. If this fails for reasons other than + // the device reboot, those failures should result in failure of + // the assertNetworkStackExecutedRollback phase. + CLog.logAndDisplay(LogLevel.INFO, "Sleep and expect to fail while sleeping"); + // Sleep for > health check deadline + Thread.sleep(260000); + } catch (AssertionError e) { + // AssertionError is expected. + } + + getDevice().waitForDeviceAvailable(); + // Verify rollback was executed after health check deadline + runPhase("assertNetworkStackRollbackCommitted"); + } + + /** + * Tests passed network health check does not trigger watchdog staged rollbacks. + */ + @Test + public void testNetworkPassedDoesNotRollback() throws Exception { + // Remove available rollbacks and uninstall NetworkStack on /data/ + runPhase("resetNetworkStack"); + // Reduce health check deadline, here unlike the network failed case, we use + // a longer deadline because joining a network can take a much longer time for + // reasons external to the device than 'not joining' + getDevice().executeShellCommand("device_config put rollback " + + "watchdog_request_timeout_millis 300000"); + // Simulate re-installation of new NetworkStack with rollbacks enabled + getDevice().executeShellCommand("pm install -r --staged --enable-rollback " + + "/system/priv-app/NetworkStack/NetworkStack.apk"); + + // Sleep to allow writes to disk before reboot + Thread.sleep(5000); + // Reboot device to activate staged package + getDevice().reboot(); + getDevice().waitForDeviceAvailable(); + + // Verify rollback was enabled + runPhase("assertNetworkStackRollbackAvailable"); + + // Connect to internet so network health check passes + getDevice().executeShellCommand("svc wifi enable"); + getDevice().executeShellCommand("svc data enable"); + + // Wait for device available because emulator device may restart after turning + // on mobile data + getDevice().waitForDeviceAvailable(); + + // Sleep for > health check deadline + Thread.sleep(310000); + // Verify rollback was not executed after health check deadline + runPhase("assertNoNetworkStackRollbackCommitted"); + } +} diff --git a/tests/RollbackTest/TEST_MAPPING b/tests/RollbackTest/TEST_MAPPING new file mode 100644 index 000000000000..6be93a0a199b --- /dev/null +++ b/tests/RollbackTest/TEST_MAPPING @@ -0,0 +1,10 @@ +{ + "presubmit": [ + { + "name": "RollbackTest" + }, + { + "name": "StagedRollbackTest" + } + ] +} diff --git a/tests/RollbackTest/TestApp/ACrashingV2.xml b/tests/RollbackTest/TestApp/ACrashingV2.xml new file mode 100644 index 000000000000..77bfd4e0f9a0 --- /dev/null +++ b/tests/RollbackTest/TestApp/ACrashingV2.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.tests.rollback.testapp.A" + android:versionCode="2" + android:versionName="2.0" > + + + <uses-sdk android:minSdkVersion="19" /> + + <application android:label="Rollback Test App A v2"> + <receiver android:name="com.android.tests.rollback.testapp.ProcessUserData" + android:exported="true" /> + <activity android:name="com.android.tests.rollback.testapp.CrashingMainActivity"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.DEFAULT"/> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/tests/RollbackTest/TestApp/Av1.xml b/tests/RollbackTest/TestApp/Av1.xml new file mode 100644 index 000000000000..63729fbaaf28 --- /dev/null +++ b/tests/RollbackTest/TestApp/Av1.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.tests.rollback.testapp.A" + android:versionCode="1" + android:versionName="1.0" > + + + <uses-sdk android:minSdkVersion="19" /> + + <application android:label="Rollback Test App A v1"> + <receiver android:name="com.android.tests.rollback.testapp.ProcessUserData" + android:exported="true" /> + <activity android:name="com.android.tests.rollback.testapp.MainActivity"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/tests/RollbackTest/TestApp/Av2.xml b/tests/RollbackTest/TestApp/Av2.xml new file mode 100644 index 000000000000..f0e909feabf3 --- /dev/null +++ b/tests/RollbackTest/TestApp/Av2.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.tests.rollback.testapp.A" + android:versionCode="2" + android:versionName="2.0" > + + + <uses-sdk android:minSdkVersion="19" /> + + <application android:label="Rollback Test App A v2"> + <receiver android:name="com.android.tests.rollback.testapp.ProcessUserData" + android:exported="true" /> + <activity android:name="com.android.tests.rollback.testapp.MainActivity"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/tests/RollbackTest/TestApp/Av3.xml b/tests/RollbackTest/TestApp/Av3.xml new file mode 100644 index 000000000000..9725c9f7cf9e --- /dev/null +++ b/tests/RollbackTest/TestApp/Av3.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.tests.rollback.testapp.A" + android:versionCode="3" + android:versionName="3.0" > + + + <uses-sdk android:minSdkVersion="19" /> + + <application android:label="Rollback Test App A v3"> + <receiver android:name="com.android.tests.rollback.testapp.ProcessUserData" + android:exported="true" /> + <activity android:name="com.android.tests.rollback.testapp.MainActivity"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/tests/RollbackTest/TestApp/Bv1.xml b/tests/RollbackTest/TestApp/Bv1.xml new file mode 100644 index 000000000000..ca9c2ec47a20 --- /dev/null +++ b/tests/RollbackTest/TestApp/Bv1.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.tests.rollback.testapp.B" + android:versionCode="1" + android:versionName="1.0" > + + + <uses-sdk android:minSdkVersion="19" /> + + <application android:label="Rollback Test App B v1"> + <receiver android:name="com.android.tests.rollback.testapp.ProcessUserData" + android:exported="true" /> + <activity android:name="com.android.tests.rollback.testapp.MainActivity"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/tests/RollbackTest/TestApp/Bv2.xml b/tests/RollbackTest/TestApp/Bv2.xml new file mode 100644 index 000000000000..bd3e6133f6f6 --- /dev/null +++ b/tests/RollbackTest/TestApp/Bv2.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.tests.rollback.testapp.B" + android:versionCode="2" + android:versionName="2.0" > + + + <uses-sdk android:minSdkVersion="19" /> + + <application android:label="Rollback Test App B v2"> + <receiver android:name="com.android.tests.rollback.testapp.ProcessUserData" + android:exported="true" /> + <activity android:name="com.android.tests.rollback.testapp.MainActivity"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/tests/RollbackTest/TestApp/res_v1/values-anydpi/values.xml b/tests/RollbackTest/TestApp/res_v1/values-anydpi/values.xml new file mode 100644 index 000000000000..90d3da2565cc --- /dev/null +++ b/tests/RollbackTest/TestApp/res_v1/values-anydpi/values.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + <integer name="split_version">1</integer> +</resources> diff --git a/tests/RollbackTest/TestApp/res_v1/values/values.xml b/tests/RollbackTest/TestApp/res_v1/values/values.xml new file mode 100644 index 000000000000..0447c74a79a6 --- /dev/null +++ b/tests/RollbackTest/TestApp/res_v1/values/values.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + <integer name="app_version">1</integer> + <integer name="split_version">0</integer> +</resources> diff --git a/tests/RollbackTest/TestApp/res_v2/values-anydpi/values.xml b/tests/RollbackTest/TestApp/res_v2/values-anydpi/values.xml new file mode 100644 index 000000000000..9a1aa7fd8461 --- /dev/null +++ b/tests/RollbackTest/TestApp/res_v2/values-anydpi/values.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + <integer name="split_version">2</integer> +</resources> diff --git a/tests/RollbackTest/TestApp/res_v2/values/values.xml b/tests/RollbackTest/TestApp/res_v2/values/values.xml new file mode 100644 index 000000000000..fd988f597f61 --- /dev/null +++ b/tests/RollbackTest/TestApp/res_v2/values/values.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + <integer name="app_version">2</integer> + <integer name="split_version">0</integer> +</resources> diff --git a/tests/RollbackTest/TestApp/res_v3/values-anydpi/values.xml b/tests/RollbackTest/TestApp/res_v3/values-anydpi/values.xml new file mode 100644 index 000000000000..f2d8992bee37 --- /dev/null +++ b/tests/RollbackTest/TestApp/res_v3/values-anydpi/values.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + <integer name="split_version">3</integer> +</resources> diff --git a/tests/RollbackTest/TestApp/res_v3/values/values.xml b/tests/RollbackTest/TestApp/res_v3/values/values.xml new file mode 100644 index 000000000000..968168a4bf06 --- /dev/null +++ b/tests/RollbackTest/TestApp/res_v3/values/values.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + <integer name="app_version">3</integer> + <integer name="split_version">0</integer> +</resources> diff --git a/tests/RollbackTest/TestApp/src/com/android/tests/rollback/testapp/CrashingMainActivity.java b/tests/RollbackTest/TestApp/src/com/android/tests/rollback/testapp/CrashingMainActivity.java new file mode 100644 index 000000000000..97958acde21b --- /dev/null +++ b/tests/RollbackTest/TestApp/src/com/android/tests/rollback/testapp/CrashingMainActivity.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tests.rollback.testapp; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; + +/** + * A crashing test app for testing apk rollback support. + */ +public class CrashingMainActivity extends Activity { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + incrementCountAndBroadcast(); + throw new RuntimeException("Intended force crash"); + } + + private void incrementCountAndBroadcast() { + SharedPreferences preferences = getSharedPreferences("prefs", Context.MODE_PRIVATE); + SharedPreferences.Editor editor = preferences.edit(); + int count = preferences.getInt("crash_count", 0); + editor.putInt("crash_count", ++count).commit(); + + Intent intent = new Intent("com.android.tests.rollback.CRASH"); + intent.putExtra("count", count); + sendBroadcast(intent); + } +} diff --git a/tests/RollbackTest/TestApp/src/com/android/tests/rollback/testapp/MainActivity.java b/tests/RollbackTest/TestApp/src/com/android/tests/rollback/testapp/MainActivity.java new file mode 100644 index 000000000000..9f1a0609d3f1 --- /dev/null +++ b/tests/RollbackTest/TestApp/src/com/android/tests/rollback/testapp/MainActivity.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tests.rollback.testapp; + +import android.app.Activity; +import android.os.Bundle; + +/** + * A test app for testing apk rollback support. + */ +public class MainActivity extends Activity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + try { + new ProcessUserData().processUserData(this); + } catch (ProcessUserData.UserDataException e) { + throw new AssertionError("Failed to process app user data", e); + } + } +} diff --git a/tests/RollbackTest/TestApp/src/com/android/tests/rollback/testapp/ProcessUserData.java b/tests/RollbackTest/TestApp/src/com/android/tests/rollback/testapp/ProcessUserData.java new file mode 100644 index 000000000000..38c658e795aa --- /dev/null +++ b/tests/RollbackTest/TestApp/src/com/android/tests/rollback/testapp/ProcessUserData.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tests.rollback.testapp; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Scanner; + +/** + * A broadcast reciever to check for and update user app data version + * compatibility. + */ +public class ProcessUserData extends BroadcastReceiver { + + private static final String TAG = "RollbackTestApp"; + + /** + * Exception thrown in case of issue with user data. + */ + public static class UserDataException extends Exception { + public UserDataException(String message) { + super(message); + } + + public UserDataException(String message, Throwable cause) { + super(message, cause); + } + } + + @Override + public void onReceive(Context context, Intent intent) { + try { + processUserData(context); + setResultCode(1); + } catch (UserDataException e) { + setResultCode(0); + setResultData(e.getMessage()); + } + } + + /** + * Update the app's user data version to match the app version. + * + * @param context The application context. + * @throws UserDataException in case of problems with app user data. + */ + public void processUserData(Context context) throws UserDataException { + Resources res = context.getResources(); + String packageName = context.getPackageName(); + + int appVersionId = res.getIdentifier("app_version", "integer", packageName); + int appVersion = res.getInteger(appVersionId); + + int splitVersionId = res.getIdentifier("split_version", "integer", packageName); + int splitVersion = res.getInteger(splitVersionId); + + // Make sure the app version and split versions are compatible. + if (appVersion != splitVersion) { + throw new UserDataException("Split version " + splitVersion + + " does not match app version " + appVersion); + } + + // Read the version of the app's user data and ensure it is compatible + // with our version of the application. + File versionFile = new File(context.getFilesDir(), "version.txt"); + try { + Scanner s = new Scanner(versionFile); + int userDataVersion = s.nextInt(); + s.close(); + + if (userDataVersion > appVersion) { + throw new UserDataException("User data is from version " + userDataVersion + + ", which is not compatible with this version " + appVersion + + " of the RollbackTestApp"); + } + } catch (FileNotFoundException e) { + // No problem. This is a fresh install of the app or the user data + // has been wiped. + } + + // Record the current version of the app in the user data. + try { + PrintWriter pw = new PrintWriter(versionFile); + pw.println(appVersion); + pw.close(); + } catch (IOException e) { + throw new UserDataException("Unable to write user data.", e); + } + } +} diff --git a/tests/ServiceCrashTest/Android.bp b/tests/ServiceCrashTest/Android.bp index b646ae7d33d3..40a377de852f 100644 --- a/tests/ServiceCrashTest/Android.bp +++ b/tests/ServiceCrashTest/Android.bp @@ -6,7 +6,7 @@ android_test { certificate: "platform", libs: ["android.test.base"], static_libs: [ - "compatibility-device-util", - "android-support-test", + "compatibility-device-util-axt", + "androidx.test.rules", ], } diff --git a/tests/SystemMemoryTest/README.txt b/tests/SystemMemoryTest/README.txt new file mode 100644 index 000000000000..8ffca1532117 --- /dev/null +++ b/tests/SystemMemoryTest/README.txt @@ -0,0 +1,20 @@ +This directory contains a test for system server memory use. + +Directory structure +=================== +device + - those parts of the test that run on device. + +host + - those parts of the test that run on host. + +Running the test +================ + +You can manually run the test as follows: + + atest -v system-memory-test + +This installs and runs the test on device. You can see the metrics in the +tradefed output. + diff --git a/tests/SystemMemoryTest/device/Android.bp b/tests/SystemMemoryTest/device/Android.bp new file mode 100644 index 000000000000..2bf0fec0fd1f --- /dev/null +++ b/tests/SystemMemoryTest/device/Android.bp @@ -0,0 +1,20 @@ +// Copyright (C) 2018 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +android_test_helper_app { + name: "SystemMemoryTestDevice", + sdk_version: "current", + srcs: ["src/**/*.java"], + test_suites: ["general-tests"], +} diff --git a/tests/SystemMemoryTest/device/AndroidManifest.xml b/tests/SystemMemoryTest/device/AndroidManifest.xml new file mode 100644 index 000000000000..e5e7844f9660 --- /dev/null +++ b/tests/SystemMemoryTest/device/AndroidManifest.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.tests.sysmem.device"> + + <uses-sdk android:minSdkVersion="19" /> + + <instrumentation + android:name="com.android.tests.sysmem.device.Cujs" + android:targetPackage="com.android.tests.sysmem.device" /> + + <application + android:allowBackup="false" + android:label="System Memory Test"> + </application> +</manifest> diff --git a/tests/SystemMemoryTest/device/src/com/android/tests/sysmem/device/Cujs.java b/tests/SystemMemoryTest/device/src/com/android/tests/sysmem/device/Cujs.java new file mode 100644 index 000000000000..6c0c5931ed8c --- /dev/null +++ b/tests/SystemMemoryTest/device/src/com/android/tests/sysmem/device/Cujs.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tests.sysmem.device; + +import android.app.Activity; +import android.app.Instrumentation; +import android.os.Bundle; +import android.util.Log; + +/** + * Critical user journeys used to exercise the system for test, driven from + * the device. + */ +public class Cujs extends Instrumentation { + + private static final String TAG = "SystemMemoryTest"; + + @Override + public void onCreate(Bundle arguments) { + start(); + } + + @Override + public void onStart() { + // TODO: Exercise the system in more interesting ways. + // Mostly what matters is that whatever we do here is sustainable: it + // can be repeated indefinitely on the system without causing + // problems. For example, launching activities is fine. Installing + // applications is only fine if we also uninstall them as part of the + // test. + try { + Log.i(TAG, "Sleeping for 10 seconds..."); + Thread.sleep(10 * 1000); + } catch (InterruptedException ignored) { + } + + finish(Activity.RESULT_OK, null); + } +} diff --git a/tests/SystemMemoryTest/host/Android.bp b/tests/SystemMemoryTest/host/Android.bp new file mode 100644 index 000000000000..3bb5489dab6c --- /dev/null +++ b/tests/SystemMemoryTest/host/Android.bp @@ -0,0 +1,20 @@ +// Copyright (C) 2018 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +java_test_host { + name: "system-memory-test", + srcs: ["src/**/*.java"], + libs: ["tradefed"], + test_suites: ["general-tests"], +} diff --git a/tests/SystemMemoryTest/host/AndroidTest.xml b/tests/SystemMemoryTest/host/AndroidTest.xml new file mode 100644 index 000000000000..6d2c95f3094a --- /dev/null +++ b/tests/SystemMemoryTest/host/AndroidTest.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<configuration description="Runs the system memory tests"> + <option name="test-suite-tag" value="system-memory-test" /> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="test-file-name" value="SystemMemoryTestDevice.apk" /> + </target_preparer> + <test class="com.android.tradefed.testtype.HostTest" > + <option name="class" value="com.android.tests.sysmem.host.MemoryTest" /> + </test> +</configuration> diff --git a/tests/SystemMemoryTest/host/src/com/android/tests/sysmem/host/Cujs.java b/tests/SystemMemoryTest/host/src/com/android/tests/sysmem/host/Cujs.java new file mode 100644 index 000000000000..18cdf96c7131 --- /dev/null +++ b/tests/SystemMemoryTest/host/src/com/android/tests/sysmem/host/Cujs.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tests.sysmem.host; + +/** + * Critical user journeys with which to exercise the system, driven from the + * host. + */ +public class Cujs { + private Device mDevice; + + public Cujs(Device device) { + this.mDevice = device; + } + + /** + * Runs the critical user journeys. + */ + public void run() throws TestException { + // Do an explicit GC in the system server process as part of the test + // case to reduce GC-related sources of noise. + // SIGUSR1 = 10 is the magic signal to trigger the GC. + int pid = mDevice.getProcessPid("system_server"); + mDevice.executeShellCommand("kill -10 " + pid); + + // Invoke the Device Cujs instrumentation to run the cujs. + // TODO: Consider exercising the system in other interesting ways as + // well. + String command = "am instrument -w com.android.tests.sysmem.device/.Cujs"; + mDevice.executeShellCommand(command); + } +} diff --git a/tests/SystemMemoryTest/host/src/com/android/tests/sysmem/host/Device.java b/tests/SystemMemoryTest/host/src/com/android/tests/sysmem/host/Device.java new file mode 100644 index 000000000000..26146ca0ea6c --- /dev/null +++ b/tests/SystemMemoryTest/host/src/com/android/tests/sysmem/host/Device.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tests.sysmem.host; + +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.device.ITestDevice; + +/** + * Wrapper around ITestDevice exposing useful device functions. + */ +class Device { + + private ITestDevice mDevice; + + Device(ITestDevice device) { + mDevice = device; + } + + /** + * Execute a shell command and return the output as a string. + */ + public String executeShellCommand(String command) throws TestException { + try { + return mDevice.executeShellCommand(command); + } catch (DeviceNotAvailableException e) { + throw new TestException(e); + } + } + + /** + * Enable adb root + */ + public void enableAdbRoot() throws TestException { + try { + mDevice.enableAdbRoot(); + } catch (DeviceNotAvailableException e) { + throw new TestException(e); + } + } + + /** + * Returns the pid for the process with the given name. + */ + public int getProcessPid(String name) throws TestException { + try { + String pid = mDevice.getProcessPid(name); + if (pid == null) { + throw new TestException("failed to get pid for " + name); + } + return Integer.parseInt(pid); + } catch (DeviceNotAvailableException e) { + throw new TestException(e); + } + } +} diff --git a/tests/SystemMemoryTest/host/src/com/android/tests/sysmem/host/MemoryTest.java b/tests/SystemMemoryTest/host/src/com/android/tests/sysmem/host/MemoryTest.java new file mode 100644 index 000000000000..48b7f393d0a3 --- /dev/null +++ b/tests/SystemMemoryTest/host/src/com/android/tests/sysmem/host/MemoryTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tests.sysmem.host; + +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestMetrics; +import com.android.tradefed.testtype.IDeviceTest; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Runs a system memory test. + */ +@RunWith(DeviceJUnit4ClassRunner.class) +public class MemoryTest implements IDeviceTest { + + @Rule public TestMetrics testMetrics = new TestMetrics(); + @Rule public TestLogData testLogs = new TestLogData(); + + private ITestDevice mTestDevice; + private int mIterations = 0; // Number of times cujs have been run. + private Metrics mMetrics; + private Cujs mCujs; + + @Override + public void setDevice(ITestDevice testDevice) { + mTestDevice = testDevice; + Device device = new Device(testDevice); + mMetrics = new Metrics(device, testMetrics, testLogs); + mCujs = new Cujs(device); + } + + @Override + public ITestDevice getDevice() { + return mTestDevice; + } + + // Invoke a single iteration of running the cujs. + private void runCujs() throws TestException { + mCujs.run(); + mIterations++; + } + + // Sample desired memory. + private void sample() throws TestException { + mMetrics.sample(String.format("%03d", mIterations)); + } + + /** + * Runs the memory tests. + */ + @Test + public void run() throws TestException { + sample(); // Sample before running cujs + runCujs(); + sample(); // Sample after first iteration of cujs + + // Run cujs in a loop to highlight memory leaks. + for (int i = 0; i < 5; ++i) { + for (int j = 0; j < 5; j++) { + runCujs(); + } + sample(); + } + } +} diff --git a/tests/SystemMemoryTest/host/src/com/android/tests/sysmem/host/Metrics.java b/tests/SystemMemoryTest/host/src/com/android/tests/sysmem/host/Metrics.java new file mode 100644 index 000000000000..b46e642b5e92 --- /dev/null +++ b/tests/SystemMemoryTest/host/src/com/android/tests/sysmem/host/Metrics.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tests.sysmem.host; + +import com.android.tradefed.result.FileInputStreamSource; +import com.android.tradefed.result.LogDataType; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestMetrics; + +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.util.InputMismatchException; +import java.util.Scanner; + +/** + * Utilities for sampling and reporting memory metrics. + */ +class Metrics { + + private Device mDevice; + private TestMetrics mMetrics; + private TestLogData mLogs; + + /** + * Constructs a metrics instance that will output high level metrics and + * more detailed breakdowns using the given <code>metrics</code> and + * <code>logs</code> objects. + * + * @param device the device to sample metrics from + * @param metrics where to log the high level metrics when taking a sample + * @param logs where to log detailed breakdowns when taking a sample + */ + Metrics(Device device, TestMetrics metrics, TestLogData logs) { + this.mDevice = device; + this.mMetrics = metrics; + this.mLogs = logs; + } + + /** + * Writes the given <code>text</code> to a log with the given label. + */ + private void logText(String label, String text) throws TestException { + try { + File file = File.createTempFile(label, "txt"); + PrintStream ps = new PrintStream(file); + ps.print(text); + try (FileInputStreamSource dataStream = new FileInputStreamSource(file)) { + mLogs.addTestLog(label, LogDataType.TEXT, dataStream); + } + } catch (IOException e) { + throw new TestException(e); + } + } + + /** + * Samples the current memory use on the system. Outputs high level test + * metrics and detailed breakdowns to the TestMetrics and TestLogData + * objects provided when constructing this Metrics instance. The metrics + * and log names are prefixed with the given label. + * + * @param label prefix to use for metrics and logs output for this sample. + */ + void sample(String label) throws TestException { + // adb root access is required to get showmap + mDevice.enableAdbRoot(); + + int pid = mDevice.getProcessPid("system_server"); + + // Read showmap for system server and add it as a test log + String showmap = mDevice.executeShellCommand("showmap " + pid); + logText(label + ".system_server.showmap", showmap); + + // Extract VSS, PSS and RSS from the showmap and output them as metrics. + // The last lines of the showmap output looks something like: + // CHECKSTYLE:OFF Generated code + // virtual shared shared private private + // size RSS PSS clean dirty clean dirty swap swapPSS # object + //-------- -------- -------- -------- -------- -------- -------- -------- -------- ---- ------------------------------ + // 928480 113016 24860 87348 7916 3632 14120 1968 1968 1900 TOTAL + // CHECKSTYLE:ON Generated code + try { + int pos = showmap.lastIndexOf("----"); + Scanner sc = new Scanner(showmap.substring(pos)); + sc.next(); + long vss = sc.nextLong(); + long rss = sc.nextLong(); + long pss = sc.nextLong(); + + mMetrics.addTestMetric(label + ".system_server.vss", Long.toString(vss)); + mMetrics.addTestMetric(label + ".system_server.rss", Long.toString(rss)); + mMetrics.addTestMetric(label + ".system_server.pss", Long.toString(pss)); + } catch (InputMismatchException e) { + throw new TestException("unexpected showmap format", e); + } + + // Run debuggerd -j to get GC stats for system server and add it as a + // test log + String debuggerd = mDevice.executeShellCommand("debuggerd -j " + pid); + logText(label + ".system_server.debuggerd", debuggerd); + + // TODO: Experiment with other additional metrics. + + // TODO: Consider launching an instrumentation to collect metrics from + // within the device itself. + } +} diff --git a/tests/SystemMemoryTest/host/src/com/android/tests/sysmem/host/TestException.java b/tests/SystemMemoryTest/host/src/com/android/tests/sysmem/host/TestException.java new file mode 100644 index 000000000000..dc3b68f4b455 --- /dev/null +++ b/tests/SystemMemoryTest/host/src/com/android/tests/sysmem/host/TestException.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tests.sysmem.host; + +/** + * Exception thrown in case of unexpected error encountered when executing the + * test. + */ +class TestException extends Exception { + TestException(Exception cause) { + super(cause); + } + + TestException(String msg) { + super(msg); + } + + TestException(String msg, Exception cause) { + super(msg, cause); + } +} diff --git a/tests/TouchLatency/app/build.gradle b/tests/TouchLatency/app/build.gradle index 2594322e3727..04a878896f47 100644 --- a/tests/TouchLatency/app/build.gradle +++ b/tests/TouchLatency/app/build.gradle @@ -6,7 +6,7 @@ android { defaultConfig { applicationId "com.prefabulated.touchlatency" - minSdkVersion 21 + minSdkVersion 28 targetSdkVersion 28 versionCode 1 versionName "1.0" diff --git a/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivity.java b/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivity.java index 360c22f832b3..ba77a74974d1 100644 --- a/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivity.java +++ b/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivity.java @@ -24,11 +24,15 @@ import android.graphics.Paint.Align; import android.os.Bundle; import android.util.AttributeSet; import android.util.Log; +import android.view.Display; +import android.view.Display.Mode; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.os.Trace; +import android.view.Window; +import android.view.WindowManager; import java.math.RoundingMode; import java.text.DecimalFormat; @@ -219,14 +223,30 @@ class TouchLatencyView extends View implements View.OnTouchListener { } public class TouchLatencyActivity extends Activity { + private Mode mDisplayModes[]; + private int mCurrentModeIndex; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + Trace.beginSection("TouchLatencyActivity onCreate"); setContentView(R.layout.activity_touch_latency); mTouchView = findViewById(R.id.canvasView); + + WindowManager wm = getWindowManager(); + Display display = wm.getDefaultDisplay(); + mDisplayModes = display.getSupportedModes(); + Mode currentMode = getWindowManager().getDefaultDisplay().getMode(); + + for (int i = 0; i < mDisplayModes.length; i++) { + if (currentMode.getModeId() == mDisplayModes[i].getModeId()) { + mCurrentModeIndex = i; + break; + } + } + Trace.endSection(); } @@ -236,10 +256,35 @@ public class TouchLatencyActivity extends Activity { Trace.beginSection("TouchLatencyActivity onCreateOptionsMenu"); // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_touch_latency, menu); + if (mDisplayModes.length > 1) { + MenuItem menuItem = menu.findItem(R.id.display_mode); + Mode currentMode = getWindowManager().getDefaultDisplay().getMode(); + updateDisplayMode(menuItem, currentMode); + } Trace.endSection(); return true; } + + private void updateDisplayMode(MenuItem menuItem, Mode displayMode) { + int fps = (int) displayMode.getRefreshRate(); + menuItem.setTitle(fps + "hz"); + menuItem.setVisible(true); + } + + public void changeDisplayMode(MenuItem item) { + Window w = getWindow(); + WindowManager.LayoutParams params = w.getAttributes(); + + int modeIndex = (mCurrentModeIndex + 1) % mDisplayModes.length; + params.preferredDisplayModeId = mDisplayModes[modeIndex].getModeId(); + w.setAttributes(params); + + updateDisplayMode(item, mDisplayModes[modeIndex]); + mCurrentModeIndex = modeIndex; + } + + @Override public boolean onOptionsItemSelected(MenuItem item) { Trace.beginSection("TouchLatencyActivity onOptionsItemSelected"); @@ -251,6 +296,8 @@ public class TouchLatencyActivity extends Activity { //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { mTouchView.changeMode(item); + } else if (id == R.id.display_mode) { + changeDisplayMode(item); } Trace.endSection(); diff --git a/tests/TouchLatency/app/src/main/res/layout/activity_touch_latency.xml b/tests/TouchLatency/app/src/main/res/layout/activity_touch_latency.xml index 8d20ff24bfe5..625757610154 100644 --- a/tests/TouchLatency/app/src/main/res/layout/activity_touch_latency.xml +++ b/tests/TouchLatency/app/src/main/res/layout/activity_touch_latency.xml @@ -18,6 +18,7 @@ android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" + android:keepScreenOn="true" android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".TouchLatencyActivity"> <com.prefabulated.touchlatency.TouchLatencyView diff --git a/tests/TouchLatency/app/src/main/res/menu/menu_touch_latency.xml b/tests/TouchLatency/app/src/main/res/menu/menu_touch_latency.xml index 5aef72e8d383..52be91900ae8 100644 --- a/tests/TouchLatency/app/src/main/res/menu/menu_touch_latency.xml +++ b/tests/TouchLatency/app/src/main/res/menu/menu_touch_latency.xml @@ -15,6 +15,14 @@ --> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" tools:context=".TouchLatencyActivity"> - <item android:id="@+id/action_settings" android:title="@string/mode" - android:orderInCategory="100" android:showAsAction="always" /> + <item + android:id="@+id/action_settings" + android:orderInCategory="101" + android:showAsAction="always" + android:title="@string/mode"/> + <item + android:id="@+id/display_mode" + android:showAsAction="ifRoom" + android:title="@string/display_mode" + android:visible="false"/> </menu> diff --git a/tests/TouchLatency/app/src/main/res/values/strings.xml b/tests/TouchLatency/app/src/main/res/values/strings.xml index b97f095d501e..771992c8e5d3 100644 --- a/tests/TouchLatency/app/src/main/res/values/strings.xml +++ b/tests/TouchLatency/app/src/main/res/values/strings.xml @@ -17,4 +17,5 @@ <string name="app_name">Touch Latency</string> <string name="mode">Touch</string> + <string name="display_mode">Mode</string> </resources> diff --git a/tests/UiBench/Android.bp b/tests/UiBench/Android.bp index af17b97f2ea7..e0608e288459 100644 --- a/tests/UiBench/Android.bp +++ b/tests/UiBench/Android.bp @@ -8,12 +8,12 @@ android_test { // regressions are reflected in test data resource_dirs: ["res"], static_libs: [ - "android-support-design", - "android-support-v4", - "android-support-v7-appcompat", - "android-support-v7-cardview", - "android-support-v7-recyclerview", - "android-support-v17-leanback", + "com.google.android.material_material", + "androidx.legacy_legacy-support-v4", + "androidx.appcompat_appcompat", + "androidx.cardview_cardview", + "androidx.recyclerview_recyclerview", + "androidx.leanback_leanback", ], test_suites: ["device-tests"], } diff --git a/tests/UiBench/res/layout/activity_bitmap_upload.xml b/tests/UiBench/res/layout/activity_bitmap_upload.xml index 70faa07a6d75..9a8293437c21 100644 --- a/tests/UiBench/res/layout/activity_bitmap_upload.xml +++ b/tests/UiBench/res/layout/activity_bitmap_upload.xml @@ -22,7 +22,7 @@ android:layout_height="match_parent" android:padding="10dp" android:clipToPadding="false"> - <android.support.v7.widget.CardView + <androidx.cardview.widget.CardView android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"> @@ -30,22 +30,22 @@ android:id="@+id/upload_view" android:layout_width="match_parent" android:layout_height="match_parent"/> - </android.support.v7.widget.CardView> + </androidx.cardview.widget.CardView> - <android.support.v4.widget.Space + <androidx.legacy.widget.Space android:layout_height="10dp" android:layout_width="match_parent" /> - <android.support.v7.widget.CardView + <androidx.cardview.widget.CardView android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> - <android.support.v4.widget.Space + <androidx.legacy.widget.Space android:layout_height="10dp" android:layout_width="match_parent" /> - <android.support.v7.widget.CardView + <androidx.cardview.widget.CardView android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> diff --git a/tests/UiBench/res/layout/activity_navigation_drawer.xml b/tests/UiBench/res/layout/activity_navigation_drawer.xml index 8d4bfafd8dd8..383432acf152 100644 --- a/tests/UiBench/res/layout/activity_navigation_drawer.xml +++ b/tests/UiBench/res/layout/activity_navigation_drawer.xml @@ -14,7 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> -<android.support.v4.widget.DrawerLayout +<androidx.drawerlayout.widget.DrawerLayout android:id="@+id/drawer_layout" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" @@ -27,7 +27,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"/> - <android.support.design.widget.NavigationView + <com.google.android.material.navigation.NavigationView android:id="@+id/nav_view" android:layout_width="wrap_content" android:layout_height="match_parent" @@ -36,4 +36,4 @@ app:headerLayout="@layout/nav_header_navigation_drawer" app:menu="@menu/activity_navigation_drawer_drawer"/> -</android.support.v4.widget.DrawerLayout> +</androidx.drawerlayout.widget.DrawerLayout> diff --git a/tests/UiBench/res/layout/app_bar_navigation_drawer.xml b/tests/UiBench/res/layout/app_bar_navigation_drawer.xml index 5657587f7772..cf209aca9abd 100644 --- a/tests/UiBench/res/layout/app_bar_navigation_drawer.xml +++ b/tests/UiBench/res/layout/app_bar_navigation_drawer.xml @@ -14,23 +14,23 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> -<android.support.design.widget.CoordinatorLayout +<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/app_bar_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true"> - <android.support.design.widget.AppBarLayout + <com.google.android.material.appbar.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> - <android.support.v7.widget.Toolbar + <androidx.appcompat.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary"/> - </android.support.design.widget.AppBarLayout> + </com.google.android.material.appbar.AppBarLayout> -</android.support.design.widget.CoordinatorLayout> +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/tests/UiBench/res/layout/card_row.xml b/tests/UiBench/res/layout/card_row.xml index 215f9df9b7fd..9ebff8e2d6ca 100644 --- a/tests/UiBench/res/layout/card_row.xml +++ b/tests/UiBench/res/layout/card_row.xml @@ -24,7 +24,7 @@ android:paddingBottom="5dp" android:clipToPadding="false" android:background="@null"> - <android.support.v7.widget.CardView + <androidx.cardview.widget.CardView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1"> @@ -32,13 +32,13 @@ android:id="@+id/card_text" android:layout_width="match_parent" android:layout_height="match_parent"/> - </android.support.v7.widget.CardView> + </androidx.cardview.widget.CardView> - <android.support.v4.widget.Space + <androidx.legacy.widget.Space android:layout_height="match_parent" android:layout_width="10dp" /> - <android.support.v7.widget.CardView + <androidx.cardview.widget.CardView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" /> diff --git a/tests/UiBench/res/layout/recycler_view.xml b/tests/UiBench/res/layout/recycler_view.xml index 54c5b5845ae2..53eab68f7866 100644 --- a/tests/UiBench/res/layout/recycler_view.xml +++ b/tests/UiBench/res/layout/recycler_view.xml @@ -14,7 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> -<android.support.v7.widget.RecyclerView +<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/recyclerView" android:layout_width="match_parent" diff --git a/tests/UiBench/src/com/android/test/uibench/ActivityTransition.java b/tests/UiBench/src/com/android/test/uibench/ActivityTransition.java index c212c4cbf61a..50f145a1b171 100644 --- a/tests/UiBench/src/com/android/test/uibench/ActivityTransition.java +++ b/tests/UiBench/src/com/android/test/uibench/ActivityTransition.java @@ -22,7 +22,7 @@ import android.content.res.Configuration; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatActivity; import android.view.View; import android.widget.GridLayout; import android.widget.ImageView; diff --git a/tests/UiBench/src/com/android/test/uibench/ActivityTransitionDetails.java b/tests/UiBench/src/com/android/test/uibench/ActivityTransitionDetails.java index a4d57e173bfa..0f73765e87e7 100644 --- a/tests/UiBench/src/com/android/test/uibench/ActivityTransitionDetails.java +++ b/tests/UiBench/src/com/android/test/uibench/ActivityTransitionDetails.java @@ -21,7 +21,7 @@ import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatActivity; import android.view.View; import android.widget.ImageView; diff --git a/tests/UiBench/src/com/android/test/uibench/BitmapUploadActivity.java b/tests/UiBench/src/com/android/test/uibench/BitmapUploadActivity.java index 235e0e61b353..09236ffebdf4 100644 --- a/tests/UiBench/src/com/android/test/uibench/BitmapUploadActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/BitmapUploadActivity.java @@ -23,7 +23,7 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Rect; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatActivity; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.View; diff --git a/tests/UiBench/src/com/android/test/uibench/ClippedListActivity.java b/tests/UiBench/src/com/android/test/uibench/ClippedListActivity.java index fee7480fa65e..2bf6040351b8 100644 --- a/tests/UiBench/src/com/android/test/uibench/ClippedListActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/ClippedListActivity.java @@ -16,18 +16,20 @@ package com.android.test.uibench; import android.os.Bundle; -import android.support.design.widget.NavigationView; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.ListFragment; -import android.support.v4.view.GravityCompat; -import android.support.v4.widget.DrawerLayout; -import android.support.v7.app.ActionBarDrawerToggle; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; import android.view.MenuItem; import android.widget.ArrayAdapter; import android.widget.ListAdapter; +import androidx.appcompat.app.ActionBarDrawerToggle; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.view.GravityCompat; +import androidx.drawerlayout.widget.DrawerLayout; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.ListFragment; + +import com.google.android.material.navigation.NavigationView; + public class ClippedListActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener { diff --git a/tests/UiBench/src/com/android/test/uibench/DialogListActivity.java b/tests/UiBench/src/com/android/test/uibench/DialogListActivity.java index fe712d5230bb..c87b49b9f66f 100644 --- a/tests/UiBench/src/com/android/test/uibench/DialogListActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/DialogListActivity.java @@ -16,8 +16,8 @@ package com.android.test.uibench; import android.os.Bundle; -import android.support.v7.app.AlertDialog; -import android.support.v7.app.AppCompatActivity; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; import android.widget.ArrayAdapter; import android.widget.ListView; diff --git a/tests/UiBench/src/com/android/test/uibench/EditTextTypeActivity.java b/tests/UiBench/src/com/android/test/uibench/EditTextTypeActivity.java index 08ab5105a5e8..1b2c3c60ffd4 100644 --- a/tests/UiBench/src/com/android/test/uibench/EditTextTypeActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/EditTextTypeActivity.java @@ -19,7 +19,7 @@ import android.app.Instrumentation; import android.os.Bundle; import android.os.Looper; import android.os.MessageQueue; -import android.support.v7.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatActivity; import android.view.KeyEvent; import android.widget.EditText; diff --git a/tests/UiBench/src/com/android/test/uibench/FadingEdgeListActivity.java b/tests/UiBench/src/com/android/test/uibench/FadingEdgeListActivity.java index 3241e669fb1d..f8e9f03ee599 100644 --- a/tests/UiBench/src/com/android/test/uibench/FadingEdgeListActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/FadingEdgeListActivity.java @@ -15,7 +15,7 @@ */ package com.android.test.uibench; -import android.support.v4.app.ListFragment; +import androidx.fragment.app.ListFragment; import android.widget.ArrayAdapter; import android.widget.ListAdapter; diff --git a/tests/UiBench/src/com/android/test/uibench/FullscreenOverdrawActivity.java b/tests/UiBench/src/com/android/test/uibench/FullscreenOverdrawActivity.java index b9c5fac96a3e..882163bd6b0e 100644 --- a/tests/UiBench/src/com/android/test/uibench/FullscreenOverdrawActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/FullscreenOverdrawActivity.java @@ -25,7 +25,7 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatActivity; import android.view.View; /** diff --git a/tests/UiBench/src/com/android/test/uibench/GlTextureViewActivity.java b/tests/UiBench/src/com/android/test/uibench/GlTextureViewActivity.java index 36d703c8e1a2..b26a660981da 100644 --- a/tests/UiBench/src/com/android/test/uibench/GlTextureViewActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/GlTextureViewActivity.java @@ -20,7 +20,7 @@ import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.graphics.SurfaceTexture; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatActivity; import android.util.DisplayMetrics; import android.util.Log; import android.view.Gravity; diff --git a/tests/UiBench/src/com/android/test/uibench/InvalidateActivity.java b/tests/UiBench/src/com/android/test/uibench/InvalidateActivity.java index 81d03ffee0ed..76ed1ae4e445 100644 --- a/tests/UiBench/src/com/android/test/uibench/InvalidateActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/InvalidateActivity.java @@ -21,8 +21,8 @@ import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.os.Bundle; -import android.support.annotation.ColorInt; -import android.support.v7.app.AppCompatActivity; +import androidx.annotation.ColorInt; +import androidx.appcompat.app.AppCompatActivity; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; diff --git a/tests/UiBench/src/com/android/test/uibench/InvalidateTreeActivity.java b/tests/UiBench/src/com/android/test/uibench/InvalidateTreeActivity.java index b800c26bfc60..804ced14d522 100644 --- a/tests/UiBench/src/com/android/test/uibench/InvalidateTreeActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/InvalidateTreeActivity.java @@ -19,7 +19,7 @@ import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.graphics.Color; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatActivity; import android.view.ViewGroup; import android.widget.LinearLayout; diff --git a/tests/UiBench/src/com/android/test/uibench/MainActivity.java b/tests/UiBench/src/com/android/test/uibench/MainActivity.java index 79837b6ea250..0a7aa4281b00 100644 --- a/tests/UiBench/src/com/android/test/uibench/MainActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/MainActivity.java @@ -19,9 +19,9 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Bundle; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.ListFragment; -import android.support.v7.app.AppCompatActivity; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.ListFragment; +import androidx.appcompat.app.AppCompatActivity; import android.view.View; import android.widget.ListView; import android.widget.SimpleAdapter; diff --git a/tests/UiBench/src/com/android/test/uibench/NavigationDrawerActivity.java b/tests/UiBench/src/com/android/test/uibench/NavigationDrawerActivity.java index a54110454f7a..77845391cb47 100644 --- a/tests/UiBench/src/com/android/test/uibench/NavigationDrawerActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/NavigationDrawerActivity.java @@ -16,14 +16,16 @@ package com.android.test.uibench; import android.os.Bundle; -import android.support.design.widget.NavigationView; -import android.support.v4.view.GravityCompat; -import android.support.v4.widget.DrawerLayout; -import android.support.v7.app.ActionBarDrawerToggle; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; import android.view.MenuItem; +import androidx.appcompat.app.ActionBarDrawerToggle; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.view.GravityCompat; +import androidx.drawerlayout.widget.DrawerLayout; + +import com.google.android.material.navigation.NavigationView; + public class NavigationDrawerActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener { diff --git a/tests/UiBench/src/com/android/test/uibench/NotificationShadeActivity.java b/tests/UiBench/src/com/android/test/uibench/NotificationShadeActivity.java index 70deeead6cf1..1ab90fe97c87 100644 --- a/tests/UiBench/src/com/android/test/uibench/NotificationShadeActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/NotificationShadeActivity.java @@ -18,8 +18,8 @@ package com.android.test.uibench; import android.content.Context; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v7.app.AppCompatActivity; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; import android.view.GestureDetector; import android.view.LayoutInflater; import android.view.MotionEvent; diff --git a/tests/UiBench/src/com/android/test/uibench/ResizeHWLayerActivity.java b/tests/UiBench/src/com/android/test/uibench/ResizeHWLayerActivity.java index 23a2713abd40..80d495df142c 100644 --- a/tests/UiBench/src/com/android/test/uibench/ResizeHWLayerActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/ResizeHWLayerActivity.java @@ -19,7 +19,7 @@ import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; import android.graphics.Color; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatActivity; import android.util.DisplayMetrics; import android.view.View; import android.view.ViewGroup.LayoutParams; diff --git a/tests/UiBench/src/com/android/test/uibench/SaveLayerInterleaveActivity.java b/tests/UiBench/src/com/android/test/uibench/SaveLayerInterleaveActivity.java index eec91cb38066..332240d7639b 100644 --- a/tests/UiBench/src/com/android/test/uibench/SaveLayerInterleaveActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/SaveLayerInterleaveActivity.java @@ -23,7 +23,7 @@ import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatActivity; /** * Test Canvas.saveLayer performance by interleaving drawText/drawRect with saveLayer. diff --git a/tests/UiBench/src/com/android/test/uibench/ScrollableWebViewActivity.java b/tests/UiBench/src/com/android/test/uibench/ScrollableWebViewActivity.java index 02c2a0898cdd..e754c3ab5726 100644 --- a/tests/UiBench/src/com/android/test/uibench/ScrollableWebViewActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/ScrollableWebViewActivity.java @@ -16,7 +16,7 @@ package com.android.test.uibench; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatActivity; import android.webkit.WebView; public class ScrollableWebViewActivity extends AppCompatActivity { diff --git a/tests/UiBench/src/com/android/test/uibench/ShadowGridActivity.java b/tests/UiBench/src/com/android/test/uibench/ShadowGridActivity.java index 88847eed17fa..af7c65acafd4 100644 --- a/tests/UiBench/src/com/android/test/uibench/ShadowGridActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/ShadowGridActivity.java @@ -16,9 +16,9 @@ package com.android.test.uibench; import android.os.Bundle; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.ListFragment; -import android.support.v7.app.AppCompatActivity; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.ListFragment; +import androidx.appcompat.app.AppCompatActivity; import android.view.View; import android.widget.ArrayAdapter; diff --git a/tests/UiBench/src/com/android/test/uibench/SlowBindRecyclerViewActivity.java b/tests/UiBench/src/com/android/test/uibench/SlowBindRecyclerViewActivity.java index e32862f7b7fd..14e59ae46917 100644 --- a/tests/UiBench/src/com/android/test/uibench/SlowBindRecyclerViewActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/SlowBindRecyclerViewActivity.java @@ -17,8 +17,8 @@ package com.android.test.uibench; import android.content.Context; import android.os.Trace; -import android.support.v7.widget.GridLayoutManager; -import android.support.v7.widget.RecyclerView; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import com.android.test.uibench.recyclerview.RvBoxAdapter; import com.android.test.uibench.recyclerview.RvCompatListActivity; diff --git a/tests/UiBench/src/com/android/test/uibench/SlowNestedRecyclerViewActivity.java b/tests/UiBench/src/com/android/test/uibench/SlowNestedRecyclerViewActivity.java index 305c05104f95..8fdd4b90c23e 100644 --- a/tests/UiBench/src/com/android/test/uibench/SlowNestedRecyclerViewActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/SlowNestedRecyclerViewActivity.java @@ -20,8 +20,8 @@ import android.content.res.Resources; import android.graphics.Color; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.InsetDrawable; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import android.view.View; import android.view.ViewGroup; diff --git a/tests/UiBench/src/com/android/test/uibench/TrivialAnimationActivity.java b/tests/UiBench/src/com/android/test/uibench/TrivialAnimationActivity.java index 6e984727836e..3ee132efbf50 100644 --- a/tests/UiBench/src/com/android/test/uibench/TrivialAnimationActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/TrivialAnimationActivity.java @@ -19,7 +19,7 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatActivity; public class TrivialAnimationActivity extends AppCompatActivity { @Override diff --git a/tests/UiBench/src/com/android/test/uibench/TrivialRecyclerViewActivity.java b/tests/UiBench/src/com/android/test/uibench/TrivialRecyclerViewActivity.java index 4647ba774b2e..b30055810465 100644 --- a/tests/UiBench/src/com/android/test/uibench/TrivialRecyclerViewActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/TrivialRecyclerViewActivity.java @@ -15,7 +15,7 @@ */ package com.android.test.uibench; -import android.support.v7.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView; import com.android.test.uibench.recyclerview.RvArrayAdapter; import com.android.test.uibench.recyclerview.RvCompatListActivity; diff --git a/tests/UiBench/src/com/android/test/uibench/leanback/BitmapLoader.java b/tests/UiBench/src/com/android/test/uibench/leanback/BitmapLoader.java index 8af9d3bca647..ca379daedc1d 100644 --- a/tests/UiBench/src/com/android/test/uibench/leanback/BitmapLoader.java +++ b/tests/UiBench/src/com/android/test/uibench/leanback/BitmapLoader.java @@ -21,7 +21,7 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.os.AsyncTask; -import android.support.v4.util.LruCache; +import androidx.collection.LruCache; import android.util.DisplayMetrics; import android.widget.ImageView; diff --git a/tests/UiBench/src/com/android/test/uibench/leanback/BrowseActivity.java b/tests/UiBench/src/com/android/test/uibench/leanback/BrowseActivity.java index d29f0eaf6fdc..804a2f3b9dc8 100644 --- a/tests/UiBench/src/com/android/test/uibench/leanback/BrowseActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/leanback/BrowseActivity.java @@ -13,7 +13,7 @@ */ package com.android.test.uibench.leanback; -import android.support.v4.app.FragmentActivity; +import androidx.fragment.app.FragmentActivity; import android.app.Activity; import android.os.Bundle; diff --git a/tests/UiBench/src/com/android/test/uibench/leanback/BrowseFragment.java b/tests/UiBench/src/com/android/test/uibench/leanback/BrowseFragment.java index 4ab38a66d504..b1cd89d4997d 100644 --- a/tests/UiBench/src/com/android/test/uibench/leanback/BrowseFragment.java +++ b/tests/UiBench/src/com/android/test/uibench/leanback/BrowseFragment.java @@ -17,7 +17,7 @@ package com.android.test.uibench.leanback; import android.os.Bundle; -public class BrowseFragment extends android.support.v17.leanback.app.BrowseSupportFragment { +public class BrowseFragment extends androidx.leanback.app.BrowseSupportFragment { public BrowseFragment() { } diff --git a/tests/UiBench/src/com/android/test/uibench/leanback/CardPresenter.java b/tests/UiBench/src/com/android/test/uibench/leanback/CardPresenter.java index 5194555aca01..ba7180050223 100644 --- a/tests/UiBench/src/com/android/test/uibench/leanback/CardPresenter.java +++ b/tests/UiBench/src/com/android/test/uibench/leanback/CardPresenter.java @@ -17,9 +17,9 @@ package com.android.test.uibench.leanback; import android.content.Context; import android.graphics.drawable.Drawable; -import android.support.v17.leanback.widget.ImageCardView; -import android.support.v17.leanback.widget.Presenter; -import android.support.v4.content.res.ResourcesCompat; +import androidx.leanback.widget.ImageCardView; +import androidx.leanback.widget.Presenter; +import androidx.core.content.res.ResourcesCompat; import android.view.ContextThemeWrapper; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; diff --git a/tests/UiBench/src/com/android/test/uibench/leanback/TestHelper.java b/tests/UiBench/src/com/android/test/uibench/leanback/TestHelper.java index bf408f7475ac..bcb47327850a 100644 --- a/tests/UiBench/src/com/android/test/uibench/leanback/TestHelper.java +++ b/tests/UiBench/src/com/android/test/uibench/leanback/TestHelper.java @@ -19,13 +19,13 @@ import android.app.Activity; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; -import android.support.v17.leanback.app.BackgroundManager; -import android.support.v17.leanback.widget.ArrayObjectAdapter; -import android.support.v17.leanback.widget.HeaderItem; -import android.support.v17.leanback.widget.ListRow; -import android.support.v17.leanback.widget.ListRowPresenter; -import android.support.v17.leanback.widget.ObjectAdapter; -import android.support.v17.leanback.widget.Presenter; +import androidx.leanback.app.BackgroundManager; +import androidx.leanback.widget.ArrayObjectAdapter; +import androidx.leanback.widget.HeaderItem; +import androidx.leanback.widget.ListRow; +import androidx.leanback.widget.ListRowPresenter; +import androidx.leanback.widget.ObjectAdapter; +import androidx.leanback.widget.Presenter; import android.util.DisplayMetrics; import android.util.TypedValue; diff --git a/tests/UiBench/src/com/android/test/uibench/listview/CompatListActivity.java b/tests/UiBench/src/com/android/test/uibench/listview/CompatListActivity.java index bb7f4a302f8c..66595585c525 100644 --- a/tests/UiBench/src/com/android/test/uibench/listview/CompatListActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/listview/CompatListActivity.java @@ -16,9 +16,9 @@ package com.android.test.uibench.listview; import android.os.Bundle; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.ListFragment; -import android.support.v7.app.AppCompatActivity; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.ListFragment; +import androidx.appcompat.app.AppCompatActivity; import android.widget.ListAdapter; public abstract class CompatListActivity extends AppCompatActivity { diff --git a/tests/UiBench/src/com/android/test/uibench/listview/FadingEdgeListFragment.java b/tests/UiBench/src/com/android/test/uibench/listview/FadingEdgeListFragment.java index a018b40e8528..538066ef7abf 100644 --- a/tests/UiBench/src/com/android/test/uibench/listview/FadingEdgeListFragment.java +++ b/tests/UiBench/src/com/android/test/uibench/listview/FadingEdgeListFragment.java @@ -16,7 +16,7 @@ package com.android.test.uibench.listview; import android.os.Bundle; -import android.support.v4.app.ListFragment; +import androidx.fragment.app.ListFragment; import android.widget.ListView; public class FadingEdgeListFragment extends ListFragment { diff --git a/tests/UiBench/src/com/android/test/uibench/recyclerview/RvArrayAdapter.java b/tests/UiBench/src/com/android/test/uibench/recyclerview/RvArrayAdapter.java index e5a3a49cf228..7a355c8acfd3 100644 --- a/tests/UiBench/src/com/android/test/uibench/recyclerview/RvArrayAdapter.java +++ b/tests/UiBench/src/com/android/test/uibench/recyclerview/RvArrayAdapter.java @@ -15,7 +15,7 @@ */ package com.android.test.uibench.recyclerview; -import android.support.v7.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; diff --git a/tests/UiBench/src/com/android/test/uibench/recyclerview/RvBoxAdapter.java b/tests/UiBench/src/com/android/test/uibench/recyclerview/RvBoxAdapter.java index 3440f192e7c5..abb1160b6b1a 100644 --- a/tests/UiBench/src/com/android/test/uibench/recyclerview/RvBoxAdapter.java +++ b/tests/UiBench/src/com/android/test/uibench/recyclerview/RvBoxAdapter.java @@ -17,7 +17,7 @@ package com.android.test.uibench.recyclerview; import android.content.Context; import android.graphics.Color; -import android.support.v7.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView; import android.util.TypedValue; import android.view.ViewGroup; import android.widget.TextView; diff --git a/tests/UiBench/src/com/android/test/uibench/recyclerview/RvCompatListActivity.java b/tests/UiBench/src/com/android/test/uibench/recyclerview/RvCompatListActivity.java index 939b66198d72..bd313ad7636d 100644 --- a/tests/UiBench/src/com/android/test/uibench/recyclerview/RvCompatListActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/recyclerview/RvCompatListActivity.java @@ -17,12 +17,12 @@ package com.android.test.uibench.recyclerview; import android.content.Context; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; diff --git a/tests/UsageReportingTest/Android.bp b/tests/UsageReportingTest/Android.bp new file mode 100644 index 000000000000..0bac5a224b26 --- /dev/null +++ b/tests/UsageReportingTest/Android.bp @@ -0,0 +1,8 @@ +android_test { + name: "UsageReportingTest", + // Only compile source java files in this apk. + srcs: ["src/**/*.java"], + static_libs: ["androidx.legacy_legacy-support-v4"], + certificate: "platform", + platform_apis: true, +} diff --git a/tests/UsageReportingTest/AndroidManifest.xml b/tests/UsageReportingTest/AndroidManifest.xml new file mode 100644 index 000000000000..be0b09e972a5 --- /dev/null +++ b/tests/UsageReportingTest/AndroidManifest.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + Note: Add android:sharedUserId="android.uid.system" to the root element to simulate the system UID + caller case. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.tests.usagereporter" + > + + <application android:label="@string/reporter_app"> + <activity android:name="UsageReporterActivity" + android:label="UsageReporter"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + + </application> +</manifest> diff --git a/tests/ImfTest/res/layout/dialog_edit_text_no_scroll.xml b/tests/UsageReportingTest/res/layout/row_item.xml index 1a2b7eb0407e..1eb2dab29124 100644 --- a/tests/ImfTest/res/layout/dialog_edit_text_no_scroll.xml +++ b/tests/UsageReportingTest/res/layout/row_item.xml @@ -1,12 +1,12 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2009 The Android Open Source Project +<!-- Copyright (C) 2018 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - + http://www.apache.org/licenses/LICENSE-2.0 - + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,22 +15,22 @@ --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent" - android:padding="20dip" - android:orientation="vertical"> - - <View - android:id="@+id/blank" - android:layout_height="0dip" - android:layout_width="match_parent" - android:layout_weight="1"/> - - <EditText - android:id="@+id/dialog_edit_text" + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:background="@color/inactive_color"> + + <TextView android:id="@+id/token" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_width="match_parent" - android:scrollHorizontally="true" - android:textAppearance="?android:attr/textAppearanceMedium" /> + android:layout_weight="1" + android:textStyle="bold"/> + + <Button android:id="@+id/start" style="@style/ActionButton" + android:text="@string/start" /> + <Button android:id="@+id/stop" style="@style/ActionButton" + android:text="@string/stop" /> </LinearLayout> diff --git a/tests/UsageReportingTest/res/menu/main.xml b/tests/UsageReportingTest/res/menu/main.xml new file mode 100644 index 000000000000..9847c2dce8f2 --- /dev/null +++ b/tests/UsageReportingTest/res/menu/main.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8" ?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/add_token" + android:title="@string/add_token"/> + <item android:id="@+id/add_many_tokens" + android:title="@string/add_many_tokens"/> + <item android:id="@+id/stop_all" + android:title="@string/stop_all_tokens"/> + <group android:checkableBehavior="all"> + <item android:id="@+id/restore_on_start" + android:title="@string/restore_tokens_on_start"/> + </group> +</menu>
\ No newline at end of file diff --git a/tests/UsageReportingTest/res/values/colors.xml b/tests/UsageReportingTest/res/values/colors.xml new file mode 100644 index 000000000000..03bcf8a60182 --- /dev/null +++ b/tests/UsageReportingTest/res/values/colors.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<resources> + <color name="active_color">#FFF</color> + <color name="inactive_color">#AAA</color> +</resources>
\ No newline at end of file diff --git a/tests/UsageReportingTest/res/values/strings.xml b/tests/UsageReportingTest/res/values/strings.xml new file mode 100644 index 000000000000..015290e732a0 --- /dev/null +++ b/tests/UsageReportingTest/res/values/strings.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + <!-- Do not translate --> + <string name="reporter_app">Usage Reporter App</string> + <!-- Do not translate --> + <string name="start">Start</string> + <!-- Do not translate --> + <string name="stop">Stop</string> + <!-- Do not translate --> + <string name="default_token">SuperSecretToken</string> + + <!-- Do not translate --> + <string name="add_token">Add Token</string> + <!-- Do not translate --> + <string name="add_many_tokens">Add Many Tokens</string> + <!-- Do not translate --> + <string name="stop_all_tokens">Stop All</string> + <!-- Do not translate --> + <string name="restore_tokens_on_start">Readd Tokens on Start</string> + + + <!-- Do not translate --> + <string name="token_query">Enter token(s) (delimit tokens with commas)</string> + <!-- Do not translate --> + <string name="many_tokens_query">Generate how many tokens?</string> + <!-- Do not translate --> + <string name="stop_all_tokens_query">Stop all tokens?</string> + <!-- Do not translate --> + <string name="ok">OK</string> + <!-- Do not translate --> + <string name="cancel">Cancel</string> +</resources>
\ No newline at end of file diff --git a/tests/UsageReportingTest/res/values/styles.xml b/tests/UsageReportingTest/res/values/styles.xml new file mode 100644 index 000000000000..e5b86c5e836b --- /dev/null +++ b/tests/UsageReportingTest/res/values/styles.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + <style name="ActionButton"> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:textAppearance">@style/TextAppearance.ActionButton</item> + </style> + + <style name="TextAppearance" parent="android:TextAppearance"> + </style> + + <style name="TextAppearance.ActionButton"> + <item name="android:textStyle">italic</item> + </style> + +</resources> diff --git a/tests/UsageReportingTest/src/com/android/tests/usagereporter/UsageReporterActivity.java b/tests/UsageReportingTest/src/com/android/tests/usagereporter/UsageReporterActivity.java new file mode 100644 index 000000000000..946be8fe93d3 --- /dev/null +++ b/tests/UsageReportingTest/src/com/android/tests/usagereporter/UsageReporterActivity.java @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tests.usagereporter; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.ListActivity; +import android.app.usage.UsageStatsManager; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.text.InputType; +import android.text.TextUtils; +import android.util.ArraySet; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import java.util.ArrayList; + +public class UsageReporterActivity extends ListActivity { + + private Activity mActivity; + private final ArrayList<String> mTokens = new ArrayList(); + private final ArraySet<String> mActives = new ArraySet(); + private UsageStatsManager mUsageStatsManager; + private Adapter mAdapter; + private boolean mRestoreOnStart = false; + private static Context sContext; + + /** Called with the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + sContext = getApplicationContext(); + + mUsageStatsManager = (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE); + + mAdapter = new Adapter(); + setListAdapter(mAdapter); + } + + @Override + protected void onStart() { + super.onStart(); + mActivity = this; + + + if (mRestoreOnStart) { + ArrayList<String> removed = null; + for (String token : mActives) { + try { + mUsageStatsManager.reportUsageStart(mActivity, token); + } catch (Exception e) { + // Somthing went wrong, recover and move on + if (removed == null) { + removed = new ArrayList(); + } + removed.add(token); + } + } + if (removed != null) { + for (String token : removed) { + mActives.remove(token); + } + } + } else { + mActives.clear(); + } + } + + /** + * Called when the activity is about to start interacting with the user. + */ + @Override + protected void onResume() { + super.onResume(); + mAdapter.notifyDataSetChanged(); + } + + + /** + * Called when your activity's options menu needs to be created. + */ + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.main, menu); + return super.onCreateOptionsMenu(menu); + } + + + /** + * Called when a menu item is selected. + */ + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.add_token: + callAddToken(); + return true; + case R.id.add_many_tokens: + callAddManyTokens(); + return true; + case R.id.stop_all: + callStopAll(); + return true; + case R.id.restore_on_start: + mRestoreOnStart = !mRestoreOnStart; + item.setChecked(mRestoreOnStart); + return true; + } + + return super.onOptionsItemSelected(item); + } + + private void callAddToken() { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.token_query)); + final EditText input = new EditText(this); + input.setInputType(InputType.TYPE_CLASS_TEXT); + input.setHint(getString(R.string.default_token)); + builder.setView(input); + + builder.setPositiveButton(getString(R.string.ok), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String tokenNames = input.getText().toString().trim(); + if (TextUtils.isEmpty(tokenNames)) { + tokenNames = getString(R.string.default_token); + } + String[] tokens = tokenNames.split(","); + for (String token : tokens) { + if (mTokens.contains(token)) continue; + mTokens.add(token); + } + mAdapter.notifyDataSetChanged(); + + } + }); + builder.setNegativeButton(getString(R.string.cancel), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + + builder.show(); + } + + private void callAddManyTokens() { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.many_tokens_query)); + final EditText input = new EditText(this); + input.setInputType(InputType.TYPE_CLASS_NUMBER); + builder.setView(input); + + builder.setPositiveButton(getString(R.string.ok), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String val = input.getText().toString().trim(); + if (TextUtils.isEmpty(val)) return; + int n = Integer.parseInt(val); + for (int i = 0; i < n; i++) { + final String token = getString(R.string.default_token) + i; + if (mTokens.contains(token)) continue; + mTokens.add(token); + } + mAdapter.notifyDataSetChanged(); + + } + }); + builder.setNegativeButton(getString(R.string.cancel), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + + builder.show(); + } + + private void callStopAll() { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.stop_all_tokens_query)); + + builder.setPositiveButton(getString(R.string.ok), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + for (String token : mActives) { + mUsageStatsManager.reportUsageStop(mActivity, token); + } + mActives.clear(); + mAdapter.notifyDataSetChanged(); + } + }); + builder.setNegativeButton(getString(R.string.cancel), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + builder.show(); + } + + /** + * A call-back for when the user presses the back button. + */ + OnClickListener mStartListener = new OnClickListener() { + public void onClick(View v) { + final View parent = (View) v.getParent(); + final String token = ((TextView) parent.findViewById(R.id.token)).getText().toString(); + try { + mUsageStatsManager.reportUsageStart(mActivity, token); + } catch (Exception e) { + Toast.makeText(sContext, e.toString(), Toast.LENGTH_LONG).show(); + } + parent.setBackgroundColor(getColor(R.color.active_color)); + mActives.add(token); + } + }; + + /** + * A call-back for when the user presses the clear button. + */ + OnClickListener mStopListener = new OnClickListener() { + public void onClick(View v) { + final View parent = (View) v.getParent(); + + final String token = ((TextView) parent.findViewById(R.id.token)).getText().toString(); + try { + mUsageStatsManager.reportUsageStop(mActivity, token); + } catch (Exception e) { + Toast.makeText(sContext, e.toString(), Toast.LENGTH_LONG).show(); + } + parent.setBackgroundColor(getColor(R.color.inactive_color)); + mActives.remove(token); + } + }; + + + private class Adapter extends BaseAdapter { + @Override + public int getCount() { + return mTokens.size(); + } + + @Override + public Object getItem(int position) { + return mTokens.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final ViewHolder holder; + if (convertView == null) { + convertView = LayoutInflater.from(UsageReporterActivity.this) + .inflate(R.layout.row_item, parent, false); + holder = new ViewHolder(); + holder.tokenName = (TextView) convertView.findViewById(R.id.token); + + holder.startButton = ((Button) convertView.findViewById(R.id.start)); + holder.startButton.setOnClickListener(mStartListener); + holder.stopButton = ((Button) convertView.findViewById(R.id.stop)); + holder.stopButton.setOnClickListener(mStopListener); + convertView.setTag(holder); + } else { + holder = (ViewHolder) convertView.getTag(); + } + + final String token = mTokens.get(position); + holder.tokenName.setText(mTokens.get(position)); + if (mActives.contains(token)) { + convertView.setBackgroundColor(getColor(R.color.active_color)); + } else { + convertView.setBackgroundColor(getColor(R.color.inactive_color)); + } + return convertView; + } + } + + private static class ViewHolder { + public TextView tokenName; + public Button startButton; + public Button stopButton; + } +} diff --git a/tests/UsageStatsPerfTests/Android.bp b/tests/UsageStatsPerfTests/Android.bp new file mode 100644 index 000000000000..3991fb8366ac --- /dev/null +++ b/tests/UsageStatsPerfTests/Android.bp @@ -0,0 +1,26 @@ +// Copyright (C) 2018 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +android_test { + name: "UsageStatsPerfTests", + srcs: ["src/**/*.java"], + static_libs: [ + "androidx.test.rules", + "apct-perftests-utils", + "services.usage", + ], + platform_apis: true, + // For android.permission.FORCE_STOP_PACKAGES permission + certificate: "platform", +} diff --git a/tests/UsageStatsPerfTests/AndroidManifest.xml b/tests/UsageStatsPerfTests/AndroidManifest.xml new file mode 100644 index 000000000000..98915485c7bd --- /dev/null +++ b/tests/UsageStatsPerfTests/AndroidManifest.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.frameworks.perftests.usage"> + <uses-sdk + android:minSdkVersion="21" /> + <uses-permission android:name="android.permission.DUMP" /> + <uses-permission android:name="android.permission.FORCE_STOP_PACKAGES" /> + + <application> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.frameworks.perftests.usage"/> +</manifest> diff --git a/tests/UsageStatsPerfTests/AndroidTest.xml b/tests/UsageStatsPerfTests/AndroidTest.xml new file mode 100644 index 000000000000..b8892ebc2ff7 --- /dev/null +++ b/tests/UsageStatsPerfTests/AndroidTest.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<configuration description="Runs UsageStats Performance Tests"> + <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <option name="test-file-name" value="UsageStatsPerfTests.apk"/> + <option name="cleanup-apks" value="true"/> + </target_preparer> + + <option name="test-suite-tag" value="apct"/> + <option name="test-tag" value="UsageStatsPerfTests"/> + <test class="com.android.tradefed.testtype.AndroidJUnitTest"> + <option name="package" value="com.android.frameworks.perftests.usage"/> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner"/> + </test> +</configuration>
\ No newline at end of file diff --git a/tests/UsageStatsPerfTests/src/com/android/frameworks/perftests/usage/tests/UsageStatsDatabasePerfTest.java b/tests/UsageStatsPerfTests/src/com/android/frameworks/perftests/usage/tests/UsageStatsDatabasePerfTest.java new file mode 100644 index 000000000000..7d9d0d52b002 --- /dev/null +++ b/tests/UsageStatsPerfTests/src/com/android/frameworks/perftests/usage/tests/UsageStatsDatabasePerfTest.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.frameworks.perftests.usage.tests; + +import static junit.framework.Assert.assertEquals; + +import android.app.usage.UsageEvents; +import android.app.usage.UsageStatsManager; +import android.content.Context; +import android.os.SystemClock; +import android.perftests.utils.ManualBenchmarkState; +import android.perftests.utils.PerfManualStatusReporter; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.usage.IntervalStats; +import com.android.server.usage.UsageStatsDatabase; +import com.android.server.usage.UsageStatsDatabase.StatCombiner; + +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class UsageStatsDatabasePerfTest { + protected static Context sContext; + private static UsageStatsDatabase sUsageStatsDatabase; + private static File mTestDir; + + // Represents how many apps might have used in a day by a user with a few apps + final static int FEW_PKGS = 10; + // Represent how many apps might have used in a day by a user with many apps + final static int MANY_PKGS = 50; + // Represents how many usage events per app a device might have with light usage + final static int LIGHT_USE = 10; + // Represents how many usage events per app a device might have with heavy usage + final static int HEAVY_USE = 50; + + private static final StatCombiner<UsageEvents.Event> sUsageStatsCombiner = + new StatCombiner<UsageEvents.Event>() { + @Override + public void combine(IntervalStats stats, boolean mutable, + List<UsageEvents.Event> accResult) { + final int size = stats.events.size(); + for (int i = 0; i < size; i++) { + accResult.add(stats.events.get(i)); + } + } + }; + + + @Rule + public PerfManualStatusReporter mPerfManualStatusReporter = new PerfManualStatusReporter(); + + @BeforeClass + public static void setUpOnce() { + sContext = InstrumentationRegistry.getTargetContext(); + mTestDir = new File(sContext.getFilesDir(), "UsageStatsDatabasePerfTest"); + sUsageStatsDatabase = new UsageStatsDatabase(mTestDir); + sUsageStatsDatabase.init(1); + } + + private static void populateIntervalStats(IntervalStats intervalStats, int packageCount, + int eventsPerPackage) { + for (int pkg = 0; pkg < packageCount; pkg++) { + UsageEvents.Event event = new UsageEvents.Event(); + event.mPackage = "fake.package.name" + pkg; + event.mClass = event.mPackage + ".class1"; + event.mTimeStamp = 1; + event.mEventType = UsageEvents.Event.ACTIVITY_RESUMED; + for (int evt = 0; evt < eventsPerPackage; evt++) { + intervalStats.events.insert(event); + intervalStats.update(event.mPackage, event.mClass, event.mTimeStamp, + event.mEventType, 1); + } + } + } + + private static void clearUsageStatsFiles() { + File[] intervalDirs = mTestDir.listFiles(); + for (File intervalDir : intervalDirs) { + if (intervalDir.isDirectory()) { + File[] usageFiles = intervalDir.listFiles(); + for (File f : usageFiles) { + f.delete(); + } + } + } + } + + private void runQueryUsageStatsTest(int packageCount, int eventsPerPackage) throws IOException { + final ManualBenchmarkState benchmarkState = mPerfManualStatusReporter.getBenchmarkState(); + IntervalStats intervalStats = new IntervalStats(); + populateIntervalStats(intervalStats, packageCount, eventsPerPackage); + sUsageStatsDatabase.putUsageStats(0, intervalStats); + long elapsedTimeNs = 0; + while (benchmarkState.keepRunning(elapsedTimeNs)) { + final long startTime = SystemClock.elapsedRealtimeNanos(); + List<UsageEvents.Event> temp = sUsageStatsDatabase.queryUsageStats( + UsageStatsManager.INTERVAL_DAILY, 0, 2, sUsageStatsCombiner); + final long endTime = SystemClock.elapsedRealtimeNanos(); + elapsedTimeNs = endTime - startTime; + assertEquals(packageCount * eventsPerPackage, temp.size()); + } + } + + private void runPutUsageStatsTest(int packageCount, int eventsPerPackage) throws IOException { + final ManualBenchmarkState benchmarkState = mPerfManualStatusReporter.getBenchmarkState(); + IntervalStats intervalStats = new IntervalStats(); + populateIntervalStats(intervalStats, packageCount, eventsPerPackage); + long elapsedTimeNs = 0; + while (benchmarkState.keepRunning(elapsedTimeNs)) { + final long startTime = SystemClock.elapsedRealtimeNanos(); + sUsageStatsDatabase.putUsageStats(0, intervalStats); + final long endTime = SystemClock.elapsedRealtimeNanos(); + elapsedTimeNs = endTime - startTime; + clearUsageStatsFiles(); + } + } + + @Test + public void testQueryUsageStats_FewPkgsLightUse() throws IOException { + runQueryUsageStatsTest(FEW_PKGS, LIGHT_USE); + } + + @Test + public void testPutUsageStats_FewPkgsLightUse() throws IOException { + runPutUsageStatsTest(FEW_PKGS, LIGHT_USE); + } + + @Test + public void testQueryUsageStats_FewPkgsHeavyUse() throws IOException { + runQueryUsageStatsTest(FEW_PKGS, HEAVY_USE); + } + + @Test + public void testPutUsageStats_FewPkgsHeavyUse() throws IOException { + runPutUsageStatsTest(FEW_PKGS, HEAVY_USE); + } + + @Test + public void testQueryUsageStats_ManyPkgsLightUse() throws IOException { + runQueryUsageStatsTest(MANY_PKGS, LIGHT_USE); + } + + @Test + public void testPutUsageStats_ManyPkgsLightUse() throws IOException { + runPutUsageStatsTest(MANY_PKGS, LIGHT_USE); + } + + @Test + public void testQueryUsageStats_ManyPkgsHeavyUse() throws IOException { + runQueryUsageStatsTest(MANY_PKGS, HEAVY_USE); + } + + @Test + public void testPutUsageStats_ManyPkgsHeavyUse() throws IOException { + runPutUsageStatsTest(MANY_PKGS, HEAVY_USE); + } +} diff --git a/tests/UsageStatsTest/Android.bp b/tests/UsageStatsTest/Android.bp index 4995effa1ce9..0808b05ec053 100644 --- a/tests/UsageStatsTest/Android.bp +++ b/tests/UsageStatsTest/Android.bp @@ -2,7 +2,7 @@ android_test { name: "UsageStatsTest", // Only compile source java files in this apk. srcs: ["src/**/*.java"], - static_libs: ["android-support-v4"], + static_libs: ["androidx.legacy_legacy-support-v4"], certificate: "platform", platform_apis: true, } diff --git a/tests/UsageStatsTest/AndroidManifest.xml b/tests/UsageStatsTest/AndroidManifest.xml index 4b1c1bd69920..fefd99394a87 100644 --- a/tests/UsageStatsTest/AndroidManifest.xml +++ b/tests/UsageStatsTest/AndroidManifest.xml @@ -11,6 +11,7 @@ <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" /> <uses-permission android:name="android.permission.OBSERVE_APP_USAGE" /> + <uses-permission android:name="android.permission.SUSPEND_APPS" /> <application android:label="Usage Access Test"> <activity android:name=".UsageStatsActivity" diff --git a/tests/UsageStatsTest/res/menu/main.xml b/tests/UsageStatsTest/res/menu/main.xml index 612267c85b1b..272e0f4e1f54 100644 --- a/tests/UsageStatsTest/res/menu/main.xml +++ b/tests/UsageStatsTest/res/menu/main.xml @@ -6,4 +6,6 @@ android:title="Call isAppInactive()"/> <item android:id="@+id/set_app_limit" android:title="Set App Limit" /> + <item android:id="@+id/set_app_usage_limit" + android:title="Set App Usage Limit" /> </menu> diff --git a/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageLogActivity.java b/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageLogActivity.java index 05cac10f92c6..53afa26796ea 100644 --- a/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageLogActivity.java +++ b/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageLogActivity.java @@ -21,13 +21,14 @@ import android.app.usage.UsageStatsManager; import android.content.Context; import android.os.Bundle; import android.os.Handler; -import android.support.v4.util.CircularArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.TextView; +import androidx.collection.CircularArray; + public class UsageLogActivity extends ListActivity implements Runnable { private static final long USAGE_STATS_PERIOD = 1000 * 60 * 60 * 24 * 14; @@ -155,10 +156,10 @@ public class UsageLogActivity extends ListActivity implements Runnable { private String eventToString(int eventType) { switch (eventType) { - case UsageEvents.Event.MOVE_TO_FOREGROUND: + case UsageEvents.Event.ACTIVITY_RESUMED: return "Foreground"; - case UsageEvents.Event.MOVE_TO_BACKGROUND: + case UsageEvents.Event.ACTIVITY_PAUSED: return "Background"; case UsageEvents.Event.CONFIGURATION_CHANGE: diff --git a/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java b/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java index 3d8ce21a2c00..bb985d7f8be2 100644 --- a/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java +++ b/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java @@ -21,6 +21,8 @@ import android.app.ListActivity; import android.app.PendingIntent; import android.app.usage.UsageStats; import android.app.usage.UsageStatsManager; +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -39,6 +41,7 @@ import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; +import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -49,6 +52,8 @@ public class UsageStatsActivity extends ListActivity { private static final long USAGE_STATS_PERIOD = 1000 * 60 * 60 * 24 * 14; private static final String EXTRA_KEY_TIMEOUT = "com.android.tests.usagestats.extra.TIMEOUT"; private UsageStatsManager mUsageStatsManager; + private ClipboardManager mClipboard; + private ClipData mClip; private Adapter mAdapter; private Comparator<UsageStats> mComparator = new Comparator<UsageStats>() { @Override @@ -61,6 +66,7 @@ public class UsageStatsActivity extends ListActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mUsageStatsManager = (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE); + mClipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); mAdapter = new Adapter(); setListAdapter(mAdapter); Bundle extras = getIntent().getExtras(); @@ -98,6 +104,8 @@ public class UsageStatsActivity extends ListActivity { case R.id.set_app_limit: callSetAppLimit(); return true; + case R.id.set_app_usage_limit: + callSetAppUsageLimit(); default: return super.onOptionsItemSelected(item); } @@ -155,7 +163,7 @@ public class UsageStatsActivity extends ListActivity { intent.setPackage(getPackageName()); intent.putExtra(EXTRA_KEY_TIMEOUT, true); mUsageStatsManager.registerAppUsageObserver(1, packages, - 30, TimeUnit.SECONDS, PendingIntent.getActivity(UsageStatsActivity.this, + 60, TimeUnit.SECONDS, PendingIntent.getActivity(UsageStatsActivity.this, 1, intent, 0)); } } @@ -170,6 +178,40 @@ public class UsageStatsActivity extends ListActivity { builder.show(); } + private void callSetAppUsageLimit() { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Enter package name"); + final EditText input = new EditText(this); + input.setInputType(InputType.TYPE_CLASS_TEXT); + input.setHint("com.android.tests.usagestats"); + builder.setView(input); + + builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + final String packageName = input.getText().toString().trim(); + if (!TextUtils.isEmpty(packageName)) { + String[] packages = packageName.split(","); + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClass(UsageStatsActivity.this, UsageStatsActivity.class); + intent.setPackage(getPackageName()); + intent.putExtra(EXTRA_KEY_TIMEOUT, true); + mUsageStatsManager.registerAppUsageLimitObserver(1, packages, + Duration.ofSeconds(60), Duration.ofSeconds(0), + PendingIntent.getActivity(UsageStatsActivity.this, 1, intent, 0)); + } + } + }); + builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + + builder.show(); + } + private void showInactive(String packageName) { final AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setMessage( @@ -232,6 +274,21 @@ public class UsageStatsActivity extends ListActivity { holder.packageName.setText(mStats.get(position).getPackageName()); holder.usageTime.setText(DateUtils.formatDuration( mStats.get(position).getTotalTimeInForeground())); + + //copy package name to the clipboard for convenience + holder.packageName.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + String text = holder.packageName.getText().toString(); + mClip = ClipData.newPlainText("package_name", text); + mClipboard.setPrimaryClip(mClip); + + Toast.makeText(getApplicationContext(), "package name copied to clipboard", + Toast.LENGTH_SHORT).show(); + return true; + } + }); + return convertView; } } diff --git a/tests/UsbTests/Android.bp b/tests/UsbTests/Android.bp index 7dc7c85b25de..1b2cf638f514 100644 --- a/tests/UsbTests/Android.bp +++ b/tests/UsbTests/Android.bp @@ -19,7 +19,7 @@ android_test { srcs: ["**/*.java"], static_libs: [ "frameworks-base-testutils", - "android-support-test", + "androidx.test.rules", "mockito-target-inline-minus-junit4", "platform-test-annotations", "services.core", diff --git a/tests/UsbTests/AndroidManifest.xml b/tests/UsbTests/AndroidManifest.xml index 5d606951bb5e..03d1a3ee25e2 100644 --- a/tests/UsbTests/AndroidManifest.xml +++ b/tests/UsbTests/AndroidManifest.xml @@ -24,7 +24,7 @@ <uses-library android:name="android.test.runner" /> </application> - <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner" + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" android:targetPackage="com.android.server.usb" android:label="UsbTests"/> </manifest> diff --git a/tests/UsbTests/AndroidTest.xml b/tests/UsbTests/AndroidTest.xml index 4affad39b4eb..e55bc98ecdc1 100644 --- a/tests/UsbTests/AndroidTest.xml +++ b/tests/UsbTests/AndroidTest.xml @@ -24,7 +24,7 @@ <test class="com.android.tradefed.testtype.AndroidJUnitTest"> <option name="package" value="com.android.server.usb"/> - <option name="runner" value="android.support.test.runner.AndroidJUnitRunner"/> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner"/> <option name="hidden-api-checks" value="false"/> </test> </configuration> diff --git a/tests/UsbTests/src/com/android/server/usb/UsbDescriptorParserTests.java b/tests/UsbTests/src/com/android/server/usb/UsbDescriptorParserTests.java index ea027d7ae049..89dc79c08261 100644 --- a/tests/UsbTests/src/com/android/server/usb/UsbDescriptorParserTests.java +++ b/tests/UsbTests/src/com/android/server/usb/UsbDescriptorParserTests.java @@ -16,29 +16,29 @@ package com.android.server.usb; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import android.content.Context; import android.content.res.Resources; import android.content.res.Resources.NotFoundException; -import android.support.test.InstrumentationRegistry; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; import com.android.server.usb.descriptors.UsbDescriptorParser; -import com.android.server.usb.descriptors.UsbDeviceDescriptor; -import com.google.common.io.ByteStreams; -import java.io.InputStream; -import java.io.IOException; -import java.lang.Exception; +import com.google.common.io.ByteStreams; import org.junit.Test; import org.junit.runner.RunWith; +import java.io.IOException; +import java.io.InputStream; + /** * Tests for {@link com.android.server.usb.descriptors.UsbDescriptorParser} */ diff --git a/tests/UsbTests/src/com/android/server/usb/UsbHandlerTest.java b/tests/UsbTests/src/com/android/server/usb/UsbHandlerTest.java index 23311b027183..ca1eb705e457 100644 --- a/tests/UsbTests/src/com/android/server/usb/UsbHandlerTest.java +++ b/tests/UsbTests/src/com/android/server/usb/UsbHandlerTest.java @@ -31,11 +31,11 @@ import android.hardware.usb.UsbManager; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.os.UserHandle; import android.provider.Settings; -import android.support.test.InstrumentationRegistry; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; import com.android.server.FgThread; @@ -59,8 +59,6 @@ public class UsbHandlerTest { @Mock private UsbDeviceManager mUsbDeviceManager; @Mock - private UsbDebuggingManager mUsbDebuggingManager; - @Mock private UsbAlsaManager mUsbAlsaManager; @Mock private UsbSettingsManager mUsbSettingsManager; @@ -89,9 +87,8 @@ public class UsbHandlerTest { Intent mBroadcastedIntent; MockUsbHandler(Looper looper, Context context, UsbDeviceManager deviceManager, - UsbDebuggingManager debuggingManager, UsbAlsaManager alsaManager, - UsbSettingsManager settingsManager) { - super(looper, context, deviceManager, debuggingManager, alsaManager, settingsManager); + UsbAlsaManager alsaManager, UsbSettingsManager settingsManager) { + super(looper, context, deviceManager, alsaManager, settingsManager); mUseUsbNotification = false; mIsUsbTransferAllowed = true; mCurrentUsbFunctionsReceived = true; @@ -144,8 +141,8 @@ public class UsbHandlerTest { when(mSharedPreferences.edit()).thenReturn(mEditor); mUsbHandler = new MockUsbHandler(FgThread.get().getLooper(), - InstrumentationRegistry.getContext(), mUsbDeviceManager, mUsbDebuggingManager, - mUsbAlsaManager, mUsbSettingsManager); + InstrumentationRegistry.getContext(), mUsbDeviceManager, mUsbAlsaManager, + mUsbSettingsManager); } @SmallTest @@ -190,8 +187,7 @@ public class UsbHandlerTest { assertEquals(mUsbHandler.getEnabledFunctions(), UsbManager.FUNCTION_NONE); assertEquals(mMockProperties.get(UsbDeviceManager.UsbHandler .USB_PERSISTENT_CONFIG_PROPERTY), UsbManager.USB_FUNCTION_ADB); - verify(mUsbDebuggingManager).setAdbEnabled(true); - assertTrue(mUsbHandler.mAdbEnabled); + assertTrue(mUsbHandler.isAdbEnabled()); mUsbHandler.handleMessage(mUsbHandler.obtainMessage(MSG_UPDATE_STATE, 1, 1)); @@ -208,16 +204,15 @@ public class UsbHandlerTest { mMockProperties.put(UsbDeviceManager.UsbHandler.USB_PERSISTENT_CONFIG_PROPERTY, UsbManager.USB_FUNCTION_ADB); mUsbHandler = new MockUsbHandler(FgThread.get().getLooper(), - InstrumentationRegistry.getContext(), mUsbDeviceManager, mUsbDebuggingManager, - mUsbAlsaManager, mUsbSettingsManager); + InstrumentationRegistry.getContext(), mUsbDeviceManager, mUsbAlsaManager, + mUsbSettingsManager); sendBootCompleteMessages(mUsbHandler); mUsbHandler.handleMessage(mUsbHandler.obtainMessage(MSG_ENABLE_ADB, 0)); assertEquals(mUsbHandler.getEnabledFunctions(), UsbManager.FUNCTION_NONE); - assertFalse(mUsbHandler.mAdbEnabled); + assertFalse(mUsbHandler.isAdbEnabled()); assertEquals(mMockProperties.get(UsbDeviceManager.UsbHandler .USB_PERSISTENT_CONFIG_PROPERTY), ""); - verify(mUsbDebuggingManager).setAdbEnabled(false); } @SmallTest @@ -232,14 +227,13 @@ public class UsbHandlerTest { public void bootCompletedAdbEnabled() { mMockProperties.put(UsbDeviceManager.UsbHandler.USB_PERSISTENT_CONFIG_PROPERTY, "adb"); mUsbHandler = new MockUsbHandler(FgThread.get().getLooper(), - InstrumentationRegistry.getContext(), mUsbDeviceManager, mUsbDebuggingManager, - mUsbAlsaManager, mUsbSettingsManager); + InstrumentationRegistry.getContext(), mUsbDeviceManager, mUsbAlsaManager, + mUsbSettingsManager); sendBootCompleteMessages(mUsbHandler); assertEquals(mUsbHandler.getEnabledFunctions(), UsbManager.FUNCTION_NONE); assertEquals(mMockGlobalSettings.get(Settings.Global.ADB_ENABLED).intValue(), 1); - assertTrue(mUsbHandler.mAdbEnabled); - verify(mUsbDebuggingManager).setAdbEnabled(true); + assertTrue(mUsbHandler.isAdbEnabled()); } @SmallTest @@ -321,8 +315,8 @@ public class UsbHandlerTest { UsbDeviceManager.UNLOCKED_CONFIG_PREF, mUsbHandler.mCurrentUser), "")) .thenReturn(UsbManager.USB_FUNCTION_MTP); mUsbHandler = new MockUsbHandler(FgThread.get().getLooper(), - InstrumentationRegistry.getContext(), mUsbDeviceManager, mUsbDebuggingManager, - mUsbAlsaManager, mUsbSettingsManager); + InstrumentationRegistry.getContext(), mUsbDeviceManager, mUsbAlsaManager, + mUsbSettingsManager); sendBootCompleteMessages(mUsbHandler); mUsbHandler.handleMessage(mUsbHandler.obtainMessage(MSG_UPDATE_SCREEN_LOCK, 1)); mUsbHandler.handleMessage(mUsbHandler.obtainMessage(MSG_UPDATE_SCREEN_LOCK, 0)); @@ -335,4 +329,4 @@ public class UsbHandlerTest { handler.handleMessage(handler.obtainMessage(MSG_BOOT_COMPLETED)); handler.handleMessage(handler.obtainMessage(MSG_SYSTEM_READY)); } -}
\ No newline at end of file +} diff --git a/tests/WindowAnimationJank/Android.bp b/tests/WindowAnimationJank/Android.bp index 60e8f7417194..50b2297386cc 100644 --- a/tests/WindowAnimationJank/Android.bp +++ b/tests/WindowAnimationJank/Android.bp @@ -17,7 +17,7 @@ android_test { srcs: ["src/**/*.java"], static_libs: [ "ub-uiautomator", - "ub-janktesthelper", + "androidx.test.janktesthelper", "junit", ], libs: ["android.test.base.stubs"], diff --git a/tests/WindowAnimationJank/src/android/windowanimationjank/FullscreenRotationTest.java b/tests/WindowAnimationJank/src/android/windowanimationjank/FullscreenRotationTest.java index 1fb502a09874..6792a8b486af 100644 --- a/tests/WindowAnimationJank/src/android/windowanimationjank/FullscreenRotationTest.java +++ b/tests/WindowAnimationJank/src/android/windowanimationjank/FullscreenRotationTest.java @@ -17,8 +17,9 @@ package android.windowanimationjank; import android.os.Bundle; -import android.support.test.jank.JankTest; -import android.support.test.jank.GfxMonitor; + +import androidx.test.jank.GfxMonitor; +import androidx.test.jank.JankTest; /** * Detect janks during screen rotation for full-screen activity. Periodically change diff --git a/tests/WindowAnimationJank/src/android/windowanimationjank/WindowAnimationJankTestBase.java b/tests/WindowAnimationJank/src/android/windowanimationjank/WindowAnimationJankTestBase.java index bf739fa8da07..a8ace162c4d0 100644 --- a/tests/WindowAnimationJank/src/android/windowanimationjank/WindowAnimationJankTestBase.java +++ b/tests/WindowAnimationJank/src/android/windowanimationjank/WindowAnimationJankTestBase.java @@ -16,12 +16,10 @@ package android.windowanimationjank; -import java.io.IOException; -import java.util.StringTokenizer; - -import android.support.test.jank.JankTestBase; import android.support.test.uiautomator.UiDevice; +import androidx.test.jank.JankTestBase; + /** * This adds additional system level jank monitor and its result is merged with primary monitor * used in test. diff --git a/tests/WindowManagerStressTest/src/test/windowmanagerstresstest/MainActivity.java b/tests/WindowManagerStressTest/src/test/windowmanagerstresstest/MainActivity.java index ae3914ebf162..85646987940f 100644 --- a/tests/WindowManagerStressTest/src/test/windowmanagerstresstest/MainActivity.java +++ b/tests/WindowManagerStressTest/src/test/windowmanagerstresstest/MainActivity.java @@ -26,7 +26,9 @@ import android.util.MergedConfiguration; import android.view.Display; import android.view.DisplayCutout; import android.view.IWindowSession; +import android.view.InsetsState; import android.view.Surface; +import android.view.SurfaceControl; import android.view.View; import android.view.WindowManager; import android.view.WindowManager.LayoutParams; @@ -105,7 +107,7 @@ public class MainActivity extends Activity { window.mSeq, mLayoutParams, -1, -1, View.VISIBLE, 0, -1, mTmpRect, mTmpRect, mTmpRect, mTmpRect, mTmpRect, mTmpRect, mTmpRect, new DisplayCutout.ParcelableWrapper(), new MergedConfiguration(), - new Surface()); + new SurfaceControl(), new InsetsState()); } catch (RemoteException e) { e.printStackTrace(); } @@ -131,8 +133,9 @@ public class MainActivity extends Activity { final IWindowSession session = WindowManagerGlobal.getWindowSession(); final Rect tmpRect = new Rect(); try { - final int res = session.addToDisplayWithoutInputChannel(window, window.mSeq, layoutParams, - View.VISIBLE, Display.DEFAULT_DISPLAY, tmpRect, tmpRect); + final int res = session.addToDisplayWithoutInputChannel(window, window.mSeq, + layoutParams, View.VISIBLE, Display.DEFAULT_DISPLAY, tmpRect, tmpRect, + new InsetsState()); } catch (RemoteException e) { e.printStackTrace(); } diff --git a/tests/libs-permissions/Android.bp b/tests/libs-permissions/Android.bp index 16e927a0cbca..c7c4b10a8405 100644 --- a/tests/libs-permissions/Android.bp +++ b/tests/libs-permissions/Android.bp @@ -12,3 +12,18 @@ prebuilt_etc { sub_dir: "permissions", product_specific: true, } + +java_library { + name: "com.android.test.libs.product_services", + installable: true, + product_services_specific: true, + srcs: ["product_services/java/**/*.java"], + required: ["com.android.test.libs.product_services.xml"], +} + +prebuilt_etc { + name: "com.android.test.libs.product_services.xml", + src: "product_services/com.android.test.libs.product_services.xml", + sub_dir: "permissions", + product_services_specific: true, +} diff --git a/tests/libs-permissions/product_services/com.android.test.libs.product_services.xml b/tests/libs-permissions/product_services/com.android.test.libs.product_services.xml new file mode 100644 index 000000000000..082a9be80779 --- /dev/null +++ b/tests/libs-permissions/product_services/com.android.test.libs.product_services.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<permissions> + <library name="com.android.test.libs.product_services" + file="/product_services/framework/com.android.test.libs.product_services.jar" /> +</permissions> diff --git a/tests/libs-permissions/product_services/java/com/android/test/libs/product_services/LibsProductServicesTest.java b/tests/libs-permissions/product_services/java/com/android/test/libs/product_services/LibsProductServicesTest.java new file mode 100644 index 000000000000..dcbdae809889 --- /dev/null +++ b/tests/libs-permissions/product_services/java/com/android/test/libs/product_services/LibsProductServicesTest.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.libs.product_services; + +/** + * Test class for product_services libs. + */ +public class LibsProductServicesTest { + + /** + * Dummy method for testing. + */ + public static void test() { + } +} diff --git a/tests/net/Android.bp b/tests/net/Android.bp index 502aa97bfc68..e91abb6c4a44 100644 --- a/tests/net/Android.bp +++ b/tests/net/Android.bp @@ -20,8 +20,6 @@ java_defaults { "libdl_android", "libhidl-gen-utils", "libhidlbase", - "libhidltransport", - "libhwbinder", "libjsoncpp", "liblog", "liblzma", diff --git a/tests/net/integration/Android.bp b/tests/net/integration/Android.bp index 16a68d79596b..7d9b7b756ae3 100644 --- a/tests/net/integration/Android.bp +++ b/tests/net/integration/Android.bp @@ -14,6 +14,39 @@ // limitations under the License. // +android_test { + name: "FrameworksNetIntegrationTests", + platform_apis: true, + certificate: "platform", + srcs: [ + "src/**/*.kt", + "src/**/*.aidl", + ], + libs: [ + "android.test.mock", + ], + static_libs: [ + "TestNetworkStackLib", + "androidx.test.ext.junit", + "frameworks-net-integration-testutils", + "kotlin-reflect", + "mockito-target-extended-minus-junit4", + "net-tests-utils", + "services.core", + "services.net", + "testables", + ], + use_embedded_native_libs: true, + jni_libs: [ + // For mockito extended + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + // android_library does not include JNI libs: include NetworkStack dependencies here + "libnativehelper_compat_libc++", + "libnetworkstackutilsjni", + ], +} + // Utilities for testing framework code both in integration and unit tests. java_library { name: "frameworks-net-integration-testutils", diff --git a/tests/net/integration/AndroidManifest.xml b/tests/net/integration/AndroidManifest.xml new file mode 100644 index 000000000000..91b3cd9e791f --- /dev/null +++ b/tests/net/integration/AndroidManifest.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.android.server.net.integrationtests"> + + <!-- For ConnectivityService registerReceiverAsUser (receiving broadcasts) --> + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" /> + <!-- PermissionMonitor sets network permissions for each user --> + <uses-permission android:name="android.permission.MANAGE_USERS" /> + <!-- ConnectivityService sends notifications to BatteryStats --> + <uses-permission android:name="android.permission.UPDATE_DEVICE_STATS" /> + <application android:debuggable="true"> + <uses-library android:name="android.test.runner" /> + + <!-- This manifest is merged with the base manifest of the real NetworkStack app. + Remove the NetworkStackService from the base (real) manifest, and replace with a test + service that responds to the same intent --> + <service android:name="com.android.server.NetworkStackService" tools:node="remove"/> + <service android:name=".TestNetworkStackService" + android:process="com.android.server.net.integrationtests.testnetworkstack"> + <intent-filter> + <action android:name="android.net.INetworkStackConnector.Test"/> + </intent-filter> + </service> + <service android:name=".NetworkStackInstrumentationService" + android:process="com.android.server.net.integrationtests.testnetworkstack"> + <intent-filter> + <action android:name=".INetworkStackInstrumentation"/> + </intent-filter> + </service> + <service tools:replace="android:process" + android:name="com.android.server.connectivity.ipmemorystore.RegularMaintenanceJobService" + android:process="com.android.server.net.integrationtests.testnetworkstack"/> + + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.server.net.integrationtests" + android:label="Frameworks Net Integration Tests" /> + +</manifest> diff --git a/tests/net/integration/res/values/config.xml b/tests/net/integration/res/values/config.xml new file mode 100644 index 000000000000..2c8046ffd781 --- /dev/null +++ b/tests/net/integration/res/values/config.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- + Override configuration for testing. The below settings use the config_ variants, which are + normally used by RROs to override the setting with highest priority. --> + <integer name="config_captive_portal_dns_probe_timeout">12500</integer> + <string name="config_captive_portal_http_url" translatable="false">http://test.android.com</string> + <string name="config_captive_portal_https_url" translatable="false">https://secure.test.android.com</string> + <string-array name="config_captive_portal_fallback_urls" translatable="false"> + <item>http://fallback1.android.com</item> + <item>http://fallback2.android.com</item> + </string-array> + <string-array name="config_captive_portal_fallback_probe_specs" translatable="false"> + </string-array> +</resources> diff --git a/tests/net/integration/src/android/net/TestNetworkStackClient.kt b/tests/net/integration/src/android/net/TestNetworkStackClient.kt new file mode 100644 index 000000000000..01eb514a1c81 --- /dev/null +++ b/tests/net/integration/src/android/net/TestNetworkStackClient.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.net + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.os.IBinder +import com.android.server.net.integrationtests.TestNetworkStackService +import org.mockito.Mockito.any +import org.mockito.Mockito.spy +import org.mockito.Mockito.timeout +import org.mockito.Mockito.verify +import kotlin.test.fail + +const val TEST_ACTION_SUFFIX = ".Test" + +class TestNetworkStackClient(context: Context) : NetworkStackClient(TestDependencies(context)) { + // TODO: consider switching to TrackRecord for more expressive checks + private val lastCallbacks = HashMap<Network, INetworkMonitorCallbacks>() + + private class TestDependencies(private val context: Context) : Dependencies { + override fun addToServiceManager(service: IBinder) = Unit + override fun checkCallerUid() = Unit + + override fun getConnectivityModuleConnector(): ConnectivityModuleConnector { + return ConnectivityModuleConnector { _, _, _, inSystemProcess -> + getNetworkStackIntent(inSystemProcess) + }.also { it.init(context) } + } + + private fun getNetworkStackIntent(inSystemProcess: Boolean): Intent? { + // Simulate out-of-system-process config: in-process service not found (null intent) + if (inSystemProcess) return null + val intent = Intent(INetworkStackConnector::class.qualifiedName + TEST_ACTION_SUFFIX) + val serviceName = TestNetworkStackService::class.qualifiedName + ?: fail("TestNetworkStackService name not found") + intent.component = ComponentName(context.packageName, serviceName) + return intent + } + } + + // base may be an instance of an inaccessible subclass, so non-spyable. + // Use a known open class that delegates to the original instance for all methods except + // asBinder. asBinder needs to use its own non-delegated implementation as otherwise it would + // return a binder token to a class that is not spied on. + open class NetworkMonitorCallbacksWrapper(private val base: INetworkMonitorCallbacks) : + INetworkMonitorCallbacks.Stub(), INetworkMonitorCallbacks by base { + // asBinder is implemented by both base class and delegate: specify explicitly + override fun asBinder(): IBinder { + return super.asBinder() + } + } + + override fun makeNetworkMonitor(network: Network, name: String?, cb: INetworkMonitorCallbacks) { + val cbSpy = spy(NetworkMonitorCallbacksWrapper(cb)) + lastCallbacks[network] = cbSpy + super.makeNetworkMonitor(network, name, cbSpy) + } + + fun verifyNetworkMonitorCreated(network: Network, timeoutMs: Long) { + val cb = lastCallbacks[network] + ?: fail("NetworkMonitor for network $network not requested") + verify(cb, timeout(timeoutMs)).onNetworkMonitorCreated(any()) + } +}
\ No newline at end of file diff --git a/tests/net/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/net/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt new file mode 100644 index 000000000000..334b26d82129 --- /dev/null +++ b/tests/net/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.net.integrationtests + +import android.content.ComponentName +import android.content.Context +import android.content.Context.BIND_AUTO_CREATE +import android.content.Context.BIND_IMPORTANT +import android.content.Intent +import android.content.ServiceConnection +import android.net.ConnectivityManager +import android.net.IDnsResolver +import android.net.INetd +import android.net.INetworkPolicyManager +import android.net.INetworkStatsService +import android.net.LinkProperties +import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET +import android.net.NetworkCapabilities.TRANSPORT_CELLULAR +import android.net.NetworkRequest +import android.net.TestNetworkStackClient +import android.net.metrics.IpConnectivityLog +import android.os.ConditionVariable +import android.os.IBinder +import android.os.INetworkManagementService +import android.testing.TestableContext +import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.ConnectivityService +import com.android.server.LocalServices +import com.android.server.NetworkAgentWrapper +import com.android.server.TestNetIdManager +import com.android.server.connectivity.DefaultNetworkMetrics +import com.android.server.connectivity.IpConnectivityMetrics +import com.android.server.connectivity.MockableSystemProperties +import com.android.server.connectivity.ProxyTracker +import com.android.server.connectivity.Tethering +import com.android.server.net.NetworkPolicyManagerInternal +import com.android.testutils.TestableNetworkCallback +import org.junit.After +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.doNothing +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.spy +import org.mockito.MockitoAnnotations +import org.mockito.Spy +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.fail + +const val SERVICE_BIND_TIMEOUT_MS = 5_000L +const val TEST_TIMEOUT_MS = 1_000L + +/** + * Test that exercises an instrumented version of ConnectivityService against an instrumented + * NetworkStack in a different test process. + */ +@RunWith(AndroidJUnit4::class) +class ConnectivityServiceIntegrationTest { + // lateinit used here for mocks as they need to be reinitialized between each test and the test + // should crash if they are used before being initialized. + @Mock + private lateinit var netManager: INetworkManagementService + @Mock + private lateinit var statsService: INetworkStatsService + @Mock + private lateinit var policyManager: INetworkPolicyManager + @Mock + private lateinit var log: IpConnectivityLog + @Mock + private lateinit var netd: INetd + @Mock + private lateinit var dnsResolver: IDnsResolver + @Mock + private lateinit var metricsLogger: IpConnectivityMetrics.Logger + @Mock + private lateinit var defaultMetrics: DefaultNetworkMetrics + @Spy + private var context = TestableContext(realContext) + + // lateinit for these three classes under test, as they should be reset to a different instance + // for every test but should always be initialized before use (or the test should crash). + private lateinit var networkStackClient: TestNetworkStackClient + private lateinit var service: ConnectivityService + private lateinit var cm: ConnectivityManager + + companion object { + // lateinit for this binder token, as it must be initialized before any test code is run + // and use of it before init should crash the test. + private lateinit var nsInstrumentation: INetworkStackInstrumentation + private val bindingCondition = ConditionVariable(false) + + private val realContext get() = InstrumentationRegistry.getInstrumentation().context + + private class InstrumentationServiceConnection : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + Log.i("TestNetworkStack", "Service connected") + try { + if (service == null) fail("Error binding to NetworkStack instrumentation") + if (::nsInstrumentation.isInitialized) fail("Service already connected") + nsInstrumentation = INetworkStackInstrumentation.Stub.asInterface(service) + } finally { + bindingCondition.open() + } + } + + override fun onServiceDisconnected(name: ComponentName?) = Unit + } + + @BeforeClass + @JvmStatic + fun setUpClass() { + val intent = Intent(realContext, NetworkStackInstrumentationService::class.java) + intent.action = INetworkStackInstrumentation::class.qualifiedName + assertTrue(realContext.bindService(intent, InstrumentationServiceConnection(), + BIND_AUTO_CREATE or BIND_IMPORTANT), + "Error binding to instrumentation service") + assertTrue(bindingCondition.block(SERVICE_BIND_TIMEOUT_MS), + "Timed out binding to instrumentation service " + + "after $SERVICE_BIND_TIMEOUT_MS ms") + } + } + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + doReturn(defaultMetrics).`when`(metricsLogger).defaultNetworkMetrics() + doNothing().`when`(context).sendStickyBroadcastAsUser(any(), any(), any()) + + networkStackClient = TestNetworkStackClient(realContext) + networkStackClient.init() + networkStackClient.start() + + LocalServices.removeServiceForTest(NetworkPolicyManagerInternal::class.java) + LocalServices.addService(NetworkPolicyManagerInternal::class.java, + mock(NetworkPolicyManagerInternal::class.java)) + + service = TestConnectivityService(makeDependencies()) + cm = ConnectivityManager(context, service) + context.addMockSystemService(Context.CONNECTIVITY_SERVICE, cm) + + service.systemReady() + } + + private inner class TestConnectivityService(deps: Dependencies) : ConnectivityService( + context, netManager, statsService, policyManager, dnsResolver, log, netd, deps) + + private fun makeDependencies(): ConnectivityService.Dependencies { + val deps = spy(ConnectivityService.Dependencies()) + doReturn(networkStackClient).`when`(deps).networkStack + doReturn(metricsLogger).`when`(deps).metricsLogger + doReturn(mock(Tethering::class.java)).`when`(deps).makeTethering( + any(), any(), any(), any(), any()) + doReturn(mock(ProxyTracker::class.java)).`when`(deps).makeProxyTracker(any(), any()) + doReturn(mock(MockableSystemProperties::class.java)).`when`(deps).systemProperties + doReturn(TestNetIdManager()).`when`(deps).makeNetIdManager() + return deps + } + + @After + fun tearDown() { + nsInstrumentation.clearAllState() + } + + @Test + fun testValidation() { + val request = NetworkRequest.Builder() + .clearCapabilities() + .addCapability(NET_CAPABILITY_INTERNET) + .build() + val testCallback = TestableNetworkCallback() + + cm.registerNetworkCallback(request, testCallback) + nsInstrumentation.addHttpResponse(HttpResponse( + "http://test.android.com", + responseCode = 204, contentLength = 42, redirectUrl = null)) + nsInstrumentation.addHttpResponse(HttpResponse( + "https://secure.test.android.com", + responseCode = 204, contentLength = 42, redirectUrl = null)) + + val na = NetworkAgentWrapper(TRANSPORT_CELLULAR, LinkProperties(), context) + networkStackClient.verifyNetworkMonitorCreated(na.network, TEST_TIMEOUT_MS) + + na.addCapability(NET_CAPABILITY_INTERNET) + na.connect() + + testCallback.expectAvailableThenValidatedCallbacks(na.network, TEST_TIMEOUT_MS) + assertEquals(2, nsInstrumentation.getRequestUrls().size) + } +}
\ No newline at end of file diff --git a/tests/net/integration/src/com/android/server/net/integrationtests/HttpResponse.aidl b/tests/net/integration/src/com/android/server/net/integrationtests/HttpResponse.aidl new file mode 100644 index 000000000000..9a2bcfea7641 --- /dev/null +++ b/tests/net/integration/src/com/android/server/net/integrationtests/HttpResponse.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.net.integrationtests; + +parcelable HttpResponse;
\ No newline at end of file diff --git a/tests/net/integration/src/com/android/server/net/integrationtests/HttpResponse.kt b/tests/net/integration/src/com/android/server/net/integrationtests/HttpResponse.kt new file mode 100644 index 000000000000..45073d8df3f7 --- /dev/null +++ b/tests/net/integration/src/com/android/server/net/integrationtests/HttpResponse.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.net.integrationtests + +import android.os.Parcel +import android.os.Parcelable + +data class HttpResponse( + val requestUrl: String, + val responseCode: Int, + val contentLength: Long, + val redirectUrl: String? +) : Parcelable { + constructor(p: Parcel): this(p.readString(), p.readInt(), p.readLong(), p.readString()) + + override fun writeToParcel(dest: Parcel, flags: Int) { + with(dest) { + writeString(requestUrl) + writeInt(responseCode) + writeLong(contentLength) + writeString(redirectUrl) + } + } + + override fun describeContents() = 0 + companion object CREATOR : Parcelable.Creator<HttpResponse> { + override fun createFromParcel(source: Parcel) = HttpResponse(source) + override fun newArray(size: Int) = arrayOfNulls<HttpResponse?>(size) + } +}
\ No newline at end of file diff --git a/tests/net/integration/src/com/android/server/net/integrationtests/INetworkStackInstrumentation.aidl b/tests/net/integration/src/com/android/server/net/integrationtests/INetworkStackInstrumentation.aidl new file mode 100644 index 000000000000..efc58add9cf5 --- /dev/null +++ b/tests/net/integration/src/com/android/server/net/integrationtests/INetworkStackInstrumentation.aidl @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.net.integrationtests; + +import com.android.server.net.integrationtests.HttpResponse; + +interface INetworkStackInstrumentation { + void clearAllState(); + void addHttpResponse(in HttpResponse response); + List<String> getRequestUrls(); +}
\ No newline at end of file diff --git a/tests/net/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt b/tests/net/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt new file mode 100644 index 000000000000..4827d2997d93 --- /dev/null +++ b/tests/net/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.net.integrationtests + +import android.app.Service +import android.content.Intent +import java.net.URL +import java.util.Collections +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentLinkedQueue +import kotlin.collections.ArrayList +import kotlin.test.fail + +/** + * An instrumentation interface for the NetworkStack that allows controlling behavior to + * facilitate integration tests. + */ +class NetworkStackInstrumentationService : Service() { + override fun onBind(intent: Intent) = InstrumentationConnector.asBinder() + + object InstrumentationConnector : INetworkStackInstrumentation.Stub() { + private val httpResponses = ConcurrentHashMap<String, ConcurrentLinkedQueue<HttpResponse>>() + .run { + withDefault { key -> getOrPut(key) { ConcurrentLinkedQueue() } } + } + private val httpRequestUrls = Collections.synchronizedList(ArrayList<String>()) + + /** + * Called when an HTTP request is being processed by NetworkMonitor. Returns the response + * that should be simulated. + */ + fun processRequest(url: URL): HttpResponse { + val strUrl = url.toString() + httpRequestUrls.add(strUrl) + return httpResponses[strUrl]?.poll() + ?: fail("No mocked response for request: $strUrl. " + + "Mocked URL keys are: ${httpResponses.keys}") + } + + /** + * Clear all state of this connector. This is intended for use between two tests, so all + * state should be reset as if the connector was just created. + */ + override fun clearAllState() { + httpResponses.clear() + httpRequestUrls.clear() + } + + /** + * Add a response to a future HTTP request. + * + * <p>For any subsequent HTTP/HTTPS query, the first response with a matching URL will be + * used to mock the query response. + */ + override fun addHttpResponse(response: HttpResponse) { + httpResponses.getValue(response.requestUrl).add(response) + } + + /** + * Get the ordered list of request URLs that have been sent by NetworkMonitor, and were + * answered based on mock responses. + */ + override fun getRequestUrls(): List<String> { + return ArrayList(httpRequestUrls) + } + } +}
\ No newline at end of file diff --git a/tests/net/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt b/tests/net/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt new file mode 100644 index 000000000000..8e4a9dd3aecb --- /dev/null +++ b/tests/net/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.net.integrationtests + +import android.app.Service +import android.content.Context +import android.content.Intent +import android.net.INetworkMonitorCallbacks +import android.net.Network +import android.net.metrics.IpConnectivityLog +import android.net.util.SharedLog +import android.os.IBinder +import com.android.networkstack.metrics.DataStallStatsUtils +import com.android.server.NetworkStackService.NetworkMonitorConnector +import com.android.server.NetworkStackService.NetworkStackConnector +import com.android.server.connectivity.NetworkMonitor +import com.android.server.net.integrationtests.NetworkStackInstrumentationService.InstrumentationConnector +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.spy +import java.net.HttpURLConnection +import java.net.URL +import java.net.URLConnection + +private const val TEST_NETID = 42 + +/** + * Android service that can return an [android.net.INetworkStackConnector] which can be instrumented + * through [NetworkStackInstrumentationService]. + * Useful in tests to create test instrumented NetworkStack components that can receive + * instrumentation commands through [NetworkStackInstrumentationService]. + */ +class TestNetworkStackService : Service() { + override fun onBind(intent: Intent): IBinder = TestNetworkStackConnector(makeTestContext()) + + private fun makeTestContext() = spy(applicationContext).also { + doReturn(mock(IBinder::class.java)).`when`(it).getSystemService(Context.NETD_SERVICE) + } + + private class TestPermissionChecker : NetworkStackConnector.PermissionChecker() { + override fun enforceNetworkStackCallingPermission() = Unit + } + + private class NetworkMonitorDeps(private val privateDnsBypassNetwork: Network) : + NetworkMonitor.Dependencies() { + override fun getPrivateDnsBypassNetwork(network: Network?) = privateDnsBypassNetwork + override fun sendNetworkConditionsBroadcast(context: Context, broadcast: Intent) = Unit + } + + private inner class TestNetworkStackConnector(context: Context) : + NetworkStackConnector(context, TestPermissionChecker()) { + + private val network = Network(TEST_NETID) + private val privateDnsBypassNetwork = TestNetwork(TEST_NETID) + + private inner class TestNetwork(netId: Int) : Network(netId) { + override fun openConnection(url: URL): URLConnection { + val response = InstrumentationConnector.processRequest(url) + + val connection = mock(HttpURLConnection::class.java) + doReturn(response.responseCode).`when`(connection).responseCode + doReturn(response.contentLength).`when`(connection).contentLengthLong + doReturn(response.redirectUrl).`when`(connection).getHeaderField("location") + return connection + } + } + + override fun makeNetworkMonitor( + network: Network, + name: String?, + cb: INetworkMonitorCallbacks + ) { + val nm = NetworkMonitor(this@TestNetworkStackService, cb, + this.network, + mock(IpConnectivityLog::class.java), mock(SharedLog::class.java), + NetworkMonitorDeps(privateDnsBypassNetwork), + mock(DataStallStatsUtils::class.java)) + cb.onNetworkMonitorCreated(NetworkMonitorConnector(nm, TestPermissionChecker())) + } + } +}
\ No newline at end of file diff --git a/tests/net/java/android/net/MacAddressTest.java b/tests/net/java/android/net/MacAddressTest.java index b0e5fb1a65f2..daf187d01533 100644 --- a/tests/net/java/android/net/MacAddressTest.java +++ b/tests/net/java/android/net/MacAddressTest.java @@ -254,6 +254,39 @@ public class MacAddressTest { } } + @Test + public void testMatches() { + // match 4 bytes prefix + assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches( + MacAddress.fromString("aa:bb:cc:dd:00:00"), + MacAddress.fromString("ff:ff:ff:ff:00:00"))); + + // match bytes 0,1,2 and 5 + assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches( + MacAddress.fromString("aa:bb:cc:00:00:11"), + MacAddress.fromString("ff:ff:ff:00:00:ff"))); + + // match 34 bit prefix + assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches( + MacAddress.fromString("aa:bb:cc:dd:c0:00"), + MacAddress.fromString("ff:ff:ff:ff:c0:00"))); + + // fail to match 36 bit prefix + assertFalse(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches( + MacAddress.fromString("aa:bb:cc:dd:40:00"), + MacAddress.fromString("ff:ff:ff:ff:f0:00"))); + + // match all 6 bytes + assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches( + MacAddress.fromString("aa:bb:cc:dd:ee:11"), + MacAddress.fromString("ff:ff:ff:ff:ff:ff"))); + + // match none of 6 bytes + assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches( + MacAddress.fromString("00:00:00:00:00:00"), + MacAddress.fromString("00:00:00:00:00:00"))); + } + /** * Tests that link-local address generation from MAC is valid. */ diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java index a028a54a5b74..f2f258a737e3 100644 --- a/tests/net/java/com/android/server/ConnectivityServiceTest.java +++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java @@ -184,6 +184,7 @@ import android.util.Log; import android.util.SparseArray; import androidx.test.InstrumentationRegistry; +import androidx.test.filters.FlakyTest; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -2259,9 +2260,8 @@ public class ConnectivityServiceTest { // If user accepted partial connectivity network before, // NetworkMonitor#setAcceptPartialConnectivity() will be called in // ConnectivityService#updateNetworkInfo(). - waitForIdle(); - verify(mWiFiNetworkAgent.mNetworkMonitor, times(1)).setAcceptPartialConnectivity(); callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + verify(mWiFiNetworkAgent.mNetworkMonitor, times(1)).setAcceptPartialConnectivity(); callback.expectCallback(CallbackRecord.LOSING, mCellNetworkAgent); nc = callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); assertFalse(nc.hasCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY)); @@ -2281,9 +2281,8 @@ public class ConnectivityServiceTest { // If user accepted partial connectivity network before, // NetworkMonitor#setAcceptPartialConnectivity() will be called in // ConnectivityService#updateNetworkInfo(). - waitForIdle(); - verify(mWiFiNetworkAgent.mNetworkMonitor, times(1)).setAcceptPartialConnectivity(); callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + verify(mWiFiNetworkAgent.mNetworkMonitor, times(1)).setAcceptPartialConnectivity(); callback.expectCallback(CallbackRecord.LOSING, mCellNetworkAgent); assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); callback.expectCapabilitiesWith(NET_CAPABILITY_PARTIAL_CONNECTIVITY, mWiFiNetworkAgent); @@ -2306,9 +2305,8 @@ public class ConnectivityServiceTest { // valid, because ConnectivityService calls setAcceptPartialConnectivity before it calls // notifyNetworkConnected. mWiFiNetworkAgent.connectWithPartialValidConnectivity(); - waitForIdle(); - verify(mWiFiNetworkAgent.mNetworkMonitor, times(1)).setAcceptPartialConnectivity(); callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + verify(mWiFiNetworkAgent.mNetworkMonitor, times(1)).setAcceptPartialConnectivity(); callback.expectCallback(CallbackRecord.LOSING, mCellNetworkAgent); callback.expectCapabilitiesWith( NET_CAPABILITY_PARTIAL_CONNECTIVITY | NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); @@ -3425,7 +3423,7 @@ public class ConnectivityServiceTest { testFactory.setScoreFilter(40); // Register the factory and expect it to receive the default request. - testFactory.expectAddRequestsWithScores(0); // default request score is 0, not served yet + testFactory.expectAddRequestsWithScores(0); testFactory.register(); SparseArray<NetworkRequest> requests = testFactory.waitForNetworkRequests(1); @@ -3633,6 +3631,7 @@ public class ConnectivityServiceTest { } @Test + @FlakyTest(bugId = 140305589) public void testPacketKeepalives() throws Exception { InetAddress myIPv4 = InetAddress.getByName("192.0.2.129"); InetAddress notMyIPv4 = InetAddress.getByName("192.0.2.35"); @@ -5692,26 +5691,40 @@ public class ConnectivityServiceTest { String[] values = tcpBufferSizes.split(","); String rmemValues = String.join(" ", values[0], values[1], values[2]); String wmemValues = String.join(" ", values[3], values[4], values[5]); - waitForIdle(); verify(mMockNetd, atLeastOnce()).setTcpRWmemorySize(rmemValues, wmemValues); reset(mMockNetd); } @Test + @FlakyTest(bugId = 140305678) public void testTcpBufferReset() throws Exception { final String testTcpBufferSizes = "1,2,3,4,5,6"; + final NetworkRequest networkRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_INTERNET) + .build(); + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + mCm.registerNetworkCallback(networkRequest, networkCallback); mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); reset(mMockNetd); // Switching default network updates TCP buffer sizes. mCellNetworkAgent.connect(false); + networkCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); verifyTcpBufferSizeChange(ConnectivityService.DEFAULT_TCP_BUFFER_SIZES); // Change link Properties should have updated tcp buffer size. LinkProperties lp = new LinkProperties(); lp.setTcpBufferSizes(testTcpBufferSizes); mCellNetworkAgent.sendLinkProperties(lp); + networkCallback.expectCallback(CallbackRecord.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); verifyTcpBufferSizeChange(testTcpBufferSizes); + + // Clean up. + mCellNetworkAgent.disconnect(); + networkCallback.expectCallback(CallbackRecord.LOST, mCellNetworkAgent); + networkCallback.assertNoCallback(); + mCm.unregisterNetworkCallback(networkCallback); } @Test diff --git a/tests/net/java/com/android/server/IpSecServiceParameterizedTest.java b/tests/net/java/com/android/server/IpSecServiceParameterizedTest.java index 7c40adfac002..71b72b84de81 100644 --- a/tests/net/java/com/android/server/IpSecServiceParameterizedTest.java +++ b/tests/net/java/com/android/server/IpSecServiceParameterizedTest.java @@ -32,6 +32,7 @@ import static org.mockito.Mockito.when; import android.app.AppOpsManager; import android.content.Context; +import android.content.pm.PackageManager; import android.net.INetd; import android.net.IpSecAlgorithm; import android.net.IpSecConfig; @@ -57,6 +58,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import java.net.Inet4Address; import java.net.Socket; import java.util.Arrays; import java.util.Collection; @@ -119,6 +121,11 @@ public class IpSecServiceParameterizedTest { } @Override + public PackageManager getPackageManager() { + return mMockPkgMgr; + } + + @Override public void enforceCallingOrSelfPermission(String permission, String message) { if (permission == android.Manifest.permission.MANAGE_IPSEC_TUNNELS) { return; @@ -128,6 +135,7 @@ public class IpSecServiceParameterizedTest { }; INetd mMockNetd; + PackageManager mMockPkgMgr; IpSecService.IpSecServiceConfiguration mMockIpSecSrvConfig; IpSecService mIpSecService; Network fakeNetwork = new Network(0xAB); @@ -152,11 +160,16 @@ public class IpSecServiceParameterizedTest { @Before public void setUp() throws Exception { mMockNetd = mock(INetd.class); + mMockPkgMgr = mock(PackageManager.class); mMockIpSecSrvConfig = mock(IpSecService.IpSecServiceConfiguration.class); mIpSecService = new IpSecService(mMockContext, mMockIpSecSrvConfig); // Injecting mock netd when(mMockIpSecSrvConfig.getNetdInstance()).thenReturn(mMockNetd); + + // PackageManager should always return true (feature flag tests in IpSecServiceTest) + when(mMockPkgMgr.hasSystemFeature(anyString())).thenReturn(true); + // A package granted the AppOp for MANAGE_IPSEC_TUNNELS will be MODE_ALLOWED. when(mMockAppOps.noteOp(anyInt(), anyInt(), eq("blessedPackage"))) .thenReturn(AppOpsManager.MODE_ALLOWED); @@ -709,4 +722,18 @@ public class IpSecServiceParameterizedTest { } catch (SecurityException expected) { } } + + @Test + public void testFeatureFlagVerification() throws Exception { + when(mMockPkgMgr.hasSystemFeature(eq(PackageManager.FEATURE_IPSEC_TUNNELS))) + .thenReturn(false); + + try { + String addr = Inet4Address.getLoopbackAddress().getHostAddress(); + mIpSecService.createTunnelInterface( + addr, addr, new Network(0), new Binder(), "blessedPackage"); + fail("Expected UnsupportedOperationException for disabled feature"); + } catch (UnsupportedOperationException expected) { + } + } } diff --git a/tests/net/java/com/android/server/NetIdManagerTest.kt b/tests/net/java/com/android/server/NetIdManagerTest.kt new file mode 100644 index 000000000000..045f89f85e3b --- /dev/null +++ b/tests/net/java/com/android/server/NetIdManagerTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server + +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.server.NetIdManager.MIN_NET_ID +import com.android.testutils.ExceptionUtils.ThrowingRunnable +import com.android.testutils.assertThrows +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.test.assertEquals + +@RunWith(AndroidJUnit4::class) +@SmallTest +class NetIdManagerTest { + @Test + fun testReserveReleaseNetId() { + val manager = NetIdManager(MIN_NET_ID + 4) + assertEquals(MIN_NET_ID, manager.reserveNetId()) + assertEquals(MIN_NET_ID + 1, manager.reserveNetId()) + assertEquals(MIN_NET_ID + 2, manager.reserveNetId()) + assertEquals(MIN_NET_ID + 3, manager.reserveNetId()) + + manager.releaseNetId(MIN_NET_ID + 1) + manager.releaseNetId(MIN_NET_ID + 3) + // IDs only loop once there is no higher ID available + assertEquals(MIN_NET_ID + 4, manager.reserveNetId()) + assertEquals(MIN_NET_ID + 1, manager.reserveNetId()) + assertEquals(MIN_NET_ID + 3, manager.reserveNetId()) + assertThrows(IllegalStateException::class.java, ThrowingRunnable { manager.reserveNetId() }) + manager.releaseNetId(MIN_NET_ID + 5) + // Still no ID available: MIN_NET_ID + 5 was not reserved + assertThrows(IllegalStateException::class.java, ThrowingRunnable { manager.reserveNetId() }) + manager.releaseNetId(MIN_NET_ID + 2) + // Throwing an exception still leaves the manager in a working state + assertEquals(MIN_NET_ID + 2, manager.reserveNetId()) + } +}
\ No newline at end of file diff --git a/tests/permission/src/com/android/framework/permission/tests/ActivityManagerPermissionTests.java b/tests/permission/src/com/android/framework/permission/tests/ActivityManagerPermissionTests.java index 89bd8d8f1dd4..5fb23b0ad507 100644 --- a/tests/permission/src/com/android/framework/permission/tests/ActivityManagerPermissionTests.java +++ b/tests/permission/src/com/android/framework/permission/tests/ActivityManagerPermissionTests.java @@ -17,6 +17,7 @@ package com.android.framework.permission.tests; import android.app.ActivityManager; +import android.app.ActivityTaskManager; import android.app.IActivityManager; import android.content.res.Configuration; import android.os.RemoteException; @@ -39,17 +40,7 @@ public class ActivityManagerPermissionTests extends TestCase { @SmallTest public void testREORDER_TASKS() { try { - mAm.moveTaskToFront(0, 0, null); - fail("IActivityManager.moveTaskToFront did not throw SecurityException as" - + " expected"); - } catch (SecurityException e) { - // expected - } catch (RemoteException e) { - fail("Unexpected remote exception"); - } - - try { - mAm.moveTaskBackwards(-1); + mAm.moveTaskToFront(null, null, 0, 0, null); fail("IActivityManager.moveTaskToFront did not throw SecurityException as" + " expected"); } catch (SecurityException e) { @@ -62,7 +53,7 @@ public class ActivityManagerPermissionTests extends TestCase { @SmallTest public void testCHANGE_CONFIGURATION() { try { - mAm.updateConfiguration(new Configuration()); + ActivityTaskManager.getService().updateConfiguration(new Configuration()); fail("IActivityManager.updateConfiguration did not throw SecurityException as" + " expected"); } catch (SecurityException e) { diff --git a/tests/permission/src/com/android/framework/permission/tests/SmsManagerPermissionTest.java b/tests/permission/src/com/android/framework/permission/tests/SmsManagerPermissionTest.java index 273943fe2d2e..41727431bd5e 100644 --- a/tests/permission/src/com/android/framework/permission/tests/SmsManagerPermissionTest.java +++ b/tests/permission/src/com/android/framework/permission/tests/SmsManagerPermissionTest.java @@ -16,12 +16,12 @@ package com.android.framework.permission.tests; -import java.util.ArrayList; - import android.telephony.SmsManager; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; +import java.util.ArrayList; + /** * Verify that SmsManager apis cannot be called without required permissions. */ @@ -32,6 +32,10 @@ public class SmsManagerPermissionTest extends AndroidTestCase { private static final String DEST_NUMBER = "4567"; private static final String SRC_NUMBER = "1234"; + private static final int CELL_BROADCAST_MESSAGE_ID_START = 10; + private static final int CELL_BROADCAST_MESSAGE_ID_END = 20; + private static final int CELL_BROADCAST_GSM_RAN_TYPE = 0; + /** * Verify that SmsManager.sendTextMessage requires permissions. * <p>Tests Permission: @@ -82,4 +86,34 @@ public class SmsManagerPermissionTest extends AndroidTestCase { // expected } } + + /** + * Verify that SmsManager.enableCellBroadcastRange requires permissions. + * <p>Tests system permission: android.Manifest.permission.RECEIVE_EMERGENCY_BROADCAST + */ + @SmallTest + public void testEnableCellBroadcastRange() { + try { + SmsManager.getDefault().enableCellBroadcastRange(CELL_BROADCAST_MESSAGE_ID_START, + CELL_BROADCAST_MESSAGE_ID_END, CELL_BROADCAST_GSM_RAN_TYPE); + fail("SmsManager.sendDataMessage did not throw SecurityException as expected"); + } catch (SecurityException e) { + // expected + } + } + + /** + * Verify that SmsManager.disableCellBroadcastRange requires permissions. + * <p>Tests system permission: android.Manifest.permission.RECEIVE_EMERGENCY_BROADCAST + */ + @SmallTest + public void testDisableCellBroadcastRange() { + try { + SmsManager.getDefault().disableCellBroadcastRange(CELL_BROADCAST_MESSAGE_ID_START, + CELL_BROADCAST_MESSAGE_ID_END, CELL_BROADCAST_GSM_RAN_TYPE); + fail("SmsManager.sendDataMessage did not throw SecurityException as expected"); + } catch (SecurityException e) { + // expected + } + } } diff --git a/tests/permission/src/com/android/framework/permission/tests/VibratorServicePermissionTest.java b/tests/permission/src/com/android/framework/permission/tests/VibratorServicePermissionTest.java index 2757296f588f..388c7d03dff2 100644 --- a/tests/permission/src/com/android/framework/permission/tests/VibratorServicePermissionTest.java +++ b/tests/permission/src/com/android/framework/permission/tests/VibratorServicePermissionTest.java @@ -52,7 +52,7 @@ public class VibratorServicePermissionTest extends TestCase { final VibrationEffect effect = VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE); mVibratorService.vibrate(Process.myUid(), null, effect, AudioManager.STREAM_ALARM, - new Binder()); + "testVibrate", new Binder()); fail("vibrate did not throw SecurityException as expected"); } catch (SecurityException e) { // expected diff --git a/tests/permission/src/com/android/framework/permission/tests/WindowManagerPermissionTests.java b/tests/permission/src/com/android/framework/permission/tests/WindowManagerPermissionTests.java index 9c4da1f72e38..4c2a984f8198 100644 --- a/tests/permission/src/com/android/framework/permission/tests/WindowManagerPermissionTests.java +++ b/tests/permission/src/com/android/framework/permission/tests/WindowManagerPermissionTests.java @@ -16,15 +16,16 @@ package com.android.framework.permission.tests; -import android.content.res.Configuration; +import static android.view.Display.DEFAULT_DISPLAY; + import android.os.Binder; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.UserHandle; import android.test.suitebuilder.annotation.SmallTest; import android.view.IWindowManager; -import junit.framework.TestCase; -import static android.view.Display.DEFAULT_DISPLAY; +import junit.framework.TestCase; /** * TODO: Remove this. This is only a placeholder, need to implement this. @@ -72,27 +73,6 @@ public class WindowManagerPermissionTests extends TestCase { } try { - mWm.updateOrientationFromAppTokens(new Configuration(), - null /* freezeThisOneIfNeeded */, DEFAULT_DISPLAY); - fail("IWindowManager.updateOrientationFromAppTokens did not throw SecurityException as" - + " expected"); - } catch (SecurityException e) { - // expected - } catch (RemoteException e) { - fail("Unexpected remote exception"); - } - - try { - mWm.setFocusedApp(null, false); - fail("IWindowManager.setFocusedApp did not throw SecurityException as" - + " expected"); - } catch (SecurityException e) { - // expected - } catch (RemoteException e) { - fail("Unexpected remote exception"); - } - - try { mWm.prepareAppTransition(0, false); fail("IWindowManager.prepareAppTransition did not throw SecurityException as" + " expected"); @@ -117,7 +97,7 @@ public class WindowManagerPermissionTests extends TestCase { public void testDISABLE_KEYGUARD() { Binder token = new Binder(); try { - mWm.disableKeyguard(token, "foo"); + mWm.disableKeyguard(token, "foo", UserHandle.myUserId()); fail("IWindowManager.disableKeyguard did not throw SecurityException as" + " expected"); } catch (SecurityException e) { @@ -127,7 +107,7 @@ public class WindowManagerPermissionTests extends TestCase { } try { - mWm.reenableKeyguard(token); + mWm.reenableKeyguard(token, UserHandle.myUserId()); fail("IWindowManager.reenableKeyguard did not throw SecurityException as" + " expected"); } catch (SecurityException e) { diff --git a/tests/privapp-permissions/Android.bp b/tests/privapp-permissions/Android.bp index 066d4f94e896..ca7864f047c7 100644 --- a/tests/privapp-permissions/Android.bp +++ b/tests/privapp-permissions/Android.bp @@ -43,3 +43,19 @@ prebuilt_etc { sub_dir: "permissions", product_specific: true, } + +android_app { + name: "ProductServicesPrivAppPermissionTest", + sdk_version: "current", + privileged: true, + manifest: "product_services/AndroidManifest.xml", + product_services_specific: true, + required: ["product_servicesprivapp-permissions-test.xml"], +} + +prebuilt_etc { + name: "product_servicesprivapp-permissions-test.xml", + src: "product_services/privapp-permissions-test.xml", + sub_dir: "permissions", + product_services_specific: true, +} diff --git a/tests/privapp-permissions/product_services/AndroidManifest.xml b/tests/privapp-permissions/product_services/AndroidManifest.xml new file mode 100644 index 000000000000..511ddee729ca --- /dev/null +++ b/tests/privapp-permissions/product_services/AndroidManifest.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + * Copyright (C) 2018 Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.framework.permission.privapp.tests.product_services"> + + <!-- MANAGE_USB is signature|privileged --> + <uses-permission android:name="android.permission.MANAGE_USB"/> +</manifest> diff --git a/tests/privapp-permissions/product_services/privapp-permissions-test.xml b/tests/privapp-permissions/product_services/privapp-permissions-test.xml new file mode 100644 index 000000000000..43baebbb0aad --- /dev/null +++ b/tests/privapp-permissions/product_services/privapp-permissions-test.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<permissions> + <privapp-permissions package="com.android.framework.permission.privapp.tests.product_services"> + <permission name="android.permission.MANAGE_USB"/> + </privapp-permissions> +</permissions> diff --git a/tests/testables/src/android/testing/BaseFragmentTest.java b/tests/testables/src/android/testing/BaseFragmentTest.java index c76b93cbb577..6cd88b51cb82 100644 --- a/tests/testables/src/android/testing/BaseFragmentTest.java +++ b/tests/testables/src/android/testing/BaseFragmentTest.java @@ -21,7 +21,9 @@ import android.app.Fragment; import android.app.FragmentController; import android.app.FragmentHostCallback; import android.app.FragmentManagerNonConfig; +import android.content.Context; import android.graphics.PixelFormat; +import android.os.Bundle; import android.os.Handler; import android.os.Parcelable; import android.view.LayoutInflater; @@ -50,7 +52,7 @@ public abstract class BaseFragmentTest { private static final int VIEW_ID = 42; private final Class<? extends Fragment> mCls; - private Handler mHandler; + protected Handler mHandler; protected FrameLayout mView; protected FragmentController mFragments; protected Fragment mFragment; @@ -76,7 +78,7 @@ public abstract class BaseFragmentTest { TestableLooper.get(this).runWithLooper(() -> { mHandler = new Handler(); - mFragment = mCls.newInstance(); + mFragment = instantiate(mContext, mCls.getName(), null); mFragments = FragmentController.createController(new HostCallbacks()); mFragments.attachHost(null); mFragments.getFragmentManager().beginTransaction() @@ -188,6 +190,13 @@ public abstract class BaseFragmentTest { TestableLooper.get(this).processAllMessages(); } + /** + * Method available for override to replace fragment instantiation. + */ + protected Fragment instantiate(Context context, String className, @Nullable Bundle arguments) { + return Fragment.instantiate(context, className, arguments); + } + private View findViewById(int id) { return mView.findViewById(id); } @@ -207,6 +216,11 @@ public abstract class BaseFragmentTest { } @Override + public Fragment instantiate(Context context, String className, Bundle arguments) { + return BaseFragmentTest.this.instantiate(context, className, arguments); + } + + @Override public boolean onShouldSaveFragmentState(Fragment fragment) { return true; // True for now. } diff --git a/tests/testables/src/android/testing/TestableContext.java b/tests/testables/src/android/testing/TestableContext.java index cf84c7926549..e2668bc4281f 100644 --- a/tests/testables/src/android/testing/TestableContext.java +++ b/tests/testables/src/android/testing/TestableContext.java @@ -53,7 +53,7 @@ import org.junit.runners.model.Statement; * Like the following:</p> * <pre class="prettyprint"> * @Rule - * private final TestableContext mContext = new TestableContext(InstrumentationRegister.getContext()); + * public final TestableContext mContext = new TestableContext(InstrumentationRegister.getContext()); * </pre> */ public class TestableContext extends ContextWrapper implements TestRule { @@ -296,13 +296,13 @@ public class TestableContext extends ContextWrapper implements TestRule { @Override public void registerComponentCallbacks(ComponentCallbacks callback) { if (mComponent != null) mComponent.getLeakInfo(callback).addAllocation(new Throwable()); - super.registerComponentCallbacks(callback); + getBaseContext().registerComponentCallbacks(callback); } @Override public void unregisterComponentCallbacks(ComponentCallbacks callback) { if (mComponent != null) mComponent.getLeakInfo(callback).clearAllocations(); - super.unregisterComponentCallbacks(callback); + getBaseContext().unregisterComponentCallbacks(callback); } public TestablePermissions getTestablePermissions() { diff --git a/tests/testables/src/android/testing/TestableInstrumentation.java b/tests/testables/src/android/testing/TestableInstrumentation.java index f5862064aa6b..ed867c133141 100644 --- a/tests/testables/src/android/testing/TestableInstrumentation.java +++ b/tests/testables/src/android/testing/TestableInstrumentation.java @@ -39,22 +39,26 @@ public class TestableInstrumentation extends AndroidJUnitRunner { @Override public void onCreate(Bundle arguments) { - sManager = new MainLooperManager(); - Log.setWtfHandler((tag, what, system) -> { - if (system) { - Log.e(TAG, "WTF!!", what); - } else { - // These normally kill the app, but we don't want that in a test, instead we want - // it to throw. - throw new RuntimeException(what); - } - }); + if (TestableLooper.HOLD_MAIN_THREAD) { + sManager = new MainLooperManager(); + Log.setWtfHandler((tag, what, system) -> { + if (system) { + Log.e(TAG, "WTF!!", what); + } else { + // These normally kill the app, but we don't want that in a test, instead we want + // it to throw. + throw new RuntimeException(what); + } + }); + } super.onCreate(arguments); } @Override public void finish(int resultCode, Bundle results) { - sManager.destroy(); + if (TestableLooper.HOLD_MAIN_THREAD) { + sManager.destroy(); + } super.finish(resultCode, results); } diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java index cec8b4b49f25..8d99ac7100eb 100644 --- a/tests/testables/src/android/testing/TestableLooper.java +++ b/tests/testables/src/android/testing/TestableLooper.java @@ -40,6 +40,12 @@ import java.util.Map; */ public class TestableLooper { + /** + * Whether to hold onto the main thread through all tests in an attempt to + * catch crashes. + */ + public static final boolean HOLD_MAIN_THREAD = false; + private Looper mLooper; private MessageQueue mQueue; private MessageHandler mMessageHandler; @@ -78,7 +84,7 @@ public class TestableLooper { */ public void destroy() { mQueueWrapper.release(); - if (mLooper == Looper.getMainLooper()) { + if (HOLD_MAIN_THREAD && mLooper == Looper.getMainLooper()) { TestableInstrumentation.releaseMain(); } } @@ -200,7 +206,7 @@ public class TestableLooper { } private static TestLooperManager acquireLooperManager(Looper l) { - if (l == Looper.getMainLooper()) { + if (HOLD_MAIN_THREAD && l == Looper.getMainLooper()) { TestableInstrumentation.acquireMain(); } return InstrumentationRegistry.getInstrumentation().acquireLooperManager(l); @@ -292,7 +298,7 @@ public class TestableLooper { if (set) { mTestableLooper.mQueueWrapper.release(); mTestableLooper.mQueueWrapper = null; - if (mLooper == Looper.getMainLooper()) { + if (HOLD_MAIN_THREAD && mLooper == Looper.getMainLooper()) { TestableInstrumentation.releaseMain(); } } diff --git a/tests/utils/testutils/java/android/view/test/InsetsModeSession.java b/tests/utils/testutils/java/android/view/test/InsetsModeSession.java new file mode 100644 index 000000000000..c83dfa41d260 --- /dev/null +++ b/tests/utils/testutils/java/android/view/test/InsetsModeSession.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.test; + +import android.view.ViewRootImpl; + +/** + * Session to set insets mode for {@link ViewRootImpl#sNewInsetsMode}. + */ +public class InsetsModeSession implements AutoCloseable { + + private int mOldMode; + + public InsetsModeSession(int flag) { + mOldMode = ViewRootImpl.sNewInsetsMode; + ViewRootImpl.sNewInsetsMode = flag; + } + + @Override + public void close() throws Exception { + ViewRootImpl.sNewInsetsMode = mOldMode; + } +} diff --git a/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannelServer.java b/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannelServer.java index 49c833228b6c..ecf271601b6b 100644 --- a/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannelServer.java +++ b/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannelServer.java @@ -27,6 +27,7 @@ import com.android.internal.util.AsyncChannel; import java.util.HashMap; import java.util.Map; +import java.util.Set; /** * Provides an interface for the server side implementation of a bidirectional channel as described @@ -83,4 +84,8 @@ public class BidirectionalAsyncChannelServer { return mMessenger; } + public Set<Messenger> getClientMessengers() { + return mClients.keySet(); + } + } diff --git a/tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java b/tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java index 2ce1fc68e1ce..c9e3404e0f1a 100644 --- a/tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java +++ b/tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java @@ -34,30 +34,16 @@ import com.android.test.filters.SelectTest; public final class FrameworksTestsFilter extends SelectTest { private static final String[] SELECTED_TESTS = { + // Test specifications for FrameworksMockingCoreTests. + "android.app.activity.ActivityThreadClientTest", // Test specifications for FrameworksCoreTests. "android.app.servertransaction.", // all tests under the package. "android.view.DisplayCutoutTest", - // Test specifications for FrameworksServicesTests. - "com.android.server.policy.", // all tests under the package. - "com.android.server.am.ActivityLaunchParamsModifierTests", - "com.android.server.am.ActivityRecordTests", - "com.android.server.am.ActivityStackSupervisorTests", - "com.android.server.am.ActivityStackTests", - "com.android.server.am.ActivityStartControllerTests", - "com.android.server.am.ActivityStarterTests", - "com.android.server.am.ActivityStartInterceptorTest", - "com.android.server.am.AssistDataRequesterTest", - "com.android.server.am.ClientLifecycleManagerTests", - "com.android.server.am.LaunchParamsControllerTests", - "com.android.server.am.PendingRemoteAnimationRegistryTest", - "com.android.server.am.RecentsAnimationTest", - "com.android.server.am.RecentTasksTest", - "com.android.server.am.RunningTasksTest", - "com.android.server.am.SafeActivityOptionsTest", - "com.android.server.am.TaskLaunchParamsModifierTests", - "com.android.server.am.TaskPersisterTest", - "com.android.server.am.TaskRecordTests", - "com.android.server.am.TaskStackChangedListenerTest", + "android.view.InsetsAnimationControlImplTest", + "android.view.InsetsControllerTest", + "android.view.InsetsSourceTest", + "android.view.InsetsSourceConsumerTest", + "android.view.InsetsStateTest", }; public FrameworksTestsFilter(Bundle testArgs) { |