diff options
Diffstat (limited to 'tests')
254 files changed, 15172 insertions, 1218 deletions
diff --git a/tests/ActivityManagerPerfTests/stub-app/Android.bp b/tests/ActivityManagerPerfTests/stub-app/Android.bp new file mode 100644 index 000000000000..a3c1f5b2f17d --- /dev/null +++ b/tests/ActivityManagerPerfTests/stub-app/Android.bp @@ -0,0 +1,68 @@ +// 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: "ActivityManagerPerfTestsStubApp1", + static_libs: ["ActivityManagerPerfTestsUtils"], + srcs: [ + "src/**/*.java", + ], + resource_dirs: [ + "app1/res", + "res", + ], + platform_apis: true, + certificate: "platform", + aaptflags: [ + "--rename-manifest-package com.android.stubs.am1", + "--auto-add-overlay", + ], +} + +android_test_helper_app { + name: "ActivityManagerPerfTestsStubApp2", + static_libs: ["ActivityManagerPerfTestsUtils"], + srcs: [ + "src/**/*.java", + ], + resource_dirs: [ + "app2/res", + "res", + ], + platform_apis: true, + certificate: "platform", + aaptflags: [ + "--rename-manifest-package com.android.stubs.am2", + "--auto-add-overlay", + ], +} + +android_test_helper_app { + name: "ActivityManagerPerfTestsStubApp3", + static_libs: ["ActivityManagerPerfTestsUtils"], + srcs: [ + "src/**/*.java", + ], + resource_dirs: [ + "app3/res", + "res", + ], + platform_apis: true, + certificate: "platform", + aaptflags: [ + "--rename-manifest-package com.android.stubs.am3", + "--auto-add-overlay", + ], +} + diff --git a/tests/ActivityManagerPerfTests/stub-app/AndroidManifest.xml b/tests/ActivityManagerPerfTests/stub-app/AndroidManifest.xml new file mode 100644 index 000000000000..a57f64c320c8 --- /dev/null +++ b/tests/ActivityManagerPerfTests/stub-app/AndroidManifest.xml @@ -0,0 +1,54 @@ +<?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.stubs.am"> + + <uses-permission android:name="android.permission.START_ACTIVITIES_FROM_BACKGROUND"/> + <application android:label="Android TestCase" > + <provider + android:authorities="@string/authority" + android:name=".TestContentProvider" + android:exported="true" /> + <receiver + android:name=".TestBroadcastReceiver" + android:exported="true"> + <intent-filter> + <action android:name="com.android.stubs.am.ACTION_BROADCAST_TEST" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </receiver> + <service + android:name=".InitService" + android:exported="true" /> + <service + android:name=".TestService" + android:exported="true" /> + <activity + android:name=".TestActivity" + android:excludeFromRecents="true" + android:turnScreenOn="true" + android:launchMode="singleTask"> + <intent-filter> + <action android:name="com.android.stubs.am.ACTION_START_TEST_ACTIVITY" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </activity> + </application> + +</manifest> + diff --git a/tests/ActivityManagerPerfTests/stub-app/app1/res/values/config.xml b/tests/ActivityManagerPerfTests/stub-app/app1/res/values/config.xml new file mode 100644 index 000000000000..667472db5f83 --- /dev/null +++ b/tests/ActivityManagerPerfTests/stub-app/app1/res/values/config.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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="authority" translatable="false">com.android.stubs.am1.testapp</string> +</resources> diff --git a/tests/ActivityManagerPerfTests/stub-app/app2/res/values/config.xml b/tests/ActivityManagerPerfTests/stub-app/app2/res/values/config.xml new file mode 100644 index 000000000000..085273574d95 --- /dev/null +++ b/tests/ActivityManagerPerfTests/stub-app/app2/res/values/config.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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="authority" translatable="false">com.android.stubs.am2.testapp</string> +</resources> diff --git a/tests/ActivityManagerPerfTests/stub-app/app3/res/values/config.xml b/tests/ActivityManagerPerfTests/stub-app/app3/res/values/config.xml new file mode 100644 index 000000000000..6895d7258dad --- /dev/null +++ b/tests/ActivityManagerPerfTests/stub-app/app3/res/values/config.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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="authority" translatable="false">com.android.stubs.am3.testapp</string> +</resources> diff --git a/tests/ActivityManagerPerfTests/stub-app/res/layout/activity_content.xml b/tests/ActivityManagerPerfTests/stub-app/res/layout/activity_content.xml new file mode 100644 index 000000000000..f79f006087d7 --- /dev/null +++ b/tests/ActivityManagerPerfTests/stub-app/res/layout/activity_content.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. +--> + +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/content" + android:layout_width="match_parent" + android:layout_height="match_parent"/> diff --git a/tests/ActivityManagerPerfTests/stub-app/src/com/android/stubs/am/InitService.java b/tests/ActivityManagerPerfTests/stub-app/src/com/android/stubs/am/InitService.java new file mode 100644 index 000000000000..18fdc442bfbf --- /dev/null +++ b/tests/ActivityManagerPerfTests/stub-app/src/com/android/stubs/am/InitService.java @@ -0,0 +1,301 @@ +/* + * 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.stubs.am; + +import static com.android.frameworks.perftests.am.util.Constants.COMMAND_ACQUIRE_CONTENT_PROVIDER; +import static com.android.frameworks.perftests.am.util.Constants.COMMAND_BIND_SERVICE; +import static com.android.frameworks.perftests.am.util.Constants.COMMAND_RELEASE_CONTENT_PROVIDER; +import static com.android.frameworks.perftests.am.util.Constants.COMMAND_SEND_BROADCAST; +import static com.android.frameworks.perftests.am.util.Constants.COMMAND_START_ACTIVITY; +import static com.android.frameworks.perftests.am.util.Constants.COMMAND_STOP_ACTIVITY; +import static com.android.frameworks.perftests.am.util.Constants.COMMAND_UNBIND_SERVICE; + +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.IContentProvider; +import android.content.Intent; +import android.content.ServiceConnection; +import android.net.Uri; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.Log; + +import com.android.frameworks.perftests.am.util.Constants; +import com.android.frameworks.perftests.am.util.ICommandReceiver; + +public class InitService extends Service { + private static final String TAG = "InitService"; + public static final boolean VERBOSE = false; + + private static class Stub extends ICommandReceiver.Stub { + private final Context mContext; + private final Messenger mCallback; + private final Handler mHandler; + private final Messenger mMessenger; + final ArrayMap<String, MyServiceConnection> mServices = + new ArrayMap<String, MyServiceConnection>(); + final ArrayMap<Uri, IContentProvider> mProviders = + new ArrayMap<Uri, IContentProvider>(); + + Stub(Context context, Messenger callback) { + mContext = context; + mCallback = callback; + HandlerThread thread = new HandlerThread("result handler"); + thread.start(); + mHandler = new H(thread.getLooper()); + mMessenger = new Messenger(mHandler); + } + + private class H extends Handler { + H(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + if (msg.what == Constants.MSG_DEFAULT) { + if (VERBOSE) { + Log.i(TAG, "H: received seq=" + msg.arg1 + ", result=" + msg.arg2); + } + sendResult(mCallback, Constants.REPLY_COMMAND_RESULT, msg.arg1, msg.arg2, null); + } else if (msg.what == Constants.MSG_UNBIND_DONE) { + if (VERBOSE) { + Log.i(TAG, "H: received unbind=" + msg.obj); + } + synchronized (InitService.sStub) { + Bundle b = (Bundle) msg.obj; + String pkg = b.getString(Constants.EXTRA_SOURCE_PACKAGE, ""); + MyServiceConnection c = mServices.remove(pkg); + if (c != null) { + sendResult(mCallback, Constants.REPLY_COMMAND_RESULT, c.mSeq, + Constants.RESULT_NO_ERROR, null); + } + } + } + } + } + + @Override + public void sendCommand(int command, int seq, String sourcePackage, String targetPackage, + int flags, Bundle bundle) { + if (VERBOSE) { + Log.i(TAG, "Received command=" + command + ", seq=" + seq + ", from=" + + sourcePackage + ", to=" + targetPackage + ", flags=" + flags); + } + switch (command) { + case COMMAND_BIND_SERVICE: + handleBindService(seq, targetPackage, flags, bundle); + break; + case COMMAND_UNBIND_SERVICE: + handleUnbindService(seq, targetPackage); + break; + case COMMAND_ACQUIRE_CONTENT_PROVIDER: + acquireProvider(seq, bundle.getParcelable(Constants.EXTRA_URI)); + break; + case COMMAND_RELEASE_CONTENT_PROVIDER: + releaseProvider(seq, bundle.getParcelable(Constants.EXTRA_URI)); + break; + case COMMAND_SEND_BROADCAST: + sendBroadcast(seq, targetPackage); + break; + case COMMAND_START_ACTIVITY: + startActivity(seq, targetPackage); + break; + case COMMAND_STOP_ACTIVITY: + stopActivity(seq, targetPackage); + break; + } + } + + private void handleBindService(int seq, String targetPackage, int flags, Bundle bundle) { + Intent intent = new Intent(); + intent.setClassName(targetPackage, "com.android.stubs.am.TestService"); + intent.putExtra(Constants.EXTRA_RECEIVER_CALLBACK, mMessenger); + if (bundle != null) { + intent.putExtras(bundle); + } + synchronized (this) { + if (!mServices.containsKey(targetPackage)) { + MyServiceConnection c = new MyServiceConnection(mCallback); + c.mSeq = seq; + if (!mContext.bindService(intent, c, flags)) { + Log.e(TAG, "Unable to bind to service in " + targetPackage); + sendResult(mCallback, Constants.REPLY_COMMAND_RESULT, seq, + Constants.RESULT_ERROR, null); + } else { + if (VERBOSE) { + Log.i(TAG, "Bind to service " + intent); + } + mServices.put(targetPackage, c); + } + } else { + sendResult(mCallback, Constants.REPLY_COMMAND_RESULT, seq, + Constants.RESULT_NO_ERROR, null); + } + } + } + + private void handleUnbindService(int seq, String target) { + MyServiceConnection c = null; + synchronized (this) { + c = mServices.get(target); + } + if (c != null) { + c.mSeq = seq; + mContext.unbindService(c); + } + } + + private void acquireProvider(int seq, Uri uri) { + ContentResolver resolver = mContext.getContentResolver(); + IContentProvider provider = resolver.acquireProvider(uri); + if (provider != null) { + synchronized (mProviders) { + mProviders.put(uri, provider); + } + sendResult(mCallback, Constants.REPLY_COMMAND_RESULT, seq, + Constants.RESULT_NO_ERROR, null); + } else { + sendResult(mCallback, Constants.REPLY_COMMAND_RESULT, seq, + Constants.RESULT_ERROR, null); + } + } + + private void releaseProvider(int seq, Uri uri) { + ContentResolver resolver = mContext.getContentResolver(); + IContentProvider provider; + synchronized (mProviders) { + provider = mProviders.get(uri); + } + if (provider != null) { + resolver.releaseProvider(provider); + synchronized (mProviders) { + mProviders.remove(uri); + } + } + sendResult(mCallback, Constants.REPLY_COMMAND_RESULT, seq, + Constants.RESULT_NO_ERROR, null); + } + + private void sendBroadcast(final int seq, String targetPackage) { + Intent intent = new Intent(Constants.STUB_ACTION_BROADCAST); + intent.setPackage(targetPackage); + mContext.sendOrderedBroadcast(intent, null, new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + sendResult(mCallback, Constants.REPLY_COMMAND_RESULT, seq, + Constants.RESULT_NO_ERROR, null); + } + }, null, 0, null, null); + } + + private void startActivity(int seq, String targetPackage) { + Intent intent = new Intent(Constants.STUB_ACTION_ACTIVITY); + intent.setClassName(targetPackage, "com.android.stubs.am.TestActivity"); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); + intent.putExtra(Constants.EXTRA_ARG1, seq); + intent.putExtra(Constants.EXTRA_RECEIVER_CALLBACK, mMessenger); + mContext.startActivity(intent); + } + + private void stopActivity(int seq, String targetPackage) { + Intent intent = new Intent(Constants.STUB_ACTION_ACTIVITY); + intent.setClassName(targetPackage, "com.android.stubs.am.TestActivity"); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); + intent.putExtra(Constants.EXTRA_REQ_FINISH_ACTIVITY, true); + intent.putExtra(Constants.EXTRA_ARG1, seq); + intent.putExtra(Constants.EXTRA_RECEIVER_CALLBACK, mMessenger); + mContext.startActivity(intent); + } + }; + + private static void sendResult(Messenger callback, int what, int seq, int result, Object obj) { + Message msg = Message.obtain(); + msg.what = what; + msg.arg1 = seq; + msg.arg2 = result; + msg.obj = obj; + try { + if (VERBOSE) { + Log.i(TAG, "Sending result seq=" + seq + ", result=" + result); + } + callback.send(msg); + } catch (RemoteException e) { + Log.e(TAG, "Error in sending result back", e); + } + msg.recycle(); + } + + private static class MyServiceConnection implements ServiceConnection { + private Messenger mCallback; + int mSeq; + + MyServiceConnection(Messenger callback) { + mCallback = callback; + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + sendResult(mCallback, Constants.REPLY_COMMAND_RESULT, mSeq, + Constants.RESULT_NO_ERROR, null); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + synchronized (sStub) { + MyServiceConnection c = sStub.mServices.remove(name.getPackageName()); + if (c != null) { + sendResult(mCallback, Constants.REPLY_COMMAND_RESULT, c.mSeq, + Constants.RESULT_NO_ERROR, null); + } + } + } + } + + private static Stub sStub = null; + + @Override + public IBinder onBind(Intent intent) { + return new Binder(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Messenger callback = intent.getParcelableExtra(Constants.EXTRA_RECEIVER_CALLBACK); + if (sStub == null) { + sStub = new Stub(getApplicationContext(), callback); + } + + Bundle extras = new Bundle(); + extras.putString(Constants.EXTRA_SOURCE_PACKAGE, getPackageName()); + extras.putBinder(Constants.EXTRA_RECEIVER_CALLBACK, sStub); + sendResult(callback, Constants.REPLY_PACKAGE_START_RESULT, + intent.getIntExtra(Constants.EXTRA_SEQ, -1), 0, extras); + return START_NOT_STICKY; + } +} diff --git a/tests/ActivityManagerPerfTests/stub-app/src/com/android/stubs/am/TestActivity.java b/tests/ActivityManagerPerfTests/stub-app/src/com/android/stubs/am/TestActivity.java new file mode 100644 index 000000000000..f7ea35672f0a --- /dev/null +++ b/tests/ActivityManagerPerfTests/stub-app/src/com/android/stubs/am/TestActivity.java @@ -0,0 +1,86 @@ +/* + * 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.stubs.am; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.util.Log; + +import com.android.frameworks.perftests.am.util.Constants; + +public class TestActivity extends Activity { + private static final String TAG = "TestActivity"; + private static final boolean VERBOSE = InitService.VERBOSE; + + private Messenger mResultTo; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + if (VERBOSE) { + Log.i(TAG, getPackageName() + " onCreate()"); + } + setContentView(R.layout.activity_content); + mResultTo = getIntent().getParcelableExtra(Constants.EXTRA_RECEIVER_CALLBACK); + } + + @Override + public void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + mResultTo = intent.getParcelableExtra(Constants.EXTRA_RECEIVER_CALLBACK); + if (intent.getBooleanExtra(Constants.EXTRA_REQ_FINISH_ACTIVITY, false)) { + if (VERBOSE) { + Log.i(TAG, getPackageName() + " finishing activity"); + } + finish(); + } + } + + @Override + protected void onResume() { + super.onResume(); + if (VERBOSE) { + Log.i(TAG, getPackageName() + " onResume()"); + } + sendResult(Constants.RESULT_NO_ERROR); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (VERBOSE) { + Log.i(TAG, getPackageName() + " onDestroy()"); + } + sendResult(Constants.RESULT_NO_ERROR); + } + + private void sendResult(int result) { + Message msg = Message.obtain(); + msg.arg1 = getIntent().getIntExtra(Constants.EXTRA_ARG1, -1); + msg.arg2 = result; + try { + mResultTo.send(msg); + } catch (RemoteException e) { + Log.e(TAG, "Error in sending result back", e); + } + } +} diff --git a/tests/ActivityManagerPerfTests/stub-app/src/com/android/stubs/am/TestBroadcastReceiver.java b/tests/ActivityManagerPerfTests/stub-app/src/com/android/stubs/am/TestBroadcastReceiver.java new file mode 100644 index 000000000000..36c7a0a3a405 --- /dev/null +++ b/tests/ActivityManagerPerfTests/stub-app/src/com/android/stubs/am/TestBroadcastReceiver.java @@ -0,0 +1,32 @@ +/* + * 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.stubs.am; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +public class TestBroadcastReceiver extends BroadcastReceiver { + private static final String TAG = "TestBroadcastReceiver"; + + @Override + public void onReceive(Context context, Intent intent) { + Log.i(TAG, context.getPackageName() + " received broadcast: " + intent); + } +} diff --git a/tests/ActivityManagerPerfTests/stub-app/src/com/android/stubs/am/TestContentProvider.java b/tests/ActivityManagerPerfTests/stub-app/src/com/android/stubs/am/TestContentProvider.java new file mode 100644 index 000000000000..4fdbf1f29036 --- /dev/null +++ b/tests/ActivityManagerPerfTests/stub-app/src/com/android/stubs/am/TestContentProvider.java @@ -0,0 +1,62 @@ +/* + * 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.stubs.am; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.util.Log; + +public class TestContentProvider extends ContentProvider { + private static final String TAG = "TestContentProvider"; + private static final boolean VERBOSE = InitService.VERBOSE; + + @Override + public Uri insert(Uri uri, ContentValues values) { + return null; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + return null; + } + + @Override + public String getType(Uri uri) { + return null; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + return 0; + } + + @Override + public boolean onCreate() { + if (VERBOSE) { + Log.i(TAG, getContext().getPackageName() + " onCreate()"); + } + return false; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + return 0; + } +} diff --git a/tests/ActivityManagerPerfTests/stub-app/src/com/android/stubs/am/TestService.java b/tests/ActivityManagerPerfTests/stub-app/src/com/android/stubs/am/TestService.java new file mode 100644 index 000000000000..ba220e003203 --- /dev/null +++ b/tests/ActivityManagerPerfTests/stub-app/src/com/android/stubs/am/TestService.java @@ -0,0 +1,71 @@ +/* + * 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.stubs.am; + +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.util.Log; + +import com.android.frameworks.perftests.am.util.Constants; + +public class TestService extends Service { + private static final String TAG = "TestService"; + private static final boolean VERBOSE = InitService.VERBOSE; + + private Binder mStub = new Binder(); + + @Override + public IBinder onBind(Intent intent) { + if (VERBOSE) { + Log.i(TAG, getPackageName() + " onBind()"); + } + return mStub; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (VERBOSE) { + Log.i(TAG, getPackageName() + " onStartCommand()"); + } + return START_NOT_STICKY; + } + + @Override + public boolean onUnbind(Intent intent) { + if (VERBOSE) { + Log.i(TAG, getPackageName() + " onUnbind()"); + } + Messenger messenger = intent.getParcelableExtra(Constants.EXTRA_RECEIVER_CALLBACK); + Message msg = Message.obtain(); + msg.what = Constants.MSG_UNBIND_DONE; + Bundle b = new Bundle(); + b.putString(Constants.EXTRA_SOURCE_PACKAGE, getPackageName()); + msg.obj = b; + try { + messenger.send(msg); + } catch (RemoteException e) { + Log.e(TAG, "Error in sending result back", e); + } + return false; + } +} diff --git a/tests/ActivityManagerPerfTests/tests/AndroidTest.xml b/tests/ActivityManagerPerfTests/tests/AndroidTest.xml index 76c40b2e3dc6..475bb82a9856 100644 --- a/tests/ActivityManagerPerfTests/tests/AndroidTest.xml +++ b/tests/ActivityManagerPerfTests/tests/AndroidTest.xml @@ -17,6 +17,9 @@ <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> <option name="test-file-name" value="ActivityManagerPerfTests.apk"/> <option name="test-file-name" value="ActivityManagerPerfTestsTestApp.apk"/> + <option name="test-file-name" value="ActivityManagerPerfTestsStubApp1.apk"/> + <option name="test-file-name" value="ActivityManagerPerfTestsStubApp2.apk"/> + <option name="test-file-name" value="ActivityManagerPerfTestsStubApp3.apk"/> <option name="cleanup-apks" value="true"/> </target_preparer> @@ -26,4 +29,4 @@ <option name="package" value="com.android.frameworks.perftests.amtests"/> <option name="runner" value="androidx.test.runner.AndroidJUnitRunner"/> </test> -</configuration>
\ No newline at end of file +</configuration> diff --git a/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/tests/OomAdjPerfTest.java b/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/tests/OomAdjPerfTest.java new file mode 100644 index 000000000000..1d3ff06e6bf1 --- /dev/null +++ b/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/tests/OomAdjPerfTest.java @@ -0,0 +1,241 @@ +/* + * 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.frameworks.perftests.am.tests; + + +import android.content.ContentResolver; +import android.content.Context; +import android.net.Uri; +import android.os.HandlerThread; +import android.perftests.utils.ManualBenchmarkState; +import android.perftests.utils.PerfManualStatusReporter; +import android.perftests.utils.TraceMarkParser; +import android.perftests.utils.TraceMarkParser.TraceMarkLine; +import android.perftests.utils.TraceMarkParser.TraceMarkSlice; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.LargeTest; + +import com.android.frameworks.perftests.am.util.AtraceUtils; +import com.android.frameworks.perftests.am.util.TargetPackageUtils; +import com.android.frameworks.perftests.am.util.Utils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.ArrayList; +import java.util.List; + +/** + * This benchmark test basically manipulates 3 test packages, let them bind to + * each other, send broadcast to each other, etc. All of these actions essentially + * triggers OomAdjuster to update the oom_adj scores and proc state of them. + * In the meanwhile it'll also monitor the atrace output, extract duration between + * the start and exit entries of the updateOomAdjLocked, include each of them + * into the stats; when there are enough samples in the stats, the test will + * stop and output the mean/stddev time spent on the updateOomAdjLocked. + */ +@RunWith(JUnit4.class) +@LargeTest +public final class OomAdjPerfTest { + private static final String TAG = "OomAdjPerfTest"; + private static final boolean VERBOSE = true; + + private static final String STUB_PACKAGE1_NAME = "com.android.stubs.am1"; + private static final String STUB_PACKAGE2_NAME = "com.android.stubs.am2"; + private static final String STUB_PACKAGE3_NAME = "com.android.stubs.am3"; + + private static final Uri STUB_PACKAGE1_URI = new Uri.Builder().scheme( + ContentResolver.SCHEME_CONTENT).authority("com.android.stubs.am1.testapp").build(); + private static final Uri STUB_PACKAGE2_URI = new Uri.Builder().scheme( + ContentResolver.SCHEME_CONTENT).authority("com.android.stubs.am2.testapp").build(); + private static final Uri STUB_PACKAGE3_URI = new Uri.Builder().scheme( + ContentResolver.SCHEME_CONTENT).authority("com.android.stubs.am3.testapp").build(); + private static final long NANOS_PER_MICROSECOND = 1000L; + + private static final String ATRACE_CATEGORY = "am"; + private static final String ATRACE_OOMADJ_PREFIX = "updateOomAdj_"; + + @Rule + public PerfManualStatusReporter mPerfManualStatusReporter = new PerfManualStatusReporter(); + private TraceMarkParser mTraceMarkParser = new TraceMarkParser(this::shouldFilterTraceLine); + private final ArrayList<Long> mDurations = new ArrayList<Long>(); + private Context mContext; + private HandlerThread mHandlerThread; + + @Before + public void setUp() { + mContext = InstrumentationRegistry.getTargetContext(); + mHandlerThread = new HandlerThread("command receiver"); + mHandlerThread.start(); + TargetPackageUtils.initCommandResultReceiver(mHandlerThread.getLooper()); + + Utils.runShellCommand("cmd deviceidle whitelist +" + STUB_PACKAGE1_NAME); + Utils.runShellCommand("cmd deviceidle whitelist +" + STUB_PACKAGE2_NAME); + Utils.runShellCommand("cmd deviceidle whitelist +" + STUB_PACKAGE3_NAME); + TargetPackageUtils.startStubPackage(mContext, STUB_PACKAGE1_NAME); + TargetPackageUtils.startStubPackage(mContext, STUB_PACKAGE2_NAME); + TargetPackageUtils.startStubPackage(mContext, STUB_PACKAGE3_NAME); + } + + @After + public void tearDown() { + TargetPackageUtils.stopStubPackage(mContext, STUB_PACKAGE1_NAME); + TargetPackageUtils.stopStubPackage(mContext, STUB_PACKAGE2_NAME); + TargetPackageUtils.stopStubPackage(mContext, STUB_PACKAGE3_NAME); + Utils.runShellCommand("cmd deviceidle whitelist -" + STUB_PACKAGE1_NAME); + Utils.runShellCommand("cmd deviceidle whitelist -" + STUB_PACKAGE2_NAME); + Utils.runShellCommand("cmd deviceidle whitelist -" + STUB_PACKAGE3_NAME); + mHandlerThread.quitSafely(); + } + + @Test + public void testOomAdj() { + final AtraceUtils atraceUtils = AtraceUtils.getInstance( + InstrumentationRegistry.getInstrumentation()); + final ManualBenchmarkState state = mPerfManualStatusReporter.getBenchmarkState(); + atraceUtils.startTrace(ATRACE_CATEGORY); + while (state.keepRunning(mDurations)) { + runCUJWithOomComputationOnce(); + + // Now kick off the trace dump + mDurations.clear(); + atraceUtils.performDump(mTraceMarkParser, this::handleTraceMarkSlices); + } + atraceUtils.stopTrace(); + } + + private boolean shouldFilterTraceLine(final TraceMarkLine line) { + return line.name.startsWith(ATRACE_OOMADJ_PREFIX); + } + + private void handleTraceMarkSlices(String key, List<TraceMarkSlice> slices) { + for (TraceMarkSlice slice: slices) { + mDurations.add(slice.getDurationInMicroseconds() * NANOS_PER_MICROSECOND); + } + } + + /** + * This tries to mimic a user journey, involes multiple activity/service starts/stop, + * the time spent on oom adj computation would be different between all these samples, + * but with enough samples, we'll be able to know the overall distribution of the time + * spent on it. + */ + private void runCUJWithOomComputationOnce() { + // Start activity from package1 + TargetPackageUtils.startActivity(STUB_PACKAGE1_NAME, STUB_PACKAGE1_NAME); + // Start activity from package2 + TargetPackageUtils.startActivity(STUB_PACKAGE2_NAME, STUB_PACKAGE2_NAME); + // Start activity from package3 + TargetPackageUtils.startActivity(STUB_PACKAGE3_NAME, STUB_PACKAGE3_NAME); + + // Stop activity in package1 + TargetPackageUtils.stopActivity(STUB_PACKAGE1_NAME, STUB_PACKAGE1_NAME); + // Stop activity in package2 + TargetPackageUtils.stopActivity(STUB_PACKAGE2_NAME, STUB_PACKAGE2_NAME); + // Stop activity in package3 + TargetPackageUtils.stopActivity(STUB_PACKAGE3_NAME, STUB_PACKAGE3_NAME); + + // Bind from package1 to package2 + TargetPackageUtils.bindService(STUB_PACKAGE1_NAME, STUB_PACKAGE2_NAME, + Context.BIND_AUTO_CREATE); + // Acquire content provider from package 1 to package3 + TargetPackageUtils.acquireProvider(STUB_PACKAGE1_NAME, STUB_PACKAGE3_NAME, + STUB_PACKAGE3_URI); + // Start activity from package1 + TargetPackageUtils.startActivity(STUB_PACKAGE1_NAME, STUB_PACKAGE1_NAME); + // Bind from package2 to package3 + TargetPackageUtils.bindService(STUB_PACKAGE2_NAME, STUB_PACKAGE3_NAME, + Context.BIND_AUTO_CREATE); + // Unbind from package 1 to package 2 + TargetPackageUtils.unbindService(STUB_PACKAGE1_NAME, STUB_PACKAGE2_NAME, 0); + // Stop activity in package1 + TargetPackageUtils.stopActivity(STUB_PACKAGE1_NAME, STUB_PACKAGE1_NAME); + + // Send broadcast to all of them + TargetPackageUtils.sendBroadcast(STUB_PACKAGE1_NAME, STUB_PACKAGE1_NAME); + TargetPackageUtils.sendBroadcast(STUB_PACKAGE2_NAME, STUB_PACKAGE2_NAME); + TargetPackageUtils.sendBroadcast(STUB_PACKAGE3_NAME, STUB_PACKAGE3_NAME); + + // Bind from package1 to package2 again + TargetPackageUtils.bindService(STUB_PACKAGE1_NAME, STUB_PACKAGE2_NAME, + Context.BIND_AUTO_CREATE); + // Create a cycle: package3 to package1 + TargetPackageUtils.bindService(STUB_PACKAGE3_NAME, STUB_PACKAGE1_NAME, + Context.BIND_AUTO_CREATE); + + // Send broadcast to all of them again + TargetPackageUtils.sendBroadcast(STUB_PACKAGE1_NAME, STUB_PACKAGE1_NAME); + TargetPackageUtils.sendBroadcast(STUB_PACKAGE2_NAME, STUB_PACKAGE2_NAME); + TargetPackageUtils.sendBroadcast(STUB_PACKAGE3_NAME, STUB_PACKAGE3_NAME); + // Start activity in package3 + TargetPackageUtils.startActivity(STUB_PACKAGE3_NAME, STUB_PACKAGE3_NAME); + + // Break the cycle: unbind from package3 to package1 + TargetPackageUtils.unbindService(STUB_PACKAGE3_NAME, STUB_PACKAGE1_NAME, 0); + + // Bind from package3 to package1 with waive priority + TargetPackageUtils.bindService(STUB_PACKAGE3_NAME, STUB_PACKAGE1_NAME, + Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY); + // Release provider connection + TargetPackageUtils.releaseProvider(STUB_PACKAGE1_NAME, STUB_PACKAGE3_NAME, + STUB_PACKAGE3_URI); + // Unbind from package1 to package2 + TargetPackageUtils.unbindService(STUB_PACKAGE1_NAME, STUB_PACKAGE2_NAME, 0); + // Unbind from package2 to packagae3 + TargetPackageUtils.unbindService(STUB_PACKAGE2_NAME, STUB_PACKAGE3_NAME, 0); + + // Bind from package3 to package2 with BIND_ABOVE_CLIENT + TargetPackageUtils.bindService(STUB_PACKAGE3_NAME, STUB_PACKAGE2_NAME, + Context.BIND_AUTO_CREATE | Context.BIND_ABOVE_CLIENT); + // Unbind from package3 to packagae2 + TargetPackageUtils.unbindService(STUB_PACKAGE3_NAME, STUB_PACKAGE2_NAME, 0); + + // Bind from package3 to package2 with BIND_ALLOW_OOM_MANAGEMENT + TargetPackageUtils.bindService(STUB_PACKAGE3_NAME, STUB_PACKAGE2_NAME, + Context.BIND_AUTO_CREATE | Context.BIND_ALLOW_OOM_MANAGEMENT); + // Unbind from package3 to packagae2 + TargetPackageUtils.unbindService(STUB_PACKAGE3_NAME, STUB_PACKAGE2_NAME, 0); + + // Bind from package3 to package2 with BIND_IMPORTANT + TargetPackageUtils.bindService(STUB_PACKAGE3_NAME, STUB_PACKAGE2_NAME, + Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT); + // Unbind from package3 to packagae2 + TargetPackageUtils.unbindService(STUB_PACKAGE3_NAME, STUB_PACKAGE2_NAME, 0); + + // Bind from package3 to package2 with BIND_NOT_FOREGROUND + TargetPackageUtils.bindService(STUB_PACKAGE3_NAME, STUB_PACKAGE2_NAME, + Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND); + // Unbind from package3 to packagae2 + TargetPackageUtils.unbindService(STUB_PACKAGE3_NAME, STUB_PACKAGE2_NAME, 0); + + // Bind from package3 to package2 with BIND_NOT_PERCEPTIBLE + TargetPackageUtils.bindService(STUB_PACKAGE3_NAME, STUB_PACKAGE2_NAME, + Context.BIND_AUTO_CREATE | Context.BIND_NOT_PERCEPTIBLE); + // Unbind from package3 to packagae2 + TargetPackageUtils.unbindService(STUB_PACKAGE3_NAME, STUB_PACKAGE2_NAME, 0); + + // Stop activity in package3 + TargetPackageUtils.stopActivity(STUB_PACKAGE3_NAME, STUB_PACKAGE3_NAME); + // Unbind from package3 to package1 + TargetPackageUtils.unbindService(STUB_PACKAGE3_NAME, STUB_PACKAGE1_NAME, 0); + } +} diff --git a/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/util/AtraceUtils.java b/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/util/AtraceUtils.java new file mode 100644 index 000000000000..fcccfce8bd0e --- /dev/null +++ b/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/util/AtraceUtils.java @@ -0,0 +1,108 @@ +/* + * 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.frameworks.perftests.am.util; + +import android.app.Instrumentation; +import android.app.UiAutomation; +import android.os.ParcelFileDescriptor; +import android.perftests.utils.TraceMarkParser; +import android.perftests.utils.TraceMarkParser.TraceMarkSlice; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.List; +import java.util.function.BiConsumer; + +// Simplified version of AtraceLogger. +public class AtraceUtils { + private static final String TAG = "AtraceUtils"; + private static final boolean VERBOSE = true; + + private static final String ATRACE_START = "atrace --async_start -b %d -c %s"; + private static final String ATRACE_DUMP = "atrace --async_dump"; + private static final String ATRACE_STOP = "atrace --async_stop"; + private static final int DEFAULT_ATRACE_BUF_SIZE = 1024; + + private UiAutomation mAutomation; + private static AtraceUtils sUtils = null; + private boolean mStarted = false; + + private AtraceUtils(Instrumentation instrumentation) { + mAutomation = instrumentation.getUiAutomation(); + } + + public static AtraceUtils getInstance(Instrumentation instrumentation) { + if (sUtils == null) { + sUtils = new AtraceUtils(instrumentation); + } + return sUtils; + } + + /** + * @param categories The list of the categories to trace, separated with space. + */ + public void startTrace(String categories) { + synchronized (this) { + if (mStarted) { + throw new IllegalStateException("atrace already started"); + } + Utils.runShellCommand(String.format( + ATRACE_START, DEFAULT_ATRACE_BUF_SIZE, categories)); + mStarted = true; + } + } + + public void stopTrace() { + synchronized (this) { + mStarted = false; + Utils.runShellCommand(ATRACE_STOP); + } + } + + /** + * @param parser The function that can accept the buffer of atrace dump and parse it. + * @param handler The parse result handler + */ + public void performDump(TraceMarkParser parser, + BiConsumer<String, List<TraceMarkSlice>> handler) { + parser.reset(); + try { + if (VERBOSE) { + Log.i(TAG, "Collecting atrace dump..."); + } + writeDataToBuf(mAutomation.executeShellCommand(ATRACE_DUMP), parser); + } catch (IOException e) { + Log.e(TAG, "Error in reading dump", e); + } + parser.forAllSlices(handler); + } + + // The given file descriptor here will be closed by this function + private void writeDataToBuf(ParcelFileDescriptor pfDescriptor, + TraceMarkParser parser) throws IOException { + InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(pfDescriptor); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + String line; + while ((line = reader.readLine()) != null) { + parser.visit(line); + } + } + } +} diff --git a/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/util/TargetPackageUtils.java b/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/util/TargetPackageUtils.java index 046dd6bb7dce..d7f4d9de6735 100644 --- a/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/util/TargetPackageUtils.java +++ b/tests/ActivityManagerPerfTests/tests/src/com/android/frameworks/perftests/am/util/TargetPackageUtils.java @@ -22,12 +22,18 @@ import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.PackageManager; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; -import android.os.ResultReceiver; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; import android.os.SystemClock; +import android.util.ArrayMap; +import android.util.Log; +import android.util.Pair; import org.junit.Assert; @@ -36,6 +42,7 @@ import java.util.concurrent.TimeUnit; public class TargetPackageUtils { private static final String TAG = TargetPackageUtils.class.getSimpleName(); + public static final boolean VERBOSE = true; public static final String PACKAGE_NAME = "com.android.frameworks.perftests.amteststestapp"; public static final String ACTIVITY_NAME = PACKAGE_NAME + ".TestActivity"; @@ -48,6 +55,12 @@ public class TargetPackageUtils { // Cache for test app's uid, so we only have to query it once. private static int sTestAppUid = -1; + private static final ArrayMap<String, ICommandReceiver> sStubPackages = + new ArrayMap<String, ICommandReceiver>(); + private static final ArrayMap<Integer, CountDownLatch> sCommandLatches = + new ArrayMap<Integer, CountDownLatch>(); + private static int sSeqNum = 0; + /** * Kills the test package synchronously. */ @@ -145,5 +158,160 @@ public class TargetPackageUtils { } } + private static boolean isUidRunning(int uid) { + return !Utils.runShellCommand(String.format("cmd activity get-uid-state %d", uid)) + .contains("(NONEXISTENT)"); + } + + public static void startStubPackage(Context context, String pkgName) { + stopStubPackage(context, pkgName); + try { + Pair<Integer, CountDownLatch> pair = obtainLatch(); + Intent intent = new Intent(); + intent.setComponent(new ComponentName(pkgName, Constants.STUB_INIT_SERVICE_NAME)); + intent.putExtra(Constants.EXTRA_SOURCE_PACKAGE, context.getPackageName()); + intent.putExtra(Constants.EXTRA_RECEIVER_CALLBACK, sMessenger); + intent.putExtra(Constants.EXTRA_SEQ, pair.first); + context.startService(intent); + Assert.assertTrue("Timeout when waiting for starting package " + pkgName, + pair.second.await(AWAIT_SERVICE_CONNECT_MS, TimeUnit.MILLISECONDS)); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + public static void stopStubPackage(Context context, String pkgName) { + final PackageManager pm = context.getPackageManager(); + try { + final int uid = pm.getPackageUid(pkgName, 0); + if (isUidRunning(uid)) { + ActivityManager am = context.getSystemService(ActivityManager.class); + am.forceStopPackage(pkgName); + while (isUidRunning(uid)) { + SystemClock.sleep(WAIT_TIME_MS); + } + } + } catch (PackageManager.NameNotFoundException e) { + throw new RuntimeException(e); + } + } + + public static void initCommandResultReceiver(Looper looper) { + if (sMessenger == null) { + sMessenger = new Messenger(new H(looper)); + } + } + + private static void onPackageStartResult(int seq, Bundle bundle) { + ICommandReceiver receiver = ICommandReceiver.Stub.asInterface( + bundle.getBinder(Constants.EXTRA_RECEIVER_CALLBACK)); + String sourcePkg = bundle.getString(Constants.EXTRA_SOURCE_PACKAGE); + sStubPackages.put(sourcePkg, receiver); + releaseLatch(seq); + } + + private static void onCommandResult(int seq, int result) { + Assert.assertTrue("Error in command seq " + seq, result == Constants.RESULT_NO_ERROR); + releaseLatch(seq); + } + + private static Messenger sMessenger = null; + private static class H extends Handler { + H(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case Constants.REPLY_PACKAGE_START_RESULT: + onPackageStartResult(msg.arg1 /* seq */, (Bundle) msg.obj); + break; + case Constants.REPLY_COMMAND_RESULT: + onCommandResult(msg.arg1, msg.arg2); + break; + } + } + } + + private static Pair<Integer, CountDownLatch> obtainLatch() { + CountDownLatch latch = new CountDownLatch(1); + int seq; + synchronized (sCommandLatches) { + seq = sSeqNum++; + sCommandLatches.put(seq, latch); + } + return new Pair<>(seq, latch); + } + + private static void releaseLatch(int seq) { + synchronized (sCommandLatches) { + CountDownLatch latch = sCommandLatches.get(seq); + if (latch != null) { + latch.countDown(); + sCommandLatches.remove(seq); + } + } + } + + public static void sendCommand(int command, String sourcePackage, String targetPackage, + int flags, Bundle bundle, boolean waitForResult) { + ICommandReceiver receiver = sStubPackages.get(sourcePackage); + Assert.assertTrue("Package hasn't been started: " + sourcePackage, receiver != null); + try { + Pair<Integer, CountDownLatch> pair = null; + if (waitForResult) { + pair = obtainLatch(); + } + if (VERBOSE) { + Log.i(TAG, "Sending command=" + command + ", seq=" + pair.first + ", from=" + + sourcePackage + ", to=" + targetPackage + ", flags=" + flags); + } + receiver.sendCommand(command, pair.first, sourcePackage, targetPackage, flags, bundle); + if (waitForResult) { + Assert.assertTrue("Timeout when waiting for command " + command + " from " + + sourcePackage + " to " + targetPackage, + pair.second.await(AWAIT_SERVICE_CONNECT_MS, TimeUnit.MILLISECONDS)); + } + } catch (RemoteException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public static void bindService(String sourcePackage, String targetPackage, int flags) { + sendCommand(Constants.COMMAND_BIND_SERVICE, sourcePackage, targetPackage, flags, null, + true); + } + + public static void unbindService(String sourcePackage, String targetPackage, int flags) { + sendCommand(Constants.COMMAND_UNBIND_SERVICE, sourcePackage, targetPackage, flags, null, + true); + } + + public static void acquireProvider(String sourcePackage, String targetPackage, Uri uri) { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.EXTRA_URI, uri); + sendCommand(Constants.COMMAND_ACQUIRE_CONTENT_PROVIDER, sourcePackage, targetPackage, 0, + bundle, true); + } + + public static void releaseProvider(String sourcePackage, String targetPackage, Uri uri) { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.EXTRA_URI, uri); + sendCommand(Constants.COMMAND_RELEASE_CONTENT_PROVIDER, sourcePackage, targetPackage, 0, + bundle, true); + } + + public static void sendBroadcast(String sourcePackage, String targetPackage) { + sendCommand(Constants.COMMAND_SEND_BROADCAST, sourcePackage, targetPackage, 0, null, true); + } + + public static void startActivity(String sourcePackage, String targetPackage) { + sendCommand(Constants.COMMAND_START_ACTIVITY, sourcePackage, targetPackage, 0, null, true); + } + + public static void stopActivity(String sourcePackage, String targetPackage) { + sendCommand(Constants.COMMAND_STOP_ACTIVITY, sourcePackage, targetPackage, 0, null, true); + } } diff --git a/tests/ActivityManagerPerfTests/utils/Android.bp b/tests/ActivityManagerPerfTests/utils/Android.bp index 300b7ea998fa..766c3acf3c09 100644 --- a/tests/ActivityManagerPerfTests/utils/Android.bp +++ b/tests/ActivityManagerPerfTests/utils/Android.bp @@ -18,6 +18,7 @@ java_test { srcs: [ "src/**/*.java", "src/com/android/frameworks/perftests/am/util/ITimeReceiverCallback.aidl", + "src/com/android/frameworks/perftests/am/util/ICommandReceiver.aidl", ], static_libs: [ "androidx.test.rules", diff --git a/tests/ActivityManagerPerfTests/utils/src/com/android/frameworks/perftests/am/util/Constants.java b/tests/ActivityManagerPerfTests/utils/src/com/android/frameworks/perftests/am/util/Constants.java index 9b076c507ff8..8e58665f5352 100644 --- a/tests/ActivityManagerPerfTests/utils/src/com/android/frameworks/perftests/am/util/Constants.java +++ b/tests/ActivityManagerPerfTests/utils/src/com/android/frameworks/perftests/am/util/Constants.java @@ -30,4 +30,33 @@ public class Constants { public static final String EXTRA_RECEIVER_CALLBACK = "receiver_callback_binder"; public static final String EXTRA_LOOPER_IDLE_CALLBACK = "looper_idle_callback_binder"; + public static final String EXTRA_SOURCE_PACKAGE = "source_package"; + public static final String EXTRA_URI = "uri"; + public static final String EXTRA_REQ_FINISH_ACTIVITY = "req_finish_activity"; + public static final String EXTRA_SEQ = "seq"; + public static final String EXTRA_ARG1 = "arg1"; + public static final String EXTRA_ARG2 = "arg2"; + + public static final int RESULT_NO_ERROR = 0; + public static final int RESULT_ERROR = 1; + public static final String STUB_INIT_SERVICE_NAME = "com.android.stubs.am.InitService"; + + public static final int COMMAND_BIND_SERVICE = 1; + public static final int COMMAND_UNBIND_SERVICE = 2; + public static final int COMMAND_ACQUIRE_CONTENT_PROVIDER = 3; + public static final int COMMAND_RELEASE_CONTENT_PROVIDER = 4; + public static final int COMMAND_SEND_BROADCAST = 5; + public static final int COMMAND_START_ACTIVITY = 6; + public static final int COMMAND_STOP_ACTIVITY = 7; + + public static final int MSG_DEFAULT = 0; + public static final int MSG_UNBIND_DONE = 1; + + public static final int REPLY_PACKAGE_START_RESULT = 0; + public static final int REPLY_COMMAND_RESULT = 1; + + public static final String STUB_ACTION_ACTIVITY = + "com.android.stubs.am.ACTION_START_TEST_ACTIVITY"; + public static final String STUB_ACTION_BROADCAST = + "com.android.stubs.am.ACTION_BROADCAST_TEST"; } diff --git a/tests/ActivityManagerPerfTests/utils/src/com/android/frameworks/perftests/am/util/ICommandReceiver.aidl b/tests/ActivityManagerPerfTests/utils/src/com/android/frameworks/perftests/am/util/ICommandReceiver.aidl new file mode 100644 index 000000000000..59ea7616a59e --- /dev/null +++ b/tests/ActivityManagerPerfTests/utils/src/com/android/frameworks/perftests/am/util/ICommandReceiver.aidl @@ -0,0 +1,24 @@ +/* + * 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.frameworks.perftests.am.util; + +import android.os.Bundle; + +interface ICommandReceiver { + oneway void sendCommand(int command, int seq, String sourcePackage, String targetPackage, + int flags, in Bundle bundle); +} 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 07b3a97817a3..768cfaea4898 100644 --- a/tests/ActivityTests/src/com/google/android/test/activity/ActivityTestMain.java +++ b/tests/ActivityTests/src/com/google/android/test/activity/ActivityTestMain.java @@ -32,6 +32,7 @@ import android.content.pm.UserInfo; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -587,7 +588,7 @@ public class ActivityTestMain extends Activity { desc.setLabel("Added #" + i); Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.icon); if ((i&1) == 0) { - desc.setIcon(bitmap); + desc.setIcon(Icon.createWithBitmap(bitmap)); } int taskId = am.addAppTask(this, intent, desc, bitmap); Log.i(TAG, "Added new task id #" + taskId); diff --git a/tests/ApkVerityTest/Android.bp b/tests/ApkVerityTest/Android.bp new file mode 100644 index 000000000000..02c75edafcd5 --- /dev/null +++ b/tests/ApkVerityTest/Android.bp @@ -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. + +java_test_host { + name: "ApkVerityTest", + srcs: ["src/**/*.java"], + libs: ["tradefed", "compatibility-tradefed", "compatibility-host-util"], + test_suites: ["general-tests", "vts"], + target_required: [ + "block_device_writer_module", + ], + data: [ + ":ApkVerityTestCertDer", + ":ApkVerityTestApp", + ":ApkVerityTestAppFsvSig", + ":ApkVerityTestAppDm", + ":ApkVerityTestAppDmFsvSig", + ":ApkVerityTestAppSplit", + ":ApkVerityTestAppSplitFsvSig", + ":ApkVerityTestAppSplitDm", + ":ApkVerityTestAppSplitDmFsvSig", + ], +} diff --git a/tests/ApkVerityTest/AndroidTest.xml b/tests/ApkVerityTest/AndroidTest.xml new file mode 100644 index 000000000000..55704eda905e --- /dev/null +++ b/tests/ApkVerityTest/AndroidTest.xml @@ -0,0 +1,41 @@ +<?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="APK fs-verity integration/regression test"> + <option name="test-suite-tag" value="apct" /> + + <!-- This test requires root to write against block device. --> + <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" /> + + <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> + <!-- Disable package verifier prevents it holding the target APK's fd that prevents cache + eviction. --> + <option name="set-global-setting" key="verifier_verify_adb_installs" value="0" /> + <option name="restore-settings" value="true" /> + + <!-- Skip in order to prevent reboot that confuses the test flow. --> + <option name="force-skip-system-props" value="true" /> + </target_preparer> + + <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> + <option name="cleanup" value="true" /> + <option name="push" value="block_device_writer->/data/local/tmp/block_device_writer" /> + <option name="push" value="ApkVerityTestCert.der->/data/local/tmp/ApkVerityTestCert.der" /> + </target_preparer> + + <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" > + <option name="jar" value="ApkVerityTest.jar" /> + </test> +</configuration> diff --git a/tests/ApkVerityTest/ApkVerityTestApp/Android.bp b/tests/ApkVerityTest/ApkVerityTestApp/Android.bp new file mode 100644 index 000000000000..69632b215822 --- /dev/null +++ b/tests/ApkVerityTest/ApkVerityTestApp/Android.bp @@ -0,0 +1,29 @@ +// 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: "ApkVerityTestApp", + manifest: "AndroidManifest.xml", + srcs: ["src/**/*.java"], +} + +android_test_helper_app { + name: "ApkVerityTestAppSplit", + manifest: "feature_split/AndroidManifest.xml", + srcs: ["src/**/*.java"], + aaptflags: [ + "--custom-package com.android.apkverity.feature_x", + "--package-id 0x80", + ], +} diff --git a/tests/ApkVerityTest/ApkVerityTestApp/AndroidManifest.xml b/tests/ApkVerityTest/ApkVerityTestApp/AndroidManifest.xml new file mode 100644 index 000000000000..0b3ff77c2cdf --- /dev/null +++ b/tests/ApkVerityTest/ApkVerityTestApp/AndroidManifest.xml @@ -0,0 +1,23 @@ +<?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.apkverity"> + <application> + <activity android:name=".DummyActivity"/> + </application> +</manifest> diff --git a/tests/ApkVerityTest/ApkVerityTestApp/feature_split/AndroidManifest.xml b/tests/ApkVerityTest/ApkVerityTestApp/feature_split/AndroidManifest.xml new file mode 100644 index 000000000000..3f1a4f3a26a1 --- /dev/null +++ b/tests/ApkVerityTest/ApkVerityTestApp/feature_split/AndroidManifest.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. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.apkverity" + android:isFeatureSplit="true" + split="feature_x"> + <application> + <activity android:name=".feature_x.DummyActivity"/> + </application> +</manifest> diff --git a/tests/ApkVerityTest/ApkVerityTestApp/feature_split/src/com/android/apkverity/feature_x/DummyActivity.java b/tests/ApkVerityTest/ApkVerityTestApp/feature_split/src/com/android/apkverity/feature_x/DummyActivity.java new file mode 100644 index 000000000000..0f694c293330 --- /dev/null +++ b/tests/ApkVerityTest/ApkVerityTestApp/feature_split/src/com/android/apkverity/feature_x/DummyActivity.java @@ -0,0 +1,22 @@ +/* + * 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.apkverity.feature_x; + +import android.app.Activity; + +/** Dummy class just to generate some dex */ +public class DummyActivity extends Activity {} diff --git a/tests/ApkVerityTest/ApkVerityTestApp/src/com/android/apkverity/DummyActivity.java b/tests/ApkVerityTest/ApkVerityTestApp/src/com/android/apkverity/DummyActivity.java new file mode 100644 index 000000000000..837c7be37504 --- /dev/null +++ b/tests/ApkVerityTest/ApkVerityTestApp/src/com/android/apkverity/DummyActivity.java @@ -0,0 +1,22 @@ +/* + * 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.apkverity; + +import android.app.Activity; + +/** Dummy class just to generate some dex */ +public class DummyActivity extends Activity {} diff --git a/tests/ApkVerityTest/TEST_MAPPING b/tests/ApkVerityTest/TEST_MAPPING new file mode 100644 index 000000000000..72d96148c69f --- /dev/null +++ b/tests/ApkVerityTest/TEST_MAPPING @@ -0,0 +1,12 @@ +{ + "presubmit": [ + { + "name": "ApkVerityTest" + }, + // nextgen test only runs during postsubmit. + { + "name": "ApkVerityTest", + "keywords": ["nextgen"] + } + ] +} diff --git a/tests/ApkVerityTest/block_device_writer/Android.bp b/tests/ApkVerityTest/block_device_writer/Android.bp new file mode 100644 index 000000000000..37fbc29470f6 --- /dev/null +++ b/tests/ApkVerityTest/block_device_writer/Android.bp @@ -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. + +// This is a cc_test just because it supports test_suites. This should be converted to something +// like cc_binary_test_helper once supported, thus auto_gen_config:false below. +cc_test { + // Depending on how the test runs, the executable may be uploaded to different location. + // Before the bug in the file pusher is fixed, workaround by making the name unique. + // See b/124718249#comment12. + name: "block_device_writer_module", + stem: "block_device_writer", + + srcs: ["block_device_writer.cpp"], + cflags: [ + "-D_FILE_OFFSET_BITS=64", + "-Wall", + "-Werror", + "-Wextra", + "-g", + ], + shared_libs: ["libbase", "libutils"], + // For some reasons, cuttlefish (x86) uses x86_64 test suites for testing. Unfortunately, when + // the uploader does not pick up the executable from correct output location. The following + // workaround allows the test to: + // * upload the 32-bit exectuable for both 32 and 64 bits devices to use + // * refer to the same executable name in Java + // * no need to force the Java test to be archiecture specific. + // + // See b/145573317 for details. + multilib: { + lib32: { + suffix: "", + }, + lib64: { + suffix: "64", // not really used + }, + }, + + auto_gen_config: false, + test_suites: ["general-tests", "pts", "vts"], + gtest: false, +} diff --git a/tests/ApkVerityTest/block_device_writer/block_device_writer.cpp b/tests/ApkVerityTest/block_device_writer/block_device_writer.cpp new file mode 100644 index 000000000000..02dfd732a716 --- /dev/null +++ b/tests/ApkVerityTest/block_device_writer/block_device_writer.cpp @@ -0,0 +1,248 @@ +/* + * 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. + */ +#include <cassert> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <memory> + +#include <errno.h> +#include <fcntl.h> +#include <linux/fiemap.h> +#include <linux/fs.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include <android-base/unique_fd.h> + +// This program modifies a file at given offset, but directly against the block +// device, purposely to bypass the filesystem. Note that the change on block +// device may not reflect the same way when read from filesystem, for example, +// when the file is encrypted on disk. +// +// Only one byte is supported for now just so that we don't need to handle the +// case when the range crosses different "extents". +// +// References: +// https://www.kernel.org/doc/Documentation/filesystems/fiemap.txt +// https://git.kernel.org/pub/scm/fs/xfs/xfsprogs-dev.git/tree/io/fiemap.c + +#ifndef F2FS_IOC_SET_PIN_FILE +#ifndef F2FS_IOCTL_MAGIC +#define F2FS_IOCTL_MAGIC 0xf5 +#endif +#define F2FS_IOC_SET_PIN_FILE _IOW(F2FS_IOCTL_MAGIC, 13, __u32) +#define F2FS_IOC_GET_PIN_FILE _IOR(F2FS_IOCTL_MAGIC, 14, __u32) +#endif + +struct Args { + const char* block_device; + const char* file_name; + uint64_t byte_offset; + bool use_f2fs_pinning; +}; + +class ScopedF2fsFilePinning { + public: + explicit ScopedF2fsFilePinning(const char* file_path) { + fd_.reset(TEMP_FAILURE_RETRY(open(file_path, O_WRONLY | O_CLOEXEC, 0))); + if (fd_.get() == -1) { + perror("Failed to open"); + return; + } + __u32 set = 1; + ioctl(fd_.get(), F2FS_IOC_SET_PIN_FILE, &set); + } + + ~ScopedF2fsFilePinning() { + __u32 set = 0; + ioctl(fd_.get(), F2FS_IOC_SET_PIN_FILE, &set); + } + + private: + android::base::unique_fd fd_; +}; + +ssize_t get_logical_block_size(const char* block_device) { + android::base::unique_fd fd(open(block_device, O_RDONLY)); + if (fd.get() < 0) { + fprintf(stderr, "open %s failed\n", block_device); + return -1; + } + + int size; + if (ioctl(fd, BLKSSZGET, &size) < 0) { + fprintf(stderr, "ioctl(BLKSSZGET) failed: %s\n", strerror(errno)); + return -1; + } + return size; +} + +int64_t get_physical_offset(const char* file_name, uint64_t byte_offset) { + android::base::unique_fd fd(open(file_name, O_RDONLY)); + if (fd.get() < 0) { + fprintf(stderr, "open %s failed\n", file_name); + return -1; + } + + const int map_size = sizeof(struct fiemap) + sizeof(struct fiemap_extent); + char fiemap_buffer[map_size] = {0}; + struct fiemap* fiemap = reinterpret_cast<struct fiemap*>(&fiemap_buffer); + + fiemap->fm_flags = FIEMAP_FLAG_SYNC; + fiemap->fm_start = byte_offset; + fiemap->fm_length = 1; + fiemap->fm_extent_count = 1; + + int ret = ioctl(fd.get(), FS_IOC_FIEMAP, fiemap); + if (ret < 0) { + fprintf(stderr, "ioctl(FS_IOC_FIEMAP) failed: %s\n", strerror(errno)); + return -1; + } + + if (fiemap->fm_mapped_extents != 1) { + fprintf(stderr, "fm_mapped_extents != 1 (is %d)\n", + fiemap->fm_mapped_extents); + return -1; + } + + struct fiemap_extent* extent = &fiemap->fm_extents[0]; + printf( + "logical offset: %llu, physical offset: %llu, length: %llu, " + "flags: %x\n", + extent->fe_logical, extent->fe_physical, extent->fe_length, + extent->fe_flags); + if (extent->fe_flags & (FIEMAP_EXTENT_UNKNOWN | + FIEMAP_EXTENT_UNWRITTEN)) { + fprintf(stderr, "Failed to locate physical offset safely\n"); + return -1; + } + + return extent->fe_physical + (byte_offset - extent->fe_logical); +} + +int read_block_from_device(const char* device_path, uint64_t block_offset, + ssize_t block_size, char* block_buffer) { + assert(block_offset % block_size == 0); + android::base::unique_fd fd(open(device_path, O_RDONLY | O_DIRECT)); + if (fd.get() < 0) { + fprintf(stderr, "open %s failed\n", device_path); + return -1; + } + + ssize_t retval = + TEMP_FAILURE_RETRY(pread(fd, block_buffer, block_size, block_offset)); + if (retval != block_size) { + fprintf(stderr, "read returns error or incomplete result (%zu): %s\n", + retval, strerror(errno)); + return -1; + } + return 0; +} + +int write_block_to_device(const char* device_path, uint64_t block_offset, + ssize_t block_size, char* block_buffer) { + assert(block_offset % block_size == 0); + android::base::unique_fd fd(open(device_path, O_WRONLY | O_DIRECT)); + if (fd.get() < 0) { + fprintf(stderr, "open %s failed\n", device_path); + return -1; + } + + ssize_t retval = TEMP_FAILURE_RETRY( + pwrite(fd.get(), block_buffer, block_size, block_offset)); + if (retval != block_size) { + fprintf(stderr, "write returns error or incomplete result (%zu): %s\n", + retval, strerror(errno)); + return -1; + } + return 0; +} + +std::unique_ptr<Args> parse_args(int argc, const char** argv) { + if (argc != 4 && argc != 5) { + fprintf(stderr, + "Usage: %s [--use-f2fs-pinning] block_dev filename byte_offset\n" + "\n" + "This program bypasses filesystem and damages the specified byte\n" + "at the physical position on <block_dev> corresponding to the\n" + "logical byte location in <filename>.\n", + argv[0]); + return nullptr; + } + + auto args = std::make_unique<Args>(); + const char** arg = &argv[1]; + args->use_f2fs_pinning = strcmp(*arg, "--use-f2fs-pinning") == 0; + if (args->use_f2fs_pinning) { + ++arg; + } + args->block_device = *(arg++); + args->file_name = *(arg++); + args->byte_offset = strtoull(*arg, nullptr, 10); + if (args->byte_offset == ULLONG_MAX) { + perror("Invalid byte offset"); + return nullptr; + } + return args; +} + +int main(int argc, const char** argv) { + std::unique_ptr<Args> args = parse_args(argc, argv); + if (args == nullptr) { + return -1; + } + + ssize_t block_size = get_logical_block_size(args->block_device); + if (block_size < 0) { + return -1; + } + + std::unique_ptr<ScopedF2fsFilePinning> pinned_file; + if (args->use_f2fs_pinning) { + pinned_file = std::make_unique<ScopedF2fsFilePinning>(args->file_name); + } + + int64_t physical_offset_signed = get_physical_offset(args->file_name, args->byte_offset); + if (physical_offset_signed < 0) { + return -1; + } + + uint64_t physical_offset = static_cast<uint64_t>(physical_offset_signed); + uint64_t offset_within_block = physical_offset % block_size; + uint64_t physical_block_offset = physical_offset - offset_within_block; + + // Direct I/O requires aligned buffer + std::unique_ptr<char> buf(static_cast<char*>( + aligned_alloc(block_size /* alignment */, block_size /* size */))); + + if (read_block_from_device(args->block_device, physical_block_offset, block_size, + buf.get()) < 0) { + return -1; + } + char* p = buf.get() + offset_within_block; + printf("before: %hhx\n", *p); + *p ^= 0xff; + printf("after: %hhx\n", *p); + if (write_block_to_device(args->block_device, physical_block_offset, block_size, + buf.get()) < 0) { + return -1; + } + + return 0; +} diff --git a/tests/ApkVerityTest/src/com/android/apkverity/ApkVerityTest.java b/tests/ApkVerityTest/src/com/android/apkverity/ApkVerityTest.java new file mode 100644 index 000000000000..629b6c714ae8 --- /dev/null +++ b/tests/ApkVerityTest/src/com/android/apkverity/ApkVerityTest.java @@ -0,0 +1,506 @@ +/* + * 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.apkverity; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; + +import android.platform.test.annotations.RootPermissionTest; + +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.log.LogUtil.CLog; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; +import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; +import com.android.tradefed.util.CommandResult; +import com.android.tradefed.util.CommandStatus; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; + +/** + * This test makes sure app installs with fs-verity signature, and on-access verification works. + * + * <p>When an app is installed, all or none of the files should have their corresponding .fsv_sig + * signature file. Otherwise, install will fail. + * + * <p>Once installed, file protected by fs-verity is verified by kernel every time a block is loaded + * from disk to memory. The file is immutable by design, enforced by filesystem. + * + * <p>In order to make sure a block of the file is readable only if the underlying block on disk + * stay intact, the test needs to bypass the filesystem and tampers with the corresponding physical + * address against the block device. + * + * <p>Requirements to run this test: + * <ul> + * <li>Device is rootable</li> + * <li>The filesystem supports fs-verity</li> + * <li>The feature flag is enabled</li> + * </ul> + */ +@RootPermissionTest +@RunWith(DeviceJUnit4ClassRunner.class) +public class ApkVerityTest extends BaseHostJUnit4Test { + private static final String TARGET_PACKAGE = "com.android.apkverity"; + + private static final String BASE_APK = "ApkVerityTestApp.apk"; + private static final String BASE_APK_DM = "ApkVerityTestApp.dm"; + private static final String SPLIT_APK = "ApkVerityTestAppSplit.apk"; + private static final String SPLIT_APK_DM = "ApkVerityTestAppSplit.dm"; + + private static final String INSTALLED_BASE_APK = "base.apk"; + private static final String INSTALLED_BASE_DM = "base.dm"; + private static final String INSTALLED_SPLIT_APK = "split_feature_x.apk"; + private static final String INSTALLED_SPLIT_DM = "split_feature_x.dm"; + private static final String INSTALLED_BASE_APK_FSV_SIG = "base.apk.fsv_sig"; + private static final String INSTALLED_BASE_DM_FSV_SIG = "base.dm.fsv_sig"; + private static final String INSTALLED_SPLIT_APK_FSV_SIG = "split_feature_x.apk.fsv_sig"; + private static final String INSTALLED_SPLIT_DM_FSV_SIG = "split_feature_x.dm.fsv_sig"; + + private static final String DAMAGING_EXECUTABLE = "/data/local/tmp/block_device_writer"; + private static final String CERT_PATH = "/data/local/tmp/ApkVerityTestCert.der"; + + private static final String APK_VERITY_STANDARD_MODE = "2"; + + /** Only 4K page is supported by fs-verity currently. */ + private static final int FSVERITY_PAGE_SIZE = 4096; + + private ITestDevice mDevice; + private String mKeyId; + + @Before + public void setUp() throws DeviceNotAvailableException { + mDevice = getDevice(); + + String apkVerityMode = mDevice.getProperty("ro.apk_verity.mode"); + assumeTrue(mDevice.getLaunchApiLevel() >= 30 + || APK_VERITY_STANDARD_MODE.equals(apkVerityMode)); + + mKeyId = expectRemoteCommandToSucceed( + "mini-keyctl padd asymmetric fsv_test .fs-verity < " + CERT_PATH).trim(); + if (!mKeyId.matches("^\\d+$")) { + String keyId = mKeyId; + mKeyId = null; + fail("Key ID is not decimal: " + keyId); + } + + uninstallPackage(TARGET_PACKAGE); + } + + @After + public void tearDown() throws DeviceNotAvailableException { + uninstallPackage(TARGET_PACKAGE); + + if (mKeyId != null) { + expectRemoteCommandToSucceed("mini-keyctl unlink " + mKeyId + " .fs-verity"); + } + } + + @Test + public void testFsverityKernelSupports() throws DeviceNotAvailableException { + ITestDevice.MountPointInfo mountPoint = mDevice.getMountPointInfo("/data"); + expectRemoteCommandToSucceed("test -f /sys/fs/" + mountPoint.type + "/features/verity"); + } + + @Test + public void testInstallBase() throws DeviceNotAvailableException, FileNotFoundException { + new InstallMultiple() + .addFileAndSignature(BASE_APK) + .run(); + assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); + + verifyInstalledFiles( + INSTALLED_BASE_APK, + INSTALLED_BASE_APK_FSV_SIG); + verifyInstalledFilesHaveFsverity(); + } + + @Test + public void testInstallBaseWithWrongSignature() + throws DeviceNotAvailableException, FileNotFoundException { + new InstallMultiple() + .addFile(BASE_APK) + .addFile(SPLIT_APK_DM + ".fsv_sig", + BASE_APK + ".fsv_sig") + .runExpectingFailure(); + } + + @Test + public void testInstallBaseWithSplit() + throws DeviceNotAvailableException, FileNotFoundException { + new InstallMultiple() + .addFileAndSignature(BASE_APK) + .addFileAndSignature(SPLIT_APK) + .run(); + assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); + + verifyInstalledFiles( + INSTALLED_BASE_APK, + INSTALLED_BASE_APK_FSV_SIG, + INSTALLED_SPLIT_APK, + INSTALLED_SPLIT_APK_FSV_SIG); + verifyInstalledFilesHaveFsverity(); + } + + @Test + public void testInstallBaseWithDm() throws DeviceNotAvailableException, FileNotFoundException { + new InstallMultiple() + .addFileAndSignature(BASE_APK) + .addFileAndSignature(BASE_APK_DM) + .run(); + assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); + + verifyInstalledFiles( + INSTALLED_BASE_APK, + INSTALLED_BASE_APK_FSV_SIG, + INSTALLED_BASE_DM, + INSTALLED_BASE_DM_FSV_SIG); + verifyInstalledFilesHaveFsverity(); + } + + @Test + public void testInstallEverything() throws DeviceNotAvailableException, FileNotFoundException { + new InstallMultiple() + .addFileAndSignature(BASE_APK) + .addFileAndSignature(BASE_APK_DM) + .addFileAndSignature(SPLIT_APK) + .addFileAndSignature(SPLIT_APK_DM) + .run(); + assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); + + verifyInstalledFiles( + INSTALLED_BASE_APK, + INSTALLED_BASE_APK_FSV_SIG, + INSTALLED_BASE_DM, + INSTALLED_BASE_DM_FSV_SIG, + INSTALLED_SPLIT_APK, + INSTALLED_SPLIT_APK_FSV_SIG, + INSTALLED_SPLIT_DM, + INSTALLED_SPLIT_DM_FSV_SIG); + verifyInstalledFilesHaveFsverity(); + } + + @Test + public void testInstallSplitOnly() + throws DeviceNotAvailableException, FileNotFoundException { + new InstallMultiple() + .addFileAndSignature(BASE_APK) + .run(); + assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); + verifyInstalledFiles( + INSTALLED_BASE_APK, + INSTALLED_BASE_APK_FSV_SIG); + + new InstallMultiple() + .inheritFrom(TARGET_PACKAGE) + .addFileAndSignature(SPLIT_APK) + .run(); + + verifyInstalledFiles( + INSTALLED_BASE_APK, + INSTALLED_BASE_APK_FSV_SIG, + INSTALLED_SPLIT_APK, + INSTALLED_SPLIT_APK_FSV_SIG); + verifyInstalledFilesHaveFsverity(); + } + + @Test + public void testInstallSplitOnlyMissingSignature() + throws DeviceNotAvailableException, FileNotFoundException { + new InstallMultiple() + .addFileAndSignature(BASE_APK) + .run(); + assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); + verifyInstalledFiles( + INSTALLED_BASE_APK, + INSTALLED_BASE_APK_FSV_SIG); + + new InstallMultiple() + .inheritFrom(TARGET_PACKAGE) + .addFile(SPLIT_APK) + .runExpectingFailure(); + } + + @Test + public void testInstallSplitOnlyWithoutBaseSignature() + throws DeviceNotAvailableException, FileNotFoundException { + new InstallMultiple() + .addFile(BASE_APK) + .run(); + assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); + verifyInstalledFiles(INSTALLED_BASE_APK); + + new InstallMultiple() + .inheritFrom(TARGET_PACKAGE) + .addFileAndSignature(SPLIT_APK) + .run(); + verifyInstalledFiles( + INSTALLED_BASE_APK, + INSTALLED_SPLIT_APK, + INSTALLED_SPLIT_APK_FSV_SIG); + + } + + @Test + public void testInstallOnlyBaseHasFsvSig() + throws DeviceNotAvailableException, FileNotFoundException { + new InstallMultiple() + .addFileAndSignature(BASE_APK) + .addFile(BASE_APK_DM) + .addFile(SPLIT_APK) + .addFile(SPLIT_APK_DM) + .runExpectingFailure(); + } + + @Test + public void testInstallOnlyDmHasFsvSig() + throws DeviceNotAvailableException, FileNotFoundException { + new InstallMultiple() + .addFile(BASE_APK) + .addFileAndSignature(BASE_APK_DM) + .addFile(SPLIT_APK) + .addFile(SPLIT_APK_DM) + .runExpectingFailure(); + } + + @Test + public void testInstallOnlySplitHasFsvSig() + throws DeviceNotAvailableException, FileNotFoundException { + new InstallMultiple() + .addFile(BASE_APK) + .addFile(BASE_APK_DM) + .addFileAndSignature(SPLIT_APK) + .addFile(SPLIT_APK_DM) + .runExpectingFailure(); + } + + @Test + public void testInstallBaseWithFsvSigThenSplitWithout() + throws DeviceNotAvailableException, FileNotFoundException { + new InstallMultiple() + .addFileAndSignature(BASE_APK) + .run(); + assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); + verifyInstalledFiles( + INSTALLED_BASE_APK, + INSTALLED_BASE_APK_FSV_SIG); + + new InstallMultiple() + .addFile(SPLIT_APK) + .runExpectingFailure(); + } + + @Test + public void testInstallBaseWithoutFsvSigThenSplitWith() + throws DeviceNotAvailableException, FileNotFoundException { + new InstallMultiple() + .addFile(BASE_APK) + .run(); + assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); + verifyInstalledFiles(INSTALLED_BASE_APK); + + new InstallMultiple() + .addFileAndSignature(SPLIT_APK) + .runExpectingFailure(); + } + + @Test + public void testFsverityFileIsImmutableAndReadable() throws DeviceNotAvailableException { + new InstallMultiple().addFileAndSignature(BASE_APK).run(); + String apkPath = getApkPath(TARGET_PACKAGE); + + assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); + expectRemoteCommandToFail("echo -n '' >> " + apkPath); + expectRemoteCommandToSucceed("cat " + apkPath + " > /dev/null"); + } + + @Test + public void testFsverityFailToReadModifiedBlockAtFront() throws DeviceNotAvailableException { + new InstallMultiple().addFileAndSignature(BASE_APK).run(); + String apkPath = getApkPath(TARGET_PACKAGE); + + long apkSize = getFileSizeInBytes(apkPath); + long offsetFirstByte = 0; + + // The first two pages should be both readable at first. + assertTrue(canReadByte(apkPath, offsetFirstByte)); + if (apkSize > offsetFirstByte + FSVERITY_PAGE_SIZE) { + assertTrue(canReadByte(apkPath, offsetFirstByte + FSVERITY_PAGE_SIZE)); + } + + // Damage the file directly against the block device. + damageFileAgainstBlockDevice(apkPath, offsetFirstByte); + + // Expect actual read from disk to fail but only at damaged page. + dropCaches(); + assertFalse(canReadByte(apkPath, offsetFirstByte)); + if (apkSize > offsetFirstByte + FSVERITY_PAGE_SIZE) { + long lastByteOfTheSamePage = + offsetFirstByte % FSVERITY_PAGE_SIZE + FSVERITY_PAGE_SIZE - 1; + assertFalse(canReadByte(apkPath, lastByteOfTheSamePage)); + assertTrue(canReadByte(apkPath, lastByteOfTheSamePage + 1)); + } + } + + @Test + public void testFsverityFailToReadModifiedBlockAtBack() throws DeviceNotAvailableException { + new InstallMultiple().addFileAndSignature(BASE_APK).run(); + String apkPath = getApkPath(TARGET_PACKAGE); + + long apkSize = getFileSizeInBytes(apkPath); + long offsetOfLastByte = apkSize - 1; + + // The first two pages should be both readable at first. + assertTrue(canReadByte(apkPath, offsetOfLastByte)); + if (offsetOfLastByte - FSVERITY_PAGE_SIZE > 0) { + assertTrue(canReadByte(apkPath, offsetOfLastByte - FSVERITY_PAGE_SIZE)); + } + + // Damage the file directly against the block device. + damageFileAgainstBlockDevice(apkPath, offsetOfLastByte); + + // Expect actual read from disk to fail but only at damaged page. + dropCaches(); + assertFalse(canReadByte(apkPath, offsetOfLastByte)); + if (offsetOfLastByte - FSVERITY_PAGE_SIZE > 0) { + long firstByteOfTheSamePage = offsetOfLastByte - offsetOfLastByte % FSVERITY_PAGE_SIZE; + assertFalse(canReadByte(apkPath, firstByteOfTheSamePage)); + assertTrue(canReadByte(apkPath, firstByteOfTheSamePage - 1)); + } + } + + private void verifyInstalledFilesHaveFsverity() throws DeviceNotAvailableException { + // Verify that all files are protected by fs-verity + String apkPath = getApkPath(TARGET_PACKAGE); + String appDir = apkPath.substring(0, apkPath.lastIndexOf("/")); + long kTargetOffset = 0; + for (String basename : expectRemoteCommandToSucceed("ls " + appDir).split("\n")) { + if (basename.endsWith(".apk") || basename.endsWith(".dm")) { + String path = appDir + "/" + basename; + damageFileAgainstBlockDevice(path, kTargetOffset); + + // Retry is sometimes needed to pass the test. Package manager may have FD leaks + // (see b/122744005 as example) that prevents the file in question to be evicted + // from filesystem cache. Forcing GC workarounds the problem. + int retry = 5; + for (; retry > 0; retry--) { + dropCaches(); + if (!canReadByte(path, kTargetOffset)) { + break; + } + try { + CLog.d("lsof: " + expectRemoteCommandToSucceed("lsof " + apkPath)); + Thread.sleep(1000); + String pid = expectRemoteCommandToSucceed("pidof system_server"); + mDevice.executeShellV2Command("kill -10 " + pid); // force GC + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + assertTrue("Read from " + path + " should fail", retry > 0); + } + } + } + + private void verifyInstalledFiles(String... filenames) throws DeviceNotAvailableException { + String apkPath = getApkPath(TARGET_PACKAGE); + String appDir = apkPath.substring(0, apkPath.lastIndexOf("/")); + // Exclude directories since we only care about files. + HashSet<String> actualFiles = new HashSet<>(Arrays.asList( + expectRemoteCommandToSucceed("ls -p " + appDir + " | grep -v '/'").split("\n"))); + + HashSet<String> expectedFiles = new HashSet<>(Arrays.asList(filenames)); + assertEquals(expectedFiles, actualFiles); + } + + private void damageFileAgainstBlockDevice(String path, long offsetOfTargetingByte) + throws DeviceNotAvailableException { + assertTrue(path.startsWith("/data/")); + ITestDevice.MountPointInfo mountPoint = mDevice.getMountPointInfo("/data"); + ArrayList<String> args = new ArrayList<>(); + args.add(DAMAGING_EXECUTABLE); + if ("f2fs".equals(mountPoint.type)) { + args.add("--use-f2fs-pinning"); + } + args.add(mountPoint.filesystem); + args.add(path); + args.add(Long.toString(offsetOfTargetingByte)); + expectRemoteCommandToSucceed(String.join(" ", args)); + } + + private String getApkPath(String packageName) throws DeviceNotAvailableException { + String line = expectRemoteCommandToSucceed("pm path " + packageName + " | grep base.apk"); + int index = line.trim().indexOf(":"); + assertTrue(index >= 0); + return line.substring(index + 1); + } + + private long getFileSizeInBytes(String packageName) throws DeviceNotAvailableException { + return Long.parseLong(expectRemoteCommandToSucceed("stat -c '%s' " + packageName).trim()); + } + + private void dropCaches() throws DeviceNotAvailableException { + expectRemoteCommandToSucceed("sync && echo 1 > /proc/sys/vm/drop_caches"); + } + + private boolean canReadByte(String filePath, long offset) throws DeviceNotAvailableException { + CommandResult result = mDevice.executeShellV2Command( + "dd if=" + filePath + " bs=1 count=1 skip=" + Long.toString(offset)); + return result.getStatus() == CommandStatus.SUCCESS; + } + + private String expectRemoteCommandToSucceed(String cmd) throws DeviceNotAvailableException { + CommandResult result = mDevice.executeShellV2Command(cmd); + assertEquals("`" + cmd + "` failed: " + result.getStderr(), CommandStatus.SUCCESS, + result.getStatus()); + return result.getStdout(); + } + + private void expectRemoteCommandToFail(String cmd) throws DeviceNotAvailableException { + CommandResult result = mDevice.executeShellV2Command(cmd); + assertTrue("Unexpected success from `" + cmd + "`: " + result.getStderr(), + result.getStatus() != CommandStatus.SUCCESS); + } + + private class InstallMultiple extends BaseInstallMultiple<InstallMultiple> { + InstallMultiple() { + super(getDevice(), getBuild()); + } + + InstallMultiple addFileAndSignature(String filename) { + try { + addFile(filename); + addFile(filename + ".fsv_sig"); + } catch (FileNotFoundException e) { + fail("Missing test file: " + e); + } + return this; + } + } +} diff --git a/tests/ApkVerityTest/src/com/android/apkverity/BaseInstallMultiple.java b/tests/ApkVerityTest/src/com/android/apkverity/BaseInstallMultiple.java new file mode 100644 index 000000000000..02e73d157dde --- /dev/null +++ b/tests/ApkVerityTest/src/com/android/apkverity/BaseInstallMultiple.java @@ -0,0 +1,140 @@ +/* + * 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.apkverity; + +import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; +import com.android.tradefed.build.IBuildInfo; +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.device.ITestDevice; + +import junit.framework.TestCase; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Base class for invoking the install-multiple command via ADB. Subclass this for less typing: + * + * <code> private class InstallMultiple extends BaseInstallMultiple<InstallMultiple> { public + * InstallMultiple() { super(getDevice(), null); } } </code> + */ +/*package*/ class BaseInstallMultiple<T extends BaseInstallMultiple<?>> { + + private final ITestDevice mDevice; + private final IBuildInfo mBuild; + + private final List<String> mArgs = new ArrayList<>(); + private final Map<File, String> mFileToRemoteMap = new HashMap<>(); + + /*package*/ BaseInstallMultiple(ITestDevice device, IBuildInfo buildInfo) { + mDevice = device; + mBuild = buildInfo; + addArg("-g"); + } + + T addArg(String arg) { + mArgs.add(arg); + return (T) this; + } + + T addFile(String filename) throws FileNotFoundException { + return addFile(filename, filename); + } + + T addFile(String filename, String remoteName) throws FileNotFoundException { + CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mBuild); + mFileToRemoteMap.put(buildHelper.getTestFile(filename), remoteName); + return (T) this; + } + + T inheritFrom(String packageName) { + addArg("-r"); + addArg("-p " + packageName); + return (T) this; + } + + void run() throws DeviceNotAvailableException { + run(true); + } + + void runExpectingFailure() throws DeviceNotAvailableException { + run(false); + } + + private void run(boolean expectingSuccess) throws DeviceNotAvailableException { + final ITestDevice device = mDevice; + + // Create an install session + final StringBuilder cmd = new StringBuilder(); + cmd.append("pm install-create"); + for (String arg : mArgs) { + cmd.append(' ').append(arg); + } + + String result = device.executeShellCommand(cmd.toString()); + TestCase.assertTrue(result, result.startsWith("Success")); + + final int start = result.lastIndexOf("["); + final int end = result.lastIndexOf("]"); + int sessionId = -1; + try { + if (start != -1 && end != -1 && start < end) { + sessionId = Integer.parseInt(result.substring(start + 1, end)); + } + } catch (NumberFormatException e) { + throw new IllegalStateException("Failed to parse install session: " + result); + } + if (sessionId == -1) { + throw new IllegalStateException("Failed to create install session: " + result); + } + + // Push our files into session. Ideally we'd use stdin streaming, + // but ddmlib doesn't support it yet. + for (final Map.Entry<File, String> entry : mFileToRemoteMap.entrySet()) { + final File file = entry.getKey(); + final String remoteName = entry.getValue(); + final String remotePath = "/data/local/tmp/" + file.getName(); + if (!device.pushFile(file, remotePath)) { + throw new IllegalStateException("Failed to push " + file); + } + + cmd.setLength(0); + cmd.append("pm install-write"); + cmd.append(' ').append(sessionId); + cmd.append(' ').append(remoteName); + cmd.append(' ').append(remotePath); + + result = device.executeShellCommand(cmd.toString()); + TestCase.assertTrue(result, result.startsWith("Success")); + } + + // Everything staged; let's pull trigger + cmd.setLength(0); + cmd.append("pm install-commit"); + cmd.append(' ').append(sessionId); + + result = device.executeShellCommand(cmd.toString()); + if (expectingSuccess) { + TestCase.assertTrue(result, result.contains("Success")); + } else { + TestCase.assertFalse(result, result.contains("Success")); + } + } +} diff --git a/tests/ApkVerityTest/testdata/Android.bp b/tests/ApkVerityTest/testdata/Android.bp new file mode 100644 index 000000000000..c10b0cef21d7 --- /dev/null +++ b/tests/ApkVerityTest/testdata/Android.bp @@ -0,0 +1,77 @@ +// 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. + +filegroup { + name: "ApkVerityTestKeyPem", + srcs: ["ApkVerityTestKey.pem"], +} + +filegroup { + name: "ApkVerityTestCertPem", + srcs: ["ApkVerityTestCert.pem"], +} + +filegroup { + name: "ApkVerityTestCertDer", + srcs: ["ApkVerityTestCert.der"], +} + +filegroup { + name: "ApkVerityTestAppDm", + srcs: ["ApkVerityTestApp.dm"], +} + +filegroup { + name: "ApkVerityTestAppSplitDm", + srcs: ["ApkVerityTestAppSplit.dm"], +} + +genrule_defaults { + name: "apk_verity_sig_gen_default", + tools: ["fsverity"], + tool_files: [":ApkVerityTestKeyPem", ":ApkVerityTestCertPem"], + cmd: "$(location fsverity) sign $(in) $(out) " + + "--key=$(location :ApkVerityTestKeyPem) " + + "--cert=$(location :ApkVerityTestCertPem) " + + "> /dev/null", +} + +genrule { + name: "ApkVerityTestAppFsvSig", + defaults: ["apk_verity_sig_gen_default"], + srcs: [":ApkVerityTestApp"], + out: ["ApkVerityTestApp.apk.fsv_sig"], +} + +genrule { + name: "ApkVerityTestAppDmFsvSig", + defaults: ["apk_verity_sig_gen_default"], + srcs: [":ApkVerityTestAppDm"], + out: ["ApkVerityTestApp.dm.fsv_sig"], +} + +genrule { + name: "ApkVerityTestAppSplitFsvSig", + defaults: ["apk_verity_sig_gen_default"], + srcs: [":ApkVerityTestAppSplit"], + out: ["ApkVerityTestAppSplit.apk.fsv_sig"], +} + +genrule { + name: "ApkVerityTestAppSplitDmFsvSig", + defaults: ["apk_verity_sig_gen_default"], + srcs: [":ApkVerityTestAppSplitDm"], + out: ["ApkVerityTestAppSplit.dm.fsv_sig"], +} + diff --git a/tests/ApkVerityTest/testdata/ApkVerityTestApp.dm b/tests/ApkVerityTest/testdata/ApkVerityTestApp.dm Binary files differnew file mode 100644 index 000000000000..e53a86131366 --- /dev/null +++ b/tests/ApkVerityTest/testdata/ApkVerityTestApp.dm diff --git a/tests/ApkVerityTest/testdata/ApkVerityTestAppSplit.dm b/tests/ApkVerityTest/testdata/ApkVerityTestAppSplit.dm Binary files differnew file mode 100644 index 000000000000..75396f1ba730 --- /dev/null +++ b/tests/ApkVerityTest/testdata/ApkVerityTestAppSplit.dm diff --git a/tests/ApkVerityTest/testdata/ApkVerityTestCert.der b/tests/ApkVerityTest/testdata/ApkVerityTestCert.der Binary files differnew file mode 100644 index 000000000000..fe9029b53aa1 --- /dev/null +++ b/tests/ApkVerityTest/testdata/ApkVerityTestCert.der diff --git a/tests/ApkVerityTest/testdata/ApkVerityTestCert.pem b/tests/ApkVerityTest/testdata/ApkVerityTestCert.pem new file mode 100644 index 000000000000..6c0b7b1f635a --- /dev/null +++ b/tests/ApkVerityTest/testdata/ApkVerityTestCert.pem @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFLjCCAxagAwIBAgIJAKZbtMlZZwtdMA0GCSqGSIb3DQEBCwUAMCwxCzAJBgNV +BAYTAlVTMQswCQYDVQQIDAJDQTEQMA4GA1UECgwHQW5kcm9pZDAeFw0xODEyMTky +MTA5MzVaFw0xOTAxMTgyMTA5MzVaMCwxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJD +QTEQMA4GA1UECgwHQW5kcm9pZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBAKnrw4WiFgFBq6vXqcLc97iwvcYPZmeIjQqYRF+CHwXBXx8IyDlMfPrgyIYo +ZLkosnUK/Exuypdu6UEtdqtYPknC6w9z4YkxqsKtyxyB1b13ptcTHh3bf2N8bqGr +8gWWLxj0QjumCtFi7Z/TCwB5t3b3gtC+0jVfABSWrm5PNkgk7jIP+4KeYLDCDfiJ +XH3uHu6OASiSHTOnrmLWSaSw0y6G4OFthHqQnMywasly0r6m+Mif+K0ZUV7hBRi/ +SfqcJ1HTCXTJMskEyV6Qx2sHF/VbK2gdUv56z6OVRNSs/FxPBiWVMuZZKh1FpBVI +gbGxusf2Awwtc+Soxr4/P1YFcrwfA/ff9FK3Yg/Cd3ZMGbzUkbEMEkE5BW7Gbjmx +wz3mYTiRfa2L/Bl4MiMqNi0tfORLkmg+V/EItzfhZ/HsXMOCBsnuj4KnFslmbamz +t9opypj2JLGk+lXhZ5gFNFw8tYH1AnG1AIXe5u+6Fq2nQ1y/ncGUTR5Sw4de/Gee +C0UgR+KiFEdKupMKbXgSKl+0QPz/i2eSpcDOKMwZ4WiNrkbccbCyr38so+j5DfWF +IeZA9a/IlysA6G8yU2TfXBc65VCIEQRJOQdUOZFDO8OSoqGP+fbA6edpmovGw+TH +sM/NkmpEXpQm7BVOI4oVjdf4pKPp0zaW2YcaA3xU2w6eF17pAgMBAAGjUzBRMB0G +A1UdDgQWBBRGpHYy7yiLEYalGuF1va6zJKGD/zAfBgNVHSMEGDAWgBRGpHYy7yiL +EYalGuF1va6zJKGD/zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IC +AQAao6ZBM122F0pYb2QLahIyyGEr3LfSdBGID4068pVik4ncIefFz36Xf9AFxRQd +KHmwRYNPHiLRIEGdtqplC5pZDeHz41txIArNIZKzDWOYtdcFyCz8umuj912BmsoM +YUQhT6F1sX53SWcKxEP/aJ2kltSlPFX99e3Vx9eRkceV1oe2NM6ZG8hnYCfCAMeJ +jRTpbqCGaAsEHFtIx6wt3zEtUXIVg4aYFQs/qjTjeP8ByIj0b4lZrceEoTeRimuj ++4aAI+jBxLkwaN3hseQHzRNpgPehIVV/0RU92yzOD/WN4YwE6rwjKEI1lihHNBDa ++DwGtGbHmIUzjW1qArig+mzUIhfYIJAxrx20ynPz/Q+C7+iXhTDAYQlxTle0pX8m +yM2DUdPo97eLOzQ4JDHxtcN3ntTEJKKvrmzKvWuxy/yoLwS7MtLH6RETTHabH3Qd +CP83X7z8zTyxgPxHdfHo9sgR/4C9RHGJx4OpBTQaiqfjSpDqJSIQdbrHGOQDgYwL +KQyiQuhukmNgRCB6dJoZJ/MyaNuMsXV9QobsDHW1oSuCvPAihVoWHJxt8m4Ma0jJ +EIbEPT2Umw1F/P+CeXnVQwhPvzQKHCa+6cC/YdjTqIKLmQV8X3HUBUIMhP2JGDic +MnUipTm/RwWZVOjCJaFqk5sVq3L0Lyd0XVUWSK1a4IcrsA== +-----END CERTIFICATE----- diff --git a/tests/ApkVerityTest/testdata/ApkVerityTestKey.pem b/tests/ApkVerityTest/testdata/ApkVerityTestKey.pem new file mode 100644 index 000000000000..f0746c162421 --- /dev/null +++ b/tests/ApkVerityTest/testdata/ApkVerityTestKey.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCp68OFohYBQaur +16nC3Pe4sL3GD2ZniI0KmERfgh8FwV8fCMg5THz64MiGKGS5KLJ1CvxMbsqXbulB +LXarWD5JwusPc+GJMarCrcscgdW9d6bXEx4d239jfG6hq/IFli8Y9EI7pgrRYu2f +0wsAebd294LQvtI1XwAUlq5uTzZIJO4yD/uCnmCwwg34iVx97h7ujgEokh0zp65i +1kmksNMuhuDhbYR6kJzMsGrJctK+pvjIn/itGVFe4QUYv0n6nCdR0wl0yTLJBMle +kMdrBxf1WytoHVL+es+jlUTUrPxcTwYllTLmWSodRaQVSIGxsbrH9gMMLXPkqMa+ +Pz9WBXK8HwP33/RSt2IPwnd2TBm81JGxDBJBOQVuxm45scM95mE4kX2ti/wZeDIj +KjYtLXzkS5JoPlfxCLc34Wfx7FzDggbJ7o+CpxbJZm2ps7faKcqY9iSxpPpV4WeY +BTRcPLWB9QJxtQCF3ubvuhatp0Ncv53BlE0eUsOHXvxnngtFIEfiohRHSrqTCm14 +EipftED8/4tnkqXAzijMGeFoja5G3HGwsq9/LKPo+Q31hSHmQPWvyJcrAOhvMlNk +31wXOuVQiBEESTkHVDmRQzvDkqKhj/n2wOnnaZqLxsPkx7DPzZJqRF6UJuwVTiOK +FY3X+KSj6dM2ltmHGgN8VNsOnhde6QIDAQABAoICAGT21tWnisWyXKwd2BwWKgeO +1SRDcEiihZO/CBlr+rzzum55TGdngHedauj0RW0Ttn3/SgysZCp415ZHylRjeZdg +f0VOSLu5TEqi86X7q6IJ35O6I1IAY4AcpqvfvE3/f/qm4FgLADCMRL+LqeTdbdr9 +lLguOj9GNIkHQ5v96zYQ44vRnVNugetlUuHT1KZq/+wlaqDNuRZBU0gdJeL6wnDJ +6gNojKg7F0A0ry8F0B1Cn16uVxebjJMAx4N93hpQALkI2XyQNGHnOzO6eROqQl0i +j/csPW1CUfBUOHLaWpUKy483SOhAINsFz0pqK84G2gIItqTcuRksA/N1J1AYqqQO ++/8IK5Mb9j0RaYYrBG83luGCWYauAsWg2Yol6fUGju8IY/zavOaES42XogY588Ad +JzW+njjxXcnoD/u5keWrGwbPdGfoaLLg4eMlRBT4yNicyT04knXjFG4QTfLY5lF/ +VKdvZk6RMoCLdAtgN6EKHtcwuoYR967otsbavshngZ9HE/ic5/TdNFCBjxs6q9bm +docC4CLHU/feXvOCYSnIfUpDzEPV96Gbk6o0qeYn3RUSGzRpXQHxXXfszEESUWnd +2rtfXxqA7C5n8CshBfKJND7/LKRGpBRaYWJtc4hFmo8prhXfOb40PEZNlx8mcsEz +WYZpmvFQHU8+bZIm0a5RAoIBAQDaCAje9xLKN1CYzygA/U3x2CsGuWWyh9xM1oR5 +5t+nn0EeIMrzGuHrD4hdbZiTiJcO5dpSg/3dssc/QLJEdv+BoMEgSYTf3TX03dIb +kSlj+ONobejO4nVoUP0axTvVe/nuMYvLguTM6OCFvgV752TFxVyVHl6RM+rQYGCl +ajbBCsCRg4QgpZ/RHWf+3KMJunzwWBlsAXcjOudneYqEl713h/q1lc5cONIglQDU +E+bc5q6q++c/H8dYaWq4QE4CQU8wsq77/bZk8z1jheOV0HkwaH5ShtKD7bk/4MA9 +jWQUDW6/LRXkNiPExsAZxnP3mxhtUToWq1nedF6kPmNBko+9AoIBAQDHgvAql6i7 +osTYUcY/GldPmnrvfqbKmD0nI8mnaJfN2vGwjB/ol3lm+pviKIQ4ER80xsdn4xK0 +2cC9OdkY2UX7zilKohxvKVsbVOYpUwfpoRQO1Euddb6iAMqgGDyDzRBDTzTx5pB5 +XL9B/XuJVCMkjsNdD9iEbjdY1Epv7kYf53zfvrXdqv24uSNAszPFBLLPHSC9yONE +a/t3mHGZ2cjr52leGNGY7ib6GNGBUeA34SM9g97tU9pAgy712RfZhH6fA93CLk6T +DKoch56YId71vZt2J0Lrk4TWnnpidSoRmzKfVIJwjCmgYbI+2eDp7h0Z0DnDbji6 +9BPt3RWsoZidAoIBAA2A7+O3U7+Ye3JraiPdjGVNKSUKeIT9KyTLKHtQVEvSbjsK +dudlo9ZmKOD4d7mzfP+cNtBjgmanuvVs8V2SLTL/HNb+Fq+yyLO4xVmVvQWHFbaT +EBc4KWNjmLl+u7z2J72b7feVzMvwJG/EHBzXcQNavOgzcFH38DQls/aqxGdiXhjl +F1raRzKxao57ZdGlbjWIj1KEKLfS3yAmg/DAYSi1EE8MzzIhBsqjz+BStzq5Qtou +Ld1X/4W3SbfNq8cx+lCe0H2k8hYAhq3STg0qU0cvQZuk5Abtw0p0hhOJ3UfsqQ5I +IZH31HFMiftOskIEphenLzzWMgO4G2B6yLT3+dUCggEAOLF1i7Ti5sbfBtVd70qN +6vnr2yhzPvi5z+h0ghTPpliD+3YmDxMUFXY7W63FvKTo6DdgLJ4zD58dDOhmT5BW +ObKguyuLxu7Ki965NJ76jaIPMBOVlR4DWMe+zHV2pMFd0LKuSdsJzOLVGmxscV6u +SdIjo8s/7InhQmW47UuZM7G1I2NvDJltVdOON/F0UZT/NqmBR0zRf/zrTVXNWjmv +xZFRuMJ2tO1fuAvbZNMeUuKv/+f8LhZ424IrkwLoqw/iZ09S8b306AZeRJMpNvPR +BqWlipKnioe15MLN5jKDDNO8M9hw5Ih/v6pjW0bQicj3DgHEmEs25bE8BIihgxe8 +ZQKCAQEAsWKsUv13OEbYYAoJgbzDesWF9NzamFB0NLyno9SChvFPH/d8RmAuti7Y +BQUoBswLK24DF/TKf1YocsZq8tu+pnv0Nx1wtK4K+J3A1BYDm7ElpO3Km+HPUBtf +C9KGT5hotlMQVTpYSDG/QeWbfl4UnNZcbg8pmv38NwV1eDoVDfaVrRYJzQn75+Tf +s/WUq1x5PElR/4pNIU2i6pJGd6FimhRweJu/INR36spWmbMRNX8fyXx+9EBqMbVp +vS2xGgxxQT6bAvBfRlpgi87T9v5Gqoy6/jM/wX9smH9PfUV1vK32n3Zrbd46gwZW +p2aUlQOLXU9SjQTirZbdCZP0XHtFsg== +-----END PRIVATE KEY----- diff --git a/tests/ApkVerityTest/testdata/README.md b/tests/ApkVerityTest/testdata/README.md new file mode 100644 index 000000000000..163cb183a5ad --- /dev/null +++ b/tests/ApkVerityTest/testdata/README.md @@ -0,0 +1,13 @@ +This test only runs on rooted / debuggable device. + +The test tries to install subsets of base.{apk,dm}, split.{apk,dm} and their +corresponding .fsv_sig files (generated by build rule). If installed, the +tests also tries to tamper with the file at absolute disk offset to verify +if fs-verity is effective. + +How to generate dex metadata (.dm) +================================== + + adb shell profman --generate-test-profile=/data/local/tmp/primary.prof + adb pull /data/local/tmp/primary.prof + zip foo.dm primary.prof diff --git a/tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java b/tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java index 0d05044f2c25..7d750b7bf690 100644 --- a/tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java +++ b/tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java @@ -377,6 +377,8 @@ public class AppLaunch extends InstrumentationTestCase { AppLaunchResult launchResults = null; if (hasFailureOnFirstLaunch(launch)) { // skip if the app has failures while launched first + Log.w(TAG, "Has failures on first launch: " + launch.getApp()); + forceStopApp(launch.getApp()); continue; } AtraceLogger atraceLogger = null; diff --git a/tests/AppResourcesLoaders/Android.bp b/tests/AppResourcesLoaders/Android.bp new file mode 100644 index 000000000000..e5739dbf181c --- /dev/null +++ b/tests/AppResourcesLoaders/Android.bp @@ -0,0 +1,22 @@ +// +// Copyright (C) 2020 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: "AppResourcesLoaders", + srcs: ["**/*.java"], + sdk_version: "current", + java_resources: [":AppResourcesLoaders_Overlay"] +} diff --git a/tests/AppResourcesLoaders/AndroidManifest.xml b/tests/AppResourcesLoaders/AndroidManifest.xml new file mode 100644 index 000000000000..cb403b968abf --- /dev/null +++ b/tests/AppResourcesLoaders/AndroidManifest.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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.example.loaders"> + <application android:label="AppResourcesLoaders" + android:name=".LoadersApplication"> + <activity android:name=".LoaderActivity"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + <activity android:name=".LoaderActivityIsolated" + android:process="com.android.phone" /> + </application> +</manifest> diff --git a/tests/AppResourcesLoaders/Overlay/Android.bp b/tests/AppResourcesLoaders/Overlay/Android.bp new file mode 100644 index 000000000000..80443f6c3c00 --- /dev/null +++ b/tests/AppResourcesLoaders/Overlay/Android.bp @@ -0,0 +1,19 @@ +// +// Copyright (C) 2020 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: "AppResourcesLoaders_Overlay", +} diff --git a/tests/AppResourcesLoaders/Overlay/AndroidManifest.xml b/tests/AppResourcesLoaders/Overlay/AndroidManifest.xml new file mode 100644 index 000000000000..083ba37262fd --- /dev/null +++ b/tests/AppResourcesLoaders/Overlay/AndroidManifest.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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.example.loaders"> + <application android:hasCode="false" /> +</manifest> diff --git a/tests/AppResourcesLoaders/Overlay/res/values/values.xml b/tests/AppResourcesLoaders/Overlay/res/values/values.xml new file mode 100644 index 000000000000..8f6e462ffcae --- /dev/null +++ b/tests/AppResourcesLoaders/Overlay/res/values/values.xml @@ -0,0 +1,20 @@ +<!-- + ~ Copyright (C) 2020 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> + <string name="loader_present">Loaders present: true</string> + <public type="string" name="loader_present" id="0x7f010000" /> +</resources>
\ No newline at end of file diff --git a/tests/AppResourcesLoaders/res/layout/activity_isolated.xml b/tests/AppResourcesLoaders/res/layout/activity_isolated.xml new file mode 100644 index 000000000000..0a13f00633f6 --- /dev/null +++ b/tests/AppResourcesLoaders/res/layout/activity_isolated.xml @@ -0,0 +1,25 @@ +<!-- + ~ Copyright (C) 2020 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_height="match_parent" + android:layout_width="match_parent" + android:gravity="center" + android:orientation="vertical"> + + <TextView android:text="@string/loader_present" + style="@style/ButtonStyle"/> +</LinearLayout>
\ No newline at end of file diff --git a/tests/AppResourcesLoaders/res/layout/activity_main.xml b/tests/AppResourcesLoaders/res/layout/activity_main.xml new file mode 100644 index 000000000000..7277700cb14c --- /dev/null +++ b/tests/AppResourcesLoaders/res/layout/activity_main.xml @@ -0,0 +1,29 @@ +<!-- + ~ Copyright (C) 2020 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_height="match_parent" + android:layout_width="match_parent" + android:gravity="center" + android:orientation="vertical"> + + <TextView android:text="@string/loader_present" + style="@style/ButtonStyle"/> + + <Button android:id="@+id/btn_isolated_activity" + android:text="Launch Isolated Activity" + style="@style/ButtonStyle"/> +</LinearLayout>
\ No newline at end of file diff --git a/tests/AppResourcesLoaders/res/values/styles.xml b/tests/AppResourcesLoaders/res/values/styles.xml new file mode 100644 index 000000000000..ee73d65a21a6 --- /dev/null +++ b/tests/AppResourcesLoaders/res/values/styles.xml @@ -0,0 +1,23 @@ +<!-- + ~ Copyright (C) 2020 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="ButtonStyle" > + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">40dp</item> + <item name="android:layout_centerInParent">true</item> + </style> +</resources>
\ No newline at end of file diff --git a/tests/AppResourcesLoaders/res/values/values.xml b/tests/AppResourcesLoaders/res/values/values.xml new file mode 100644 index 000000000000..af128b6e0ac9 --- /dev/null +++ b/tests/AppResourcesLoaders/res/values/values.xml @@ -0,0 +1,20 @@ +<!-- + ~ Copyright (C) 2020 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> + <string name="loader_present">Loaders present: false</string> + <public type="string" name="loader_present" id="0x7f010000" /> +</resources>
\ No newline at end of file diff --git a/tests/AppResourcesLoaders/src/com/android/example/loaders/LoaderActivity.java b/tests/AppResourcesLoaders/src/com/android/example/loaders/LoaderActivity.java new file mode 100644 index 000000000000..49ef46f1c8de --- /dev/null +++ b/tests/AppResourcesLoaders/src/com/android/example/loaders/LoaderActivity.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2020 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.example.loaders; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +public class LoaderActivity extends Activity { + + @Override + protected void attachBaseContext(Context newBase) { + super.attachBaseContext(newBase); + final String loaderPresentOnAttach = + newBase.getResources().getString(R.string.loader_present); + if (loaderPresentOnAttach == null || !loaderPresentOnAttach.endsWith("true")) { + throw new AssertionError("Loader not present in attachBaseContext"); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + findViewById(R.id.btn_isolated_activity).setOnClickListener((v) -> { + startActivity(new Intent(this, LoaderActivityIsolated.class)); + }); + } +} diff --git a/tests/AppResourcesLoaders/src/com/android/example/loaders/LoaderActivityIsolated.java b/tests/AppResourcesLoaders/src/com/android/example/loaders/LoaderActivityIsolated.java new file mode 100644 index 000000000000..04550b980ea1 --- /dev/null +++ b/tests/AppResourcesLoaders/src/com/android/example/loaders/LoaderActivityIsolated.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2020 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.example.loaders; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; + +public class LoaderActivityIsolated extends Activity { + + @Override + protected void attachBaseContext(Context newBase) { + super.attachBaseContext(newBase); + final String loaderPresentOnAttach = + newBase.getResources().getString(R.string.loader_present); + if (loaderPresentOnAttach == null || !loaderPresentOnAttach.endsWith("true")) { + throw new AssertionError("Loader not present in attachBaseContext"); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_isolated); + } +} diff --git a/tests/AppResourcesLoaders/src/com/android/example/loaders/LoadersApplication.java b/tests/AppResourcesLoaders/src/com/android/example/loaders/LoadersApplication.java new file mode 100644 index 000000000000..709c208f4174 --- /dev/null +++ b/tests/AppResourcesLoaders/src/com/android/example/loaders/LoadersApplication.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2020 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.example.loaders; + +import android.app.Application; +import android.content.res.Resources; +import android.content.res.loader.ResourcesLoader; +import android.content.res.loader.ResourcesProvider; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +public class LoadersApplication extends Application { + private static final String TAG = "LoadersApplication"; + private static final String LOADER_RESOURCES_APK = "AppResourcesLoaders_Overlay.apk"; + + @Override + public void onCreate() { + try { + final Resources resources = getResources(); + final ResourcesLoader loader = new ResourcesLoader(); + loader.addProvider(ResourcesProvider.loadFromApk(copyResource(LOADER_RESOURCES_APK))); + resources.addLoaders(loader); + } catch (IOException e) { + throw new IllegalStateException("Failed to load loader resources ", e); + } + } + + private ParcelFileDescriptor copyResource(String fileName) throws IOException { + final File apkFile = new File(getFilesDir(), fileName); + final InputStream is = getClassLoader().getResourceAsStream(LOADER_RESOURCES_APK); + final FileOutputStream os = new FileOutputStream(apkFile); + byte[] buffer = new byte[8192]; + int count; + while ((count = is.read(buffer)) != -1) { + os.write(buffer, 0, count); + } + return ParcelFileDescriptor.open(apkFile, ParcelFileDescriptor.MODE_READ_ONLY); + } +} diff --git a/tests/AutoVerify/app1/Android.bp b/tests/AutoVerify/app1/Android.bp new file mode 100644 index 000000000000..548519fa653b --- /dev/null +++ b/tests/AutoVerify/app1/Android.bp @@ -0,0 +1,11 @@ +android_app { + name: "AutoVerifyTest", + srcs: ["src/**/*.java"], + resource_dirs: ["res"], + platform_apis: true, + min_sdk_version: "26", + target_sdk_version: "26", + optimize: { + enabled: false, + }, +} diff --git a/tests/AutoVerify/app1/AndroidManifest.xml b/tests/AutoVerify/app1/AndroidManifest.xml new file mode 100644 index 000000000000..d9caad490d82 --- /dev/null +++ b/tests/AutoVerify/app1/AndroidManifest.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2020 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.autoverify" > + + <uses-sdk android:targetSdkVersion="26" /> + + <application + android:label="@string/app_name" > + <activity + android:name=".MainActivity" + android:label="@string/app_name" > + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + + <intent-filter android:autoVerify="true"> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" /> + <data android:scheme="https" /> + <data android:host="explicit.example.com" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/tests/AutoVerify/app1/res/values/strings.xml b/tests/AutoVerify/app1/res/values/strings.xml new file mode 100644 index 000000000000..e234355041c6 --- /dev/null +++ b/tests/AutoVerify/app1/res/values/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright 2020 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> + <!-- app icon label, do not translate --> + <string name="app_name" translatable="false">AutoVerify Test</string> +</resources> diff --git a/tests/AutoVerify/app1/src/com/android/test/autoverify/MainActivity.java b/tests/AutoVerify/app1/src/com/android/test/autoverify/MainActivity.java new file mode 100644 index 000000000000..09ef47212622 --- /dev/null +++ b/tests/AutoVerify/app1/src/com/android/test/autoverify/MainActivity.java @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2020 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. + */ diff --git a/tests/AutoVerify/app2/Android.bp b/tests/AutoVerify/app2/Android.bp new file mode 100644 index 000000000000..1c6c97bdf350 --- /dev/null +++ b/tests/AutoVerify/app2/Android.bp @@ -0,0 +1,11 @@ +android_app { + name: "AutoVerifyTest2", + srcs: ["src/**/*.java"], + resource_dirs: ["res"], + platform_apis: true, + min_sdk_version: "26", + target_sdk_version: "26", + optimize: { + enabled: false, + }, +} diff --git a/tests/AutoVerify/app2/AndroidManifest.xml b/tests/AutoVerify/app2/AndroidManifest.xml new file mode 100644 index 000000000000..a00807883cfc --- /dev/null +++ b/tests/AutoVerify/app2/AndroidManifest.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2020 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.autoverify" > + + <uses-sdk android:targetSdkVersion="26" /> + + <application + android:label="@string/app_name" > + <activity + android:name=".MainActivity" + android:label="@string/app_name" > + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + + <intent-filter android:autoVerify="true"> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" /> + <data android:scheme="https" /> + <data android:host="explicit.example.com" /> + <data android:host="*.wildcard.tld" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/tests/AutoVerify/app2/res/values/strings.xml b/tests/AutoVerify/app2/res/values/strings.xml new file mode 100644 index 000000000000..e234355041c6 --- /dev/null +++ b/tests/AutoVerify/app2/res/values/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright 2020 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> + <!-- app icon label, do not translate --> + <string name="app_name" translatable="false">AutoVerify Test</string> +</resources> diff --git a/tests/AutoVerify/app2/src/com/android/test/autoverify/MainActivity.java b/tests/AutoVerify/app2/src/com/android/test/autoverify/MainActivity.java new file mode 100644 index 000000000000..09ef47212622 --- /dev/null +++ b/tests/AutoVerify/app2/src/com/android/test/autoverify/MainActivity.java @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2020 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. + */ diff --git a/tests/AutoVerify/app3/Android.bp b/tests/AutoVerify/app3/Android.bp new file mode 100644 index 000000000000..70a2b77d1000 --- /dev/null +++ b/tests/AutoVerify/app3/Android.bp @@ -0,0 +1,11 @@ +android_app { + name: "AutoVerifyTest3", + srcs: ["src/**/*.java"], + resource_dirs: ["res"], + platform_apis: true, + min_sdk_version: "26", + target_sdk_version: "26", + optimize: { + enabled: false, + }, +} diff --git a/tests/AutoVerify/app3/AndroidManifest.xml b/tests/AutoVerify/app3/AndroidManifest.xml new file mode 100644 index 000000000000..efaabc9a38d3 --- /dev/null +++ b/tests/AutoVerify/app3/AndroidManifest.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2020 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.autoverify" > + + <uses-sdk android:targetSdkVersion="26" /> + + <application + android:label="@string/app_name" > + <activity + android:name=".MainActivity" + android:label="@string/app_name" > + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + + <!-- does not request autoVerify --> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" /> + <data android:scheme="https" /> + <data android:host="explicit.example.com" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/tests/AutoVerify/app3/res/values/strings.xml b/tests/AutoVerify/app3/res/values/strings.xml new file mode 100644 index 000000000000..e234355041c6 --- /dev/null +++ b/tests/AutoVerify/app3/res/values/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright 2020 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> + <!-- app icon label, do not translate --> + <string name="app_name" translatable="false">AutoVerify Test</string> +</resources> diff --git a/tests/AutoVerify/app3/src/com/android/test/autoverify/MainActivity.java b/tests/AutoVerify/app3/src/com/android/test/autoverify/MainActivity.java new file mode 100644 index 000000000000..09ef47212622 --- /dev/null +++ b/tests/AutoVerify/app3/src/com/android/test/autoverify/MainActivity.java @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2020 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. + */ diff --git a/tests/AutoVerify/app4/Android.bp b/tests/AutoVerify/app4/Android.bp new file mode 100644 index 000000000000..fbdae1181a7a --- /dev/null +++ b/tests/AutoVerify/app4/Android.bp @@ -0,0 +1,11 @@ +android_app { + name: "AutoVerifyTest4", + srcs: ["src/**/*.java"], + resource_dirs: ["res"], + platform_apis: true, + min_sdk_version: "26", + target_sdk_version: "26", + optimize: { + enabled: false, + }, +} diff --git a/tests/AutoVerify/app4/AndroidManifest.xml b/tests/AutoVerify/app4/AndroidManifest.xml new file mode 100644 index 000000000000..1c975f8336c9 --- /dev/null +++ b/tests/AutoVerify/app4/AndroidManifest.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2020 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.autoverify" > + + <uses-sdk android:targetSdkVersion="26" /> + + <application + android:label="@string/app_name" > + <activity + android:name=".MainActivity" + android:label="@string/app_name" > + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + + <!-- intentionally does not autoVerify --> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" /> + <data android:scheme="https" /> + <data android:host="explicit.example.com" /> + <data android:host="*.wildcard.tld" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/tests/AutoVerify/app4/res/values/strings.xml b/tests/AutoVerify/app4/res/values/strings.xml new file mode 100644 index 000000000000..e234355041c6 --- /dev/null +++ b/tests/AutoVerify/app4/res/values/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright 2020 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> + <!-- app icon label, do not translate --> + <string name="app_name" translatable="false">AutoVerify Test</string> +</resources> diff --git a/tests/AutoVerify/app4/src/com/android/test/autoverify/MainActivity.java b/tests/AutoVerify/app4/src/com/android/test/autoverify/MainActivity.java new file mode 100644 index 000000000000..09ef47212622 --- /dev/null +++ b/tests/AutoVerify/app4/src/com/android/test/autoverify/MainActivity.java @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2020 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. + */ diff --git a/tests/BlobStoreTestUtils/Android.bp b/tests/BlobStoreTestUtils/Android.bp new file mode 100644 index 000000000000..829c883913e1 --- /dev/null +++ b/tests/BlobStoreTestUtils/Android.bp @@ -0,0 +1,20 @@ +// Copyright (C) 2020 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_library { + name: "BlobStoreTestUtils", + srcs: ["src/**/*.java"], + static_libs: ["truth-prebuilt"], + sdk_version: "test_current", +}
\ No newline at end of file diff --git a/tests/BlobStoreTestUtils/src/com/android/utils/blob/DummyBlobData.java b/tests/BlobStoreTestUtils/src/com/android/utils/blob/DummyBlobData.java new file mode 100644 index 000000000000..371375c0d932 --- /dev/null +++ b/tests/BlobStoreTestUtils/src/com/android/utils/blob/DummyBlobData.java @@ -0,0 +1,237 @@ +/* + * Copyright 2020 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.utils.blob; + +import static com.android.utils.blob.Utils.BUFFER_SIZE_BYTES; +import static com.android.utils.blob.Utils.copy; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.blob.BlobHandle; +import android.app.blob.BlobStoreManager; +import android.content.Context; +import android.os.FileUtils; +import android.os.ParcelFileDescriptor; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.RandomAccessFile; +import java.security.MessageDigest; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +public class DummyBlobData { + private static final long DEFAULT_SIZE_BYTES = 10 * 1024L * 1024L; + + private final Random mRandom; + private final File mFile; + private final long mFileSize; + private final CharSequence mLabel; + + byte[] mFileDigest; + long mExpiryTimeMs; + + private DummyBlobData(Builder builder) { + mRandom = new Random(builder.getRandomSeed()); + mFile = new File(builder.getContext().getFilesDir(), builder.getFileName()); + mFileSize = builder.getFileSize(); + mLabel = builder.getLabel(); + } + + public static class Builder { + private final Context mContext; + private int mRandomSeed = 0; + private long mFileSize = DEFAULT_SIZE_BYTES; + private CharSequence mLabel = "Test label"; + private String mFileName = "blob_" + System.nanoTime(); + + public Builder(Context context) { + mContext = context; + } + + public Context getContext() { + return mContext; + } + + public Builder setRandomSeed(int randomSeed) { + mRandomSeed = randomSeed; + return this; + } + + public int getRandomSeed() { + return mRandomSeed; + } + + public Builder setFileSize(long fileSize) { + mFileSize = fileSize; + return this; + } + + public long getFileSize() { + return mFileSize; + } + + public Builder setLabel(CharSequence label) { + mLabel = label; + return this; + } + + public CharSequence getLabel() { + return mLabel; + } + + public Builder setFileName(String fileName) { + mFileName = fileName; + return this; + } + + public String getFileName() { + return mFileName; + } + + public DummyBlobData build() { + return new DummyBlobData(this); + } + } + + public void prepare() throws Exception { + try (RandomAccessFile file = new RandomAccessFile(mFile, "rw")) { + writeRandomData(file, mFileSize); + } + mFileDigest = FileUtils.digest(mFile, "SHA-256"); + mExpiryTimeMs = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1); + } + + public BlobHandle getBlobHandle() throws Exception { + return BlobHandle.createWithSha256(mFileDigest, mLabel, + mExpiryTimeMs, "test_tag"); + } + + public long getFileSize() throws Exception { + return mFileSize; + } + + public long getExpiryTimeMillis() { + return mExpiryTimeMs; + } + + public void delete() { + mFile.delete(); + } + + public void writeToSession(BlobStoreManager.Session session) throws Exception { + writeToSession(session, 0, mFileSize); + } + + public void writeToSession(BlobStoreManager.Session session, + long offsetBytes, long lengthBytes) throws Exception { + try (FileInputStream in = new FileInputStream(mFile)) { + Utils.writeToSession(session, in, offsetBytes, lengthBytes); + } + } + + public void writeToFd(FileDescriptor fd, long offsetBytes, long lengthBytes) throws Exception { + try (FileInputStream in = new FileInputStream(mFile)) { + in.getChannel().position(offsetBytes); + try (FileOutputStream out = new FileOutputStream(fd)) { + copy(in, out, lengthBytes); + } + } + } + + public ParcelFileDescriptor openForRead() throws Exception { + return ParcelFileDescriptor.open(mFile, ParcelFileDescriptor.MODE_READ_ONLY); + } + + public void readFromSessionAndVerifyBytes(BlobStoreManager.Session session, + long offsetBytes, int lengthBytes) throws Exception { + final byte[] expectedBytes = new byte[lengthBytes]; + try (FileInputStream in = new FileInputStream(mFile)) { + read(in, expectedBytes, offsetBytes, lengthBytes); + } + + final byte[] actualBytes = new byte[lengthBytes]; + try (FileInputStream in = new ParcelFileDescriptor.AutoCloseInputStream( + session.openRead())) { + read(in, actualBytes, offsetBytes, lengthBytes); + } + + assertThat(actualBytes).isEqualTo(expectedBytes); + + } + + private void read(FileInputStream in, byte[] buffer, + long offsetBytes, int lengthBytes) throws Exception { + in.getChannel().position(offsetBytes); + in.read(buffer, 0, lengthBytes); + } + + public void readFromSessionAndVerifyDigest(BlobStoreManager.Session session) + throws Exception { + readFromSessionAndVerifyDigest(session, 0, mFile.length()); + } + + public void readFromSessionAndVerifyDigest(BlobStoreManager.Session session, + long offsetBytes, long lengthBytes) throws Exception { + final byte[] actualDigest; + try (FileInputStream in = new ParcelFileDescriptor.AutoCloseInputStream( + session.openRead())) { + actualDigest = createSha256Digest(in, offsetBytes, lengthBytes); + } + + assertThat(actualDigest).isEqualTo(mFileDigest); + } + + public void verifyBlob(ParcelFileDescriptor pfd) throws Exception { + final byte[] actualDigest; + try (FileInputStream in = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) { + actualDigest = FileUtils.digest(in, "SHA-256"); + } + assertThat(actualDigest).isEqualTo(mFileDigest); + } + + private byte[] createSha256Digest(FileInputStream in, long offsetBytes, long lengthBytes) + throws Exception { + final MessageDigest digest = MessageDigest.getInstance("SHA-256"); + in.getChannel().position(offsetBytes); + final byte[] buffer = new byte[BUFFER_SIZE_BYTES]; + long bytesRead = 0; + while (bytesRead < lengthBytes) { + int toRead = (bytesRead + buffer.length <= lengthBytes) + ? buffer.length : (int) (lengthBytes - bytesRead); + toRead = in.read(buffer, 0, toRead); + digest.update(buffer, 0, toRead); + bytesRead += toRead; + } + return digest.digest(); + } + + private void writeRandomData(RandomAccessFile file, long fileSize) + throws Exception { + long bytesWritten = 0; + final byte[] buffer = new byte[BUFFER_SIZE_BYTES]; + while (bytesWritten < fileSize) { + mRandom.nextBytes(buffer); + final int toWrite = (bytesWritten + buffer.length <= fileSize) + ? buffer.length : (int) (fileSize - bytesWritten); + file.seek(bytesWritten); + file.write(buffer, 0, toWrite); + bytesWritten += toWrite; + } + } +} diff --git a/tests/BlobStoreTestUtils/src/com/android/utils/blob/Utils.java b/tests/BlobStoreTestUtils/src/com/android/utils/blob/Utils.java new file mode 100644 index 000000000000..6927e86213d8 --- /dev/null +++ b/tests/BlobStoreTestUtils/src/com/android/utils/blob/Utils.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2020 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.utils.blob; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.blob.BlobHandle; +import android.app.blob.BlobStoreManager; +import android.app.blob.LeaseInfo; +import android.content.Context; +import android.content.res.Resources; +import android.os.ParcelFileDescriptor; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class Utils { + public static final int BUFFER_SIZE_BYTES = 16 * 1024; + + public static final long KB_IN_BYTES = 1000; + public static final long MB_IN_BYTES = KB_IN_BYTES * 1000; + + public static void copy(InputStream in, OutputStream out, long lengthBytes) + throws IOException { + final byte[] buffer = new byte[BUFFER_SIZE_BYTES]; + long bytesWrittern = 0; + while (bytesWrittern < lengthBytes) { + final int toWrite = (bytesWrittern + buffer.length <= lengthBytes) + ? buffer.length : (int) (lengthBytes - bytesWrittern); + in.read(buffer, 0, toWrite); + out.write(buffer, 0, toWrite); + bytesWrittern += toWrite; + } + } + + public static void writeToSession(BlobStoreManager.Session session, ParcelFileDescriptor input, + long lengthBytes) throws IOException { + try (FileInputStream in = new ParcelFileDescriptor.AutoCloseInputStream(input)) { + writeToSession(session, in, 0, lengthBytes); + } + } + + public static void writeToSession(BlobStoreManager.Session session, FileInputStream in, + long offsetBytes, long lengthBytes) throws IOException { + in.getChannel().position(offsetBytes); + try (FileOutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream( + session.openWrite(offsetBytes, lengthBytes))) { + copy(in, out, lengthBytes); + } + } + + public static void assertLeasedBlobs(BlobStoreManager blobStoreManager, + BlobHandle... expectedBlobHandles) throws IOException { + assertThat(blobStoreManager.getLeasedBlobs()).containsExactly(expectedBlobHandles); + } + + public static void assertNoLeasedBlobs(BlobStoreManager blobStoreManager) + throws IOException { + assertThat(blobStoreManager.getLeasedBlobs()).isEmpty(); + } + + public static void acquireLease(Context context, + BlobHandle blobHandle, CharSequence description) throws IOException { + final BlobStoreManager blobStoreManager = (BlobStoreManager) context.getSystemService( + Context.BLOB_STORE_SERVICE); + blobStoreManager.acquireLease(blobHandle, description); + + final LeaseInfo leaseInfo = blobStoreManager.getLeaseInfo(blobHandle); + assertLeaseInfo(leaseInfo, context.getPackageName(), 0, + Resources.ID_NULL, description); + } + + public static void acquireLease(Context context, + BlobHandle blobHandle, int descriptionResId) throws IOException { + final BlobStoreManager blobStoreManager = (BlobStoreManager) context.getSystemService( + Context.BLOB_STORE_SERVICE); + blobStoreManager.acquireLease(blobHandle, descriptionResId); + + final LeaseInfo leaseInfo = blobStoreManager.getLeaseInfo(blobHandle); + assertLeaseInfo(leaseInfo, context.getPackageName(), 0, + descriptionResId, context.getString(descriptionResId)); + } + + public static void acquireLease(Context context, + BlobHandle blobHandle, CharSequence description, + long expiryTimeMs) throws IOException { + final BlobStoreManager blobStoreManager = (BlobStoreManager) context.getSystemService( + Context.BLOB_STORE_SERVICE); + blobStoreManager.acquireLease(blobHandle, description, expiryTimeMs); + + final LeaseInfo leaseInfo = blobStoreManager.getLeaseInfo(blobHandle); + assertLeaseInfo(leaseInfo, context.getPackageName(), expiryTimeMs, + Resources.ID_NULL, description); + } + + public static void acquireLease(Context context, + BlobHandle blobHandle, int descriptionResId, + long expiryTimeMs) throws IOException { + final BlobStoreManager blobStoreManager = (BlobStoreManager) context.getSystemService( + Context.BLOB_STORE_SERVICE); + blobStoreManager.acquireLease(blobHandle, descriptionResId, expiryTimeMs); + + final LeaseInfo leaseInfo = blobStoreManager.getLeaseInfo(blobHandle); + assertLeaseInfo(leaseInfo, context.getPackageName(), expiryTimeMs, + descriptionResId, context.getString(descriptionResId)); + } + + public static void releaseLease(Context context, + BlobHandle blobHandle) throws IOException { + final BlobStoreManager blobStoreManager = (BlobStoreManager) context.getSystemService( + Context.BLOB_STORE_SERVICE); + blobStoreManager.releaseLease(blobHandle); + try { + assertThat(blobStoreManager.getLeaseInfo(blobHandle)).isNull(); + } catch (SecurityException e) { + // Expected, ignore + } + } + + private static void assertLeaseInfo(LeaseInfo leaseInfo, String packageName, + long expiryTimeMs, int descriptionResId, CharSequence description) { + assertThat(leaseInfo.getPackageName()).isEqualTo(packageName); + assertThat(leaseInfo.getExpiryTimeMillis()).isEqualTo(expiryTimeMs); + assertThat(leaseInfo.getDescriptionResId()).isEqualTo(descriptionResId); + assertThat(leaseInfo.getDescription()).isEqualTo(description); + } +} diff --git a/tests/BootImageProfileTest/AndroidTest.xml b/tests/BootImageProfileTest/AndroidTest.xml index d7f820411f27..7e97fa3a8ff1 100644 --- a/tests/BootImageProfileTest/AndroidTest.xml +++ b/tests/BootImageProfileTest/AndroidTest.xml @@ -23,6 +23,10 @@ <option name="force-skip-system-props" value="true" /> </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.DeviceCleaner"> + <option name="cleanup-action" value="REBOOT" /> + </target_preparer> + <test class="com.android.tradefed.testtype.HostTest" > <option name="class" value="com.android.bootimageprofile.BootImageProfileTest" /> </test> diff --git a/tests/BootImageProfileTest/src/com/android/bootimageprofile/BootImageProfileTest.java b/tests/BootImageProfileTest/src/com/android/bootimageprofile/BootImageProfileTest.java index 9a02bd405417..4ecca2dc4c39 100644 --- a/tests/BootImageProfileTest/src/com/android/bootimageprofile/BootImageProfileTest.java +++ b/tests/BootImageProfileTest/src/com/android/bootimageprofile/BootImageProfileTest.java @@ -155,6 +155,8 @@ public class BootImageProfileTest implements IDeviceTest { for (String line : res.split("\n")) { if (line.contains("framework.jar")) { sawFramework = true; + } else if (line.contains("framework-minus-apex.jar")) { + sawFramework = true; } else if (line.contains("services.jar")) { sawServices = true; } diff --git a/tests/Codegen/Android.bp b/tests/Codegen/Android.bp new file mode 100644 index 000000000000..966c5602959c --- /dev/null +++ b/tests/Codegen/Android.bp @@ -0,0 +1,25 @@ +android_test { + name: "CodegenTests", + srcs: [ + "**/*.java", + ], + + platform_apis: true, + test_suites: ["device-tests"], + certificate: "platform", + + optimize: { + enabled: false, + }, + + plugins: [ + "staledataclass-annotation-processor", + ], + static_libs: [ + "junit", + "hamcrest", + "hamcrest-library", + "androidx.test.runner", + "androidx.test.rules", + ], +} diff --git a/tests/Codegen/AndroidManifest.xml b/tests/Codegen/AndroidManifest.xml new file mode 100644 index 000000000000..2f1855035cd8 --- /dev/null +++ b/tests/Codegen/AndroidManifest.xml @@ -0,0 +1,26 @@ +<!-- + ~ 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 + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.codegentest"> + + <application/> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.codegentest" + android:label="Codegen test" /> +</manifest> diff --git a/tests/WindowManagerStressTest/res/values/styles.xml b/tests/Codegen/AndroidTest.xml index 0983b2535878..4dbbc5556c64 100644 --- a/tests/WindowManagerStressTest/res/values/styles.xml +++ b/tests/Codegen/AndroidTest.xml @@ -1,4 +1,5 @@ -<!-- Copyright (C) 2016 The Android Open Source Project +<?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. @@ -12,12 +13,11 @@ See the License for the specific language governing permissions and limitations under the License. --> -<resources> - <!-- Base application theme. --> - <style name="AppTheme" parent="@android:style/Theme.Material.Light.DarkActionBar"> - <!-- Customize your theme here. --> - <item name="android:colorPrimary">@color/colorPrimary</item> - <item name="android:colorPrimaryDark">@color/colorPrimaryDark</item> - <item name="android:colorAccent">@color/colorAccent</item> - </style> -</resources> +<configuration description="Runs Codegen Tests."> + + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.codegentest" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration> diff --git a/tests/Codegen/OWNERS b/tests/Codegen/OWNERS new file mode 100644 index 000000000000..da723b3b67da --- /dev/null +++ b/tests/Codegen/OWNERS @@ -0,0 +1 @@ +eugenesusla@google.com
\ No newline at end of file diff --git a/tests/Codegen/runTest.sh b/tests/Codegen/runTest.sh new file mode 100755 index 000000000000..31ab6d2ba46a --- /dev/null +++ b/tests/Codegen/runTest.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +if [[ "$0" = *"/Codegen/runTest.sh" ]]; then + #running in subshell - print code to eval and exit + echo "source $0" +else + function header_and_eval() { + printf "\n[ $* ]\n" 1>&2 + eval "$@" + return $? + } + + header_and_eval m -j16 codegen_cli && \ + header_and_eval codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/SampleDataClass.java && \ + header_and_eval codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/SampleWithCustomBuilder.java && \ + header_and_eval codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/ParcelAllTheThingsDataClass.java && \ + header_and_eval codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassBase.java && \ + header_and_eval codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassChild.java && \ + header_and_eval codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/StaleDataclassDetectorFalsePositivesTest.java && \ + header_and_eval codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/SampleWithNestedDataClasses.java && \ + ( + cd $ANDROID_BUILD_TOP && + header_and_eval mmma -j16 frameworks/base/tests/Codegen && \ + header_and_eval adb install -r -t "$(find $ANDROID_TARGET_OUT_TESTCASES -name 'CodegenTests.apk')" && \ + # header_and_eval adb shell am set-debug-app -w com.android.codegentest && \ + header_and_eval adb shell am instrument -w -e package com.android.codegentest com.android.codegentest/androidx.test.runner.AndroidJUnitRunner + ) + + exitCode=$? + + # header_and_eval adb shell am clear-debug-app + + return $exitCode +fi
\ No newline at end of file diff --git a/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassBase.aidl b/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassBase.aidl new file mode 100644 index 000000000000..ab62c83fc1b9 --- /dev/null +++ b/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassBase.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.codegentest; + +parcelable HierrarchicalDataClassBase; diff --git a/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassBase.java b/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassBase.java new file mode 100644 index 000000000000..475305eb83a2 --- /dev/null +++ b/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassBase.java @@ -0,0 +1,112 @@ +/* + * 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.codegentest; + +import android.os.Parcelable; + +import com.android.internal.util.DataClass; + +/** + * @see HierrarchicalDataClassChild + */ +@DataClass( + genConstructor = false, + genSetters = true) +public class HierrarchicalDataClassBase implements Parcelable { + + private int mBaseData; + + + + // Code below generated by codegen v1.0.15. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassBase.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + @DataClass.Generated.Member + public int getBaseData() { + return mBaseData; + } + + @DataClass.Generated.Member + public HierrarchicalDataClassBase setBaseData( int value) { + mBaseData = value; + return this; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@android.annotation.NonNull android.os.Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + dest.writeInt(mBaseData); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + protected HierrarchicalDataClassBase(@android.annotation.NonNull android.os.Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + int baseData = in.readInt(); + + this.mBaseData = baseData; + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @android.annotation.NonNull Parcelable.Creator<HierrarchicalDataClassBase> CREATOR + = new Parcelable.Creator<HierrarchicalDataClassBase>() { + @Override + public HierrarchicalDataClassBase[] newArray(int size) { + return new HierrarchicalDataClassBase[size]; + } + + @Override + public HierrarchicalDataClassBase createFromParcel(@android.annotation.NonNull android.os.Parcel in) { + return new HierrarchicalDataClassBase(in); + } + }; + + @DataClass.Generated( + time = 1582685650576L, + codegenVersion = "1.0.15", + sourceFile = "frameworks/base/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassBase.java", + inputSignatures = "private int mBaseData\nclass HierrarchicalDataClassBase extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=false, genSetters=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + +} diff --git a/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassChild.aidl b/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassChild.aidl new file mode 100644 index 000000000000..a0997222b0af --- /dev/null +++ b/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassChild.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.codegentest; + +parcelable HierrarchicalDataClassChild; diff --git a/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassChild.java b/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassChild.java new file mode 100644 index 000000000000..150b324d1a30 --- /dev/null +++ b/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassChild.java @@ -0,0 +1,134 @@ +/* + * 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.codegentest; + +import android.annotation.NonNull; +import android.os.Parcelable; + +import com.android.internal.util.DataClass; + +/** + * An example of data classes that extend one another. + * + * Note that some features like constructor generation might not work well due to lack of + * information about the superclass when generating code for subclass. + * + * It is recommended to avoid inheritance in favor of composition for new data classes, + * particularly parcelable ones. + * + * However for legacy classes or where inheritance is desired for allocation efficiency, + * you can either use a technique from this example, opting for mutability/setters, or just write + * constructors by hand. + * + * @see HierrarchicalDataClassBase + */ +@DataClass( + genParcelable = true, + genConstructor = false, + genSetters = true) +public class HierrarchicalDataClassChild extends HierrarchicalDataClassBase { + + private @NonNull String mChildData; + + + + // Code below generated by codegen v1.0.15. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassChild.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + @DataClass.Generated.Member + public @NonNull String getChildData() { + return mChildData; + } + + @DataClass.Generated.Member + public HierrarchicalDataClassChild setChildData(@NonNull String value) { + mChildData = value; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mChildData); + return this; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull android.os.Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + super.writeToParcel(dest, flags); + + dest.writeString(mChildData); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + protected HierrarchicalDataClassChild(@NonNull android.os.Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + super(in); + + String childData = in.readString(); + + this.mChildData = childData; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mChildData); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<HierrarchicalDataClassChild> CREATOR + = new Parcelable.Creator<HierrarchicalDataClassChild>() { + @Override + public HierrarchicalDataClassChild[] newArray(int size) { + return new HierrarchicalDataClassChild[size]; + } + + @Override + public HierrarchicalDataClassChild createFromParcel(@NonNull android.os.Parcel in) { + return new HierrarchicalDataClassChild(in); + } + }; + + @DataClass.Generated( + time = 1582685651560L, + codegenVersion = "1.0.15", + sourceFile = "frameworks/base/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassChild.java", + inputSignatures = "private @android.annotation.NonNull java.lang.String mChildData\nclass HierrarchicalDataClassChild extends com.android.codegentest.HierrarchicalDataClassBase implements []\n@com.android.internal.util.DataClass(genParcelable=true, genConstructor=false, genSetters=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + +} diff --git a/tests/Codegen/src/com/android/codegentest/MyDateParcelling.java b/tests/Codegen/src/com/android/codegentest/MyDateParcelling.java new file mode 100644 index 000000000000..4faeb8ee8893 --- /dev/null +++ b/tests/Codegen/src/com/android/codegentest/MyDateParcelling.java @@ -0,0 +1,51 @@ +/* + * 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.codegentest; + +import android.os.Parcel; + +import com.android.internal.util.Parcelling; + +import java.util.Date; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Sample {@link Parcelling} implementation for {@link Date}. + * + * See {@link SampleDataClass#mDate} for usage. + * See {@link SampleDataClass#writeToParcel} + {@link SampleDataClass#sParcellingForDate} + * for resulting generated code. + * + * Ignore {@link #sInstanceCount} - used for testing. + */ +public class MyDateParcelling implements Parcelling<Date> { + + static AtomicInteger sInstanceCount = new AtomicInteger(0); + + public MyDateParcelling() { + sInstanceCount.getAndIncrement(); + } + + @Override + public void parcel(Date item, Parcel dest, int parcelFlags) { + dest.writeLong(item.getTime()); + } + + @Override + public Date unparcel(Parcel source) { + return new Date(source.readLong()); + } +} diff --git a/tests/Codegen/src/com/android/codegentest/ParcelAllTheThingsDataClass.java b/tests/Codegen/src/com/android/codegentest/ParcelAllTheThingsDataClass.java new file mode 100644 index 000000000000..30871566c269 --- /dev/null +++ b/tests/Codegen/src/com/android/codegentest/ParcelAllTheThingsDataClass.java @@ -0,0 +1,426 @@ +/* + * 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.codegentest; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.ArrayMap; +import android.util.SparseArray; +import android.util.SparseIntArray; + +import com.android.internal.util.AnnotationValidations; +import com.android.internal.util.DataClass; + +import java.util.List; +import java.util.Map; + +/** + * Additional test for various parcelling corner-cases. + */ +@DataClass( + genBuilder = true, + genAidl = false, + genToString = true) +public class ParcelAllTheThingsDataClass implements Parcelable { + + @NonNull String[] mStringArray = null; + @NonNull int[] mIntArray = null; + @NonNull List<String> mStringList = null; + + @NonNull Map<String, SampleWithCustomBuilder> mMap = null; + @NonNull Map<String, String> mStringMap = null; + + @NonNull SparseArray<SampleWithCustomBuilder> mSparseArray = null; + @NonNull SparseIntArray mSparseIntArray = null; + + @SuppressWarnings({"WeakerAccess"}) + @Nullable Boolean mNullableBoolean = null; + + + + + // Code below generated by codegen v1.0.15. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/ParcelAllTheThingsDataClass.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + @DataClass.Generated.Member + /* package-private */ ParcelAllTheThingsDataClass( + @NonNull String[] stringArray, + @NonNull int[] intArray, + @NonNull List<String> stringList, + @NonNull Map<String,SampleWithCustomBuilder> map, + @NonNull Map<String,String> stringMap, + @NonNull SparseArray<SampleWithCustomBuilder> sparseArray, + @NonNull SparseIntArray sparseIntArray, + @SuppressWarnings({ "WeakerAccess" }) @Nullable Boolean nullableBoolean) { + this.mStringArray = stringArray; + AnnotationValidations.validate( + NonNull.class, null, mStringArray); + this.mIntArray = intArray; + AnnotationValidations.validate( + NonNull.class, null, mIntArray); + this.mStringList = stringList; + AnnotationValidations.validate( + NonNull.class, null, mStringList); + this.mMap = map; + AnnotationValidations.validate( + NonNull.class, null, mMap); + this.mStringMap = stringMap; + AnnotationValidations.validate( + NonNull.class, null, mStringMap); + this.mSparseArray = sparseArray; + AnnotationValidations.validate( + NonNull.class, null, mSparseArray); + this.mSparseIntArray = sparseIntArray; + AnnotationValidations.validate( + NonNull.class, null, mSparseIntArray); + this.mNullableBoolean = nullableBoolean; + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public @NonNull String[] getStringArray() { + return mStringArray; + } + + @DataClass.Generated.Member + public @NonNull int[] getIntArray() { + return mIntArray; + } + + @DataClass.Generated.Member + public @NonNull List<String> getStringList() { + return mStringList; + } + + @DataClass.Generated.Member + public @NonNull Map<String,SampleWithCustomBuilder> getMap() { + return mMap; + } + + @DataClass.Generated.Member + public @NonNull Map<String,String> getStringMap() { + return mStringMap; + } + + @DataClass.Generated.Member + public @NonNull SparseArray<SampleWithCustomBuilder> getSparseArray() { + return mSparseArray; + } + + @DataClass.Generated.Member + public @NonNull SparseIntArray getSparseIntArray() { + return mSparseIntArray; + } + + @DataClass.Generated.Member + public @SuppressWarnings({ "WeakerAccess" }) @Nullable Boolean getNullableBoolean() { + return mNullableBoolean; + } + + @Override + @DataClass.Generated.Member + public String toString() { + // You can override field toString logic by defining methods like: + // String fieldNameToString() { ... } + + return "ParcelAllTheThingsDataClass { " + + "stringArray = " + java.util.Arrays.toString(mStringArray) + ", " + + "intArray = " + java.util.Arrays.toString(mIntArray) + ", " + + "stringList = " + mStringList + ", " + + "map = " + mMap + ", " + + "stringMap = " + mStringMap + ", " + + "sparseArray = " + mSparseArray + ", " + + "sparseIntArray = " + mSparseIntArray + ", " + + "nullableBoolean = " + mNullableBoolean + + " }"; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + int flg = 0; + if (mNullableBoolean != null) flg |= 0x80; + dest.writeInt(flg); + dest.writeStringArray(mStringArray); + dest.writeIntArray(mIntArray); + dest.writeStringList(mStringList); + dest.writeMap(mMap); + dest.writeMap(mStringMap); + dest.writeSparseArray(mSparseArray); + dest.writeSparseIntArray(mSparseIntArray); + if (mNullableBoolean != null) dest.writeBoolean(mNullableBoolean); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + protected ParcelAllTheThingsDataClass(@NonNull Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + int flg = in.readInt(); + String[] stringArray = in.createStringArray(); + int[] intArray = in.createIntArray(); + List<String> stringList = new java.util.ArrayList<>(); + in.readStringList(stringList); + Map<String,SampleWithCustomBuilder> map = new java.util.LinkedHashMap<>(); + in.readMap(map, SampleWithCustomBuilder.class.getClassLoader()); + Map<String,String> stringMap = new java.util.LinkedHashMap<>(); + in.readMap(stringMap, String.class.getClassLoader()); + SparseArray<SampleWithCustomBuilder> sparseArray = (SparseArray) in.readSparseArray(SampleWithCustomBuilder.class.getClassLoader()); + SparseIntArray sparseIntArray = (SparseIntArray) in.readSparseIntArray(); + Boolean nullableBoolean = (flg & 0x80) == 0 ? null : (Boolean) in.readBoolean(); + + this.mStringArray = stringArray; + AnnotationValidations.validate( + NonNull.class, null, mStringArray); + this.mIntArray = intArray; + AnnotationValidations.validate( + NonNull.class, null, mIntArray); + this.mStringList = stringList; + AnnotationValidations.validate( + NonNull.class, null, mStringList); + this.mMap = map; + AnnotationValidations.validate( + NonNull.class, null, mMap); + this.mStringMap = stringMap; + AnnotationValidations.validate( + NonNull.class, null, mStringMap); + this.mSparseArray = sparseArray; + AnnotationValidations.validate( + NonNull.class, null, mSparseArray); + this.mSparseIntArray = sparseIntArray; + AnnotationValidations.validate( + NonNull.class, null, mSparseIntArray); + this.mNullableBoolean = nullableBoolean; + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<ParcelAllTheThingsDataClass> CREATOR + = new Parcelable.Creator<ParcelAllTheThingsDataClass>() { + @Override + public ParcelAllTheThingsDataClass[] newArray(int size) { + return new ParcelAllTheThingsDataClass[size]; + } + + @Override + public ParcelAllTheThingsDataClass createFromParcel(@NonNull Parcel in) { + return new ParcelAllTheThingsDataClass(in); + } + }; + + /** + * A builder for {@link ParcelAllTheThingsDataClass} + */ + @SuppressWarnings("WeakerAccess") + @DataClass.Generated.Member + public static class Builder { + + private @NonNull String[] mStringArray; + private @NonNull int[] mIntArray; + private @NonNull List<String> mStringList; + private @NonNull Map<String,SampleWithCustomBuilder> mMap; + private @NonNull Map<String,String> mStringMap; + private @NonNull SparseArray<SampleWithCustomBuilder> mSparseArray; + private @NonNull SparseIntArray mSparseIntArray; + private @SuppressWarnings({ "WeakerAccess" }) @Nullable Boolean mNullableBoolean; + + private long mBuilderFieldsSet = 0L; + + public Builder() { + } + + @DataClass.Generated.Member + public @NonNull Builder setStringArray(@NonNull String... value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x1; + mStringArray = value; + return this; + } + + @DataClass.Generated.Member + public @NonNull Builder setIntArray(@NonNull int... value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x2; + mIntArray = value; + return this; + } + + @DataClass.Generated.Member + public @NonNull Builder setStringList(@NonNull List<String> value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x4; + mStringList = value; + return this; + } + + /** @see #setStringList */ + @DataClass.Generated.Member + public @NonNull Builder addStringList(@NonNull String value) { + // You can refine this method's name by providing item's singular name, e.g.: + // @DataClass.PluralOf("item")) mItems = ... + + if (mStringList == null) setStringList(new java.util.ArrayList<>()); + mStringList.add(value); + return this; + } + + @DataClass.Generated.Member + public @NonNull Builder setMap(@NonNull Map<String,SampleWithCustomBuilder> value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x8; + mMap = value; + return this; + } + + /** @see #setMap */ + @DataClass.Generated.Member + public @NonNull Builder addMap(@NonNull String key, @NonNull SampleWithCustomBuilder value) { + // You can refine this method's name by providing item's singular name, e.g.: + // @DataClass.PluralOf("item")) mItems = ... + + if (mMap == null) setMap(new java.util.LinkedHashMap()); + mMap.put(key, value); + return this; + } + + @DataClass.Generated.Member + public @NonNull Builder setStringMap(@NonNull Map<String,String> value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x10; + mStringMap = value; + return this; + } + + /** @see #setStringMap */ + @DataClass.Generated.Member + public @NonNull Builder addStringMap(@NonNull String key, @NonNull String value) { + // You can refine this method's name by providing item's singular name, e.g.: + // @DataClass.PluralOf("item")) mItems = ... + + if (mStringMap == null) setStringMap(new java.util.LinkedHashMap()); + mStringMap.put(key, value); + return this; + } + + @DataClass.Generated.Member + public @NonNull Builder setSparseArray(@NonNull SparseArray<SampleWithCustomBuilder> value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x20; + mSparseArray = value; + return this; + } + + @DataClass.Generated.Member + public @NonNull Builder setSparseIntArray(@NonNull SparseIntArray value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x40; + mSparseIntArray = value; + return this; + } + + @DataClass.Generated.Member + public @NonNull Builder setNullableBoolean(@SuppressWarnings({ "WeakerAccess" }) @NonNull Boolean value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x80; + mNullableBoolean = value; + return this; + } + + /** Builds the instance. This builder should not be touched after calling this! */ + public @NonNull ParcelAllTheThingsDataClass build() { + checkNotUsed(); + mBuilderFieldsSet |= 0x100; // Mark builder used + + if ((mBuilderFieldsSet & 0x1) == 0) { + mStringArray = null; + } + if ((mBuilderFieldsSet & 0x2) == 0) { + mIntArray = null; + } + if ((mBuilderFieldsSet & 0x4) == 0) { + mStringList = null; + } + if ((mBuilderFieldsSet & 0x8) == 0) { + mMap = null; + } + if ((mBuilderFieldsSet & 0x10) == 0) { + mStringMap = null; + } + if ((mBuilderFieldsSet & 0x20) == 0) { + mSparseArray = null; + } + if ((mBuilderFieldsSet & 0x40) == 0) { + mSparseIntArray = null; + } + if ((mBuilderFieldsSet & 0x80) == 0) { + mNullableBoolean = null; + } + ParcelAllTheThingsDataClass o = new ParcelAllTheThingsDataClass( + mStringArray, + mIntArray, + mStringList, + mMap, + mStringMap, + mSparseArray, + mSparseIntArray, + mNullableBoolean); + return o; + } + + private void checkNotUsed() { + if ((mBuilderFieldsSet & 0x100) != 0) { + throw new IllegalStateException( + "This Builder should not be reused. Use a new Builder instance instead"); + } + } + } + + @DataClass.Generated( + time = 1582685649678L, + codegenVersion = "1.0.15", + sourceFile = "frameworks/base/tests/Codegen/src/com/android/codegentest/ParcelAllTheThingsDataClass.java", + inputSignatures = " @android.annotation.NonNull java.lang.String[] mStringArray\n @android.annotation.NonNull int[] mIntArray\n @android.annotation.NonNull java.util.List<java.lang.String> mStringList\n @android.annotation.NonNull java.util.Map<java.lang.String,com.android.codegentest.SampleWithCustomBuilder> mMap\n @android.annotation.NonNull java.util.Map<java.lang.String,java.lang.String> mStringMap\n @android.annotation.NonNull android.util.SparseArray<com.android.codegentest.SampleWithCustomBuilder> mSparseArray\n @android.annotation.NonNull android.util.SparseIntArray mSparseIntArray\n @java.lang.SuppressWarnings({\"WeakerAccess\"}) @android.annotation.Nullable java.lang.Boolean mNullableBoolean\nclass ParcelAllTheThingsDataClass extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genBuilder=true, genAidl=false, genToString=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + +} diff --git a/tests/Codegen/src/com/android/codegentest/SampleDataClass.aidl b/tests/Codegen/src/com/android/codegentest/SampleDataClass.aidl new file mode 100644 index 000000000000..f14d47cde13f --- /dev/null +++ b/tests/Codegen/src/com/android/codegentest/SampleDataClass.aidl @@ -0,0 +1,18 @@ +/* + * 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.codegentest; + +parcelable SampleDataClass; diff --git a/tests/Codegen/src/com/android/codegentest/SampleDataClass.java b/tests/Codegen/src/com/android/codegentest/SampleDataClass.java new file mode 100644 index 000000000000..8d421bfa492a --- /dev/null +++ b/tests/Codegen/src/com/android/codegentest/SampleDataClass.java @@ -0,0 +1,1886 @@ +/* + * 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.codegentest; + +import android.annotation.FloatRange; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.Size; +import android.annotation.StringDef; +import android.annotation.StringRes; +import android.annotation.UserIdInt; +import android.net.LinkAddress; +import android.os.Parcel; +import android.os.Parcelable; +import android.view.accessibility.AccessibilityNodeInfo; + +import com.android.internal.util.AnnotationValidations; +import com.android.internal.util.DataClass; +import com.android.internal.util.DataClass.Each; +import com.android.internal.util.Parcelling; +import com.android.internal.util.Preconditions; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * Sample data class, showing off various code generation features. + * + * See javadoc on non-generated code for the explanation of the various features. + * + * See {@link SampleDataClassTest} for various invariants the generated code is expected to hold. + */ +@DataClass( +// genParcelable = true, // implied by `implements Parcelable` +// genAidl = true, // implied by `implements Parcelable` +// genGetters = true, // on by default +// genConstDefs = true, // implied by presence of constants with common prefix + genBuilder = true, // on by default if optional fields present, but suppressed by + // genConstructor + genConstructor = true, // on by default but normally suppressed by genBuilder + genEqualsHashCode = true, + genToString = true, + genForEachField = true, + genSetters = true +) +public final class SampleDataClass implements Parcelable { + + /** + * For any group of {@link int} or {@link String} constants like these, a corresponding + * {@link IntDef}/{@link StringDef} will get generated, with name based on common prefix + * by default. + * + * When {@link #SampleDataClass constructing} an instance, fields annotated with these + * annotations get automatically validated, with only provided constants being a valid value. + * + * @see StateName, the generated {@link StringDef} + * @see #mStateName annotated with {@link StateName} + */ + public static final String STATE_NAME_UNDEFINED = "?"; + public static final String STATE_NAME_ON = "on"; + public static final String STATE_NAME_OFF = "off"; + + /** + * Additionally, for any generated {@link IntDef} a corresponding static + * *ToString method will be also generated, and used in {@link #toString()}. + * + * @see #stateToString(int) + * @see #toString() + * @see State + */ + public static final int STATE_UNDEFINED = -1; + public static final int STATE_ON = 1; + public static final int STATE_OFF = 0; + + /** + * {@link IntDef}s with values specified in hex("0x...") are considered to be + * {@link IntDef#flag flags}, while ones specified with regular int literals are considered + * not to be flags. + * + * This affects their string representation, e.g. see the difference in + * {@link #requestFlagsToString} vs {@link #stateToString}. + * + * This also affects the validation logic when {@link #SampleDataClass constructing} + * an instance, with any flag combination("|") being valid. + * + * You can customize the name of the generated {@link IntDef}/{@link StringDef} annotation + * by annotating each constant with the desired name before running the generation. + * + * Here the annotation is named {@link RequestFlags} instead of the default {@code Flags}. + */ + public static final @RequestFlags int FLAG_MANUAL_REQUEST = 0x1; + public static final @RequestFlags int FLAG_COMPATIBILITY_MODE_REQUEST = 0x2; + public static final @RequestFlags int FLAG_AUGMENTED_REQUEST = 0x80000000; + + + /** + * Any property javadoc should go onto the field, and will be copied where appropriate, + * including getters, constructor parameters, builder setters, etc. + * + * <p> + * This allows to avoid the burden of maintaining copies of the same documentation + * pieces in multiple places for each field. + */ + private int mNum; + /** + * Various javadoc features should work as expected when copied, e.g {@code code}, + * {@link #mName links}, <a href="https://google.com">html links</a>, etc. + * + * @see #mNum2 ..and so should blocks at the bottom, e.g. {@code @see} blocks. + */ + private int mNum2; + /** + * {@code @hide} javadoc annotation is also propagated, which can be used to adjust the + * desired public API surface. + * + * @see #getNum4() is hidden + * @see Builder#setNum4(int) also hidden + * @hide + */ + private int mNum4; + + /** + * {@link Nullable} or {@link NonNull} annotation is required on all non-primitive fields. + */ + private @Nullable String mName; + /** + * Fields with default value expressions ("mFoo = ...") are optional, and are automatically + * initialized to the provided default expression, unless explicitly set. + * + * When using a {@link Builder} optional fields are passed via a {@link Builder#setName2 setter} + * while mandatory fields are passed via {@link Builder#Builder constructor}. + */ + private @NonNull String mName2 = "Bob"; + /** + * Alternatively, when default value computation is expensive, + * {@link #defaultName4 defaultFieldName()} can be defined to compute the default value. + */ + private @NonNull String mName4; + private static String defaultName4() { + // Expensive computation + return "Bob4"; + } + + /** + * For parcelling, any field type supported by {@link Parcel} is supported out of the box. + * E.g. {@link Parcelable} subclasses, {@link String}, {@link int}, {@link boolean}, etc. + */ + private @Nullable AccessibilityNodeInfo mOtherParcelable = null; + /** + * Additionally, support for parcelling other types can be added by implementing a + * {@link Parcelling}, and referencing it in the {@link DataClass.ParcelWith} field annotation. + * + * @see MyDateParcelling an example {@link Parcelling} implementation + */ + @DataClass.ParcelWith(MyDateParcelling.class) + private @NonNull Date mDate = new Date(42 * 42); + /** + * If a {@link Parcelling} is fairly common, consider putting it in {@link Parcelling.BuiltIn} + * to encourage its reuse. + */ + @DataClass.ParcelWith(Parcelling.BuiltIn.ForPattern.class) + private @NonNull Pattern mPattern = Pattern.compile(""); + + /** + * For lists, when using a {@link Builder}, other than a regular + * {@link Builder#setLinkAddresses2(List) setter}, and additional + * {@link Builder#addLinkAddresses2(LinkAddress) add} method is generated for convenience. + */ + private @NonNull List<LinkAddress> mLinkAddresses2 = new ArrayList<>(); + /** + * For aesthetics, you may want to consider providing a singular version of the plural field + * name, which would be used for the {@link #mLinkAddresses2 above mentioned} "add" method. + * + * @see Builder#addLinkAddress(LinkAddress) + */ + @DataClass.PluralOf("linkAddress") + private @NonNull ArrayList<LinkAddress> mLinkAddresses = new ArrayList<>(); + /** + * For array fields, when using a {@link Builder}, vararg argument format is used for + * convenience. + * + * @see Builder#setLinkAddresses4(LinkAddress...) + */ + private @Nullable LinkAddress[] mLinkAddresses4 = null; + + /** + * {@link IntDef}/{@link StringDef}-annotated fields propagate the annotation to + * getter/constructor/setter/builder parameters, making for a nicer api. + * + * @see #getStateName + * @see Builder#setStateName + */ + private @StateName @NonNull String mStateName = STATE_NAME_UNDEFINED; + /** + * Fields annotated with {@link IntDef} annotations also get a proper {@link #toString()} value. + */ + private @RequestFlags int mFlags; + /** + * Above is true for both {@link IntDef#flag flags} and enum-like {@link IntDef}s + */ + private @State int mState = STATE_UNDEFINED; + + + /** + * Making a field public will suppress getter generation in favor of accessing it directly. + */ + public @NonNull CharSequence charSeq = ""; + /** + * Final fields suppress generating a setter (when setters are requested). + */ + private final @Nullable LinkAddress[] mLinkAddresses5; + /** + * Transient fields are completely ignored and can be used for caching. + */ + private transient LinkAddress[] mLinkAddresses6; + /** + * When using transient fields for caching it's often also a good idea to initialize them + * lazily. + * + * You can declare a special method like {@link #lazyInitTmpStorage()}, to let the + * {@link #getTmpStorage getter} lazily-initialize the value on demand. + */ + transient int[] mTmpStorage; + private int[] lazyInitTmpStorage() { + return new int[100]; + } + + /** + * Fields with certain annotations are automatically validated in constructor + * + * You can see overloads in {@link AnnotationValidations} for a list of currently + * supported ones. + * + * You can also extend support to your custom annotations by creating another corresponding + * overloads like + * {@link AnnotationValidations#validate(Class, UserIdInt, int)}. + * + * @see #SampleDataClass + */ + private @StringRes int mStringRes = 0; + /** + * Validation annotations may also have parameters. + * + * Parameter values will be supplied to validation method as name-value pairs. + * + * @see AnnotationValidations#validate(Class, Size, int, String, int, String, int) + */ + private @android.annotation.IntRange(from = 0, to = 6) int mDayOfWeek = 3; + /** + * Unnamed validation annotation parameter gets supplied to the validating method named as + * "value". + * + * Validation annotations following {@link Each} annotation, will be applied for each + * array/collection element instead. + * + * @see AnnotationValidations#validate(Class, Size, int, String, int) + */ + @Size(2) + @NonNull + @Each @FloatRange(from = 0f) + private float[] mCoords = new float[] {0f, 0f}; + + + /** + * Manually declaring any method that would otherwise be generated suppresses its generation, + * allowing for fine-grained overrides of the generated behavior. + */ + public LinkAddress[] getLinkAddresses4() { + //Suppress autogen + return null; + } + + /** + * Additionally, some methods like {@link #equals}, {@link #hashCode}, {@link #toString}, + * {@link #writeToParcel}, {@link Parcelable.Creator#createFromParcel} allow you to define + * special methods to override their behavior on a per-field basis. + * + * See the generateted methods' descriptions for the detailed instructions of what the method + * signatures for such methods are expected to be. + * + * Here we use this to "fix" {@link Pattern} not implementing equals/hashCode. + * + * @see #equals + * @see #hashCode + */ + private boolean patternEquals(Pattern other) { + return Objects.equals(mPattern.pattern(), other.pattern()); + } + private int patternHashCode() { + return Objects.hashCode(mPattern.pattern()); + } + + /** + * Similarly, {@link #onConstructed()}, if defined, gets called at the end of constructing an + * instance. + * + * At this point all fields should be in place, so this is the right place to put any custom + * validation logic. + */ + private void onConstructed() { + Preconditions.checkState(mNum2 == mNum4); + } + + /** + * {@link DataClass#genForEachField} can be used to generate a generic {@link #forEachField} + * utility, which can be used for various use-cases not covered out of the box. + * Callback passed to {@link #forEachField} will be called once per each property with its name + * and value. + * + * Here for example it's used to implement a typical dump method. + * + * Note that there are 2 {@link #forEachField} versions provided, one that treats each field + * value as an {@link Object}, thus boxing primitives if any, and one that additionally takes + * specialized callbacks for particular primitive field types used in given class. + * + * Some primitives like {@link Boolean}s and {@link Integer}s within [-128, 127] don't allocate + * when boxed, so it's up to you to decide which one to use for a given use-case. + */ + public void dump(PrintWriter pw) { + forEachField((self, name, value) -> { + pw.append(" ").append(name).append(": ").append(String.valueOf(value)).append('\n'); + }); + } + + + + // Code below generated by codegen v1.0.15. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/SampleDataClass.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + @IntDef(prefix = "STATE_", value = { + STATE_UNDEFINED, + STATE_ON, + STATE_OFF + }) + @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) + @DataClass.Generated.Member + public @interface State {} + + @DataClass.Generated.Member + public static String stateToString(@State int value) { + switch (value) { + case STATE_UNDEFINED: + return "STATE_UNDEFINED"; + case STATE_ON: + return "STATE_ON"; + case STATE_OFF: + return "STATE_OFF"; + default: return Integer.toHexString(value); + } + } + + @IntDef(flag = true, prefix = "FLAG_", value = { + FLAG_MANUAL_REQUEST, + FLAG_COMPATIBILITY_MODE_REQUEST, + FLAG_AUGMENTED_REQUEST + }) + @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) + @DataClass.Generated.Member + public @interface RequestFlags {} + + @DataClass.Generated.Member + public static String requestFlagsToString(@RequestFlags int value) { + return com.android.internal.util.BitUtils.flagsToString( + value, SampleDataClass::singleRequestFlagsToString); + } + + @DataClass.Generated.Member + static String singleRequestFlagsToString(@RequestFlags int value) { + switch (value) { + case FLAG_MANUAL_REQUEST: + return "FLAG_MANUAL_REQUEST"; + case FLAG_COMPATIBILITY_MODE_REQUEST: + return "FLAG_COMPATIBILITY_MODE_REQUEST"; + case FLAG_AUGMENTED_REQUEST: + return "FLAG_AUGMENTED_REQUEST"; + default: return Integer.toHexString(value); + } + } + + @StringDef(prefix = "STATE_NAME_", value = { + STATE_NAME_UNDEFINED, + STATE_NAME_ON, + STATE_NAME_OFF + }) + @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) + @DataClass.Generated.Member + public @interface StateName {} + + /** + * Creates a new SampleDataClass. + * + * @param num + * Any property javadoc should go onto the field, and will be copied where appropriate, + * including getters, constructor parameters, builder setters, etc. + * + * <p> + * This allows to avoid the burden of maintaining copies of the same documentation + * pieces in multiple places for each field. + * @param num2 + * Various javadoc features should work as expected when copied, e.g {@code code}, + * {@link #mName links}, <a href="https://google.com">html links</a>, etc. + * @param num4 + * {@code @hide} javadoc annotation is also propagated, which can be used to adjust the + * desired public API surface. + * @param name + * {@link Nullable} or {@link NonNull} annotation is required on all non-primitive fields. + * @param name2 + * Fields with default value expressions ("mFoo = ...") are optional, and are automatically + * initialized to the provided default expression, unless explicitly set. + * + * When using a {@link Builder} optional fields are passed via a {@link Builder#setName2 setter} + * while mandatory fields are passed via {@link Builder#Builder constructor}. + * @param name4 + * Alternatively, when default value computation is expensive, + * {@link #defaultName4 defaultFieldName()} can be defined to compute the default value. + * @param otherParcelable + * For parcelling, any field type supported by {@link Parcel} is supported out of the box. + * E.g. {@link Parcelable} subclasses, {@link String}, {@link int}, {@link boolean}, etc. + * @param date + * Additionally, support for parcelling other types can be added by implementing a + * {@link Parcelling}, and referencing it in the {@link DataClass.ParcelWith} field annotation. + * @param pattern + * If a {@link Parcelling} is fairly common, consider putting it in {@link Parcelling.BuiltIn} + * to encourage its reuse. + * @param linkAddresses2 + * For lists, when using a {@link Builder}, other than a regular + * {@link Builder#setLinkAddresses2(List) setter}, and additional + * {@link Builder#addLinkAddresses2(LinkAddress) add} method is generated for convenience. + * @param linkAddresses + * For aesthetics, you may want to consider providing a singular version of the plural field + * name, which would be used for the {@link #mLinkAddresses2 above mentioned} "add" method. + * @param linkAddresses4 + * For array fields, when using a {@link Builder}, vararg argument format is used for + * convenience. + * @param stateName + * {@link IntDef}/{@link StringDef}-annotated fields propagate the annotation to + * getter/constructor/setter/builder parameters, making for a nicer api. + * @param flags + * Fields annotated with {@link IntDef} annotations also get a proper {@link #toString()} value. + * @param state + * Above is true for both {@link IntDef#flag flags} and enum-like {@link IntDef}s + * @param charSeq + * Making a field public will suppress getter generation in favor of accessing it directly. + * @param linkAddresses5 + * Final fields suppress generating a setter (when setters are requested). + * @param stringRes + * Fields with certain annotations are automatically validated in constructor + * + * You can see overloads in {@link AnnotationValidations} for a list of currently + * supported ones. + * + * You can also extend support to your custom annotations by creating another corresponding + * overloads like + * {@link AnnotationValidations#validate(Class, UserIdInt, int)}. + * @param dayOfWeek + * Validation annotations may also have parameters. + * + * Parameter values will be supplied to validation method as name-value pairs. + * @param coords + * Unnamed validation annotation parameter gets supplied to the validating method named as + * "value". + * + * Validation annotations following {@link Each} annotation, will be applied for each + * array/collection element instead. + */ + @DataClass.Generated.Member + public SampleDataClass( + int num, + int num2, + int num4, + @Nullable String name, + @NonNull String name2, + @NonNull String name4, + @Nullable AccessibilityNodeInfo otherParcelable, + @NonNull Date date, + @NonNull Pattern pattern, + @NonNull List<LinkAddress> linkAddresses2, + @NonNull ArrayList<LinkAddress> linkAddresses, + @Nullable LinkAddress[] linkAddresses4, + @StateName @NonNull String stateName, + @RequestFlags int flags, + @State int state, + @NonNull CharSequence charSeq, + @Nullable LinkAddress[] linkAddresses5, + @StringRes int stringRes, + @android.annotation.IntRange(from = 0, to = 6) int dayOfWeek, + @Size(2) @NonNull @FloatRange(from = 0f) float[] coords) { + this.mNum = num; + this.mNum2 = num2; + this.mNum4 = num4; + this.mName = name; + this.mName2 = name2; + AnnotationValidations.validate( + NonNull.class, null, mName2); + this.mName4 = name4; + AnnotationValidations.validate( + NonNull.class, null, mName4); + this.mOtherParcelable = otherParcelable; + this.mDate = date; + AnnotationValidations.validate( + NonNull.class, null, mDate); + this.mPattern = pattern; + AnnotationValidations.validate( + NonNull.class, null, mPattern); + this.mLinkAddresses2 = linkAddresses2; + AnnotationValidations.validate( + NonNull.class, null, mLinkAddresses2); + this.mLinkAddresses = linkAddresses; + AnnotationValidations.validate( + NonNull.class, null, mLinkAddresses); + this.mLinkAddresses4 = linkAddresses4; + this.mStateName = stateName; + + if (!(Objects.equals(mStateName, STATE_NAME_UNDEFINED)) + && !(Objects.equals(mStateName, STATE_NAME_ON)) + && !(Objects.equals(mStateName, STATE_NAME_OFF))) { + throw new java.lang.IllegalArgumentException( + "stateName was " + mStateName + " but must be one of: " + + "STATE_NAME_UNDEFINED(" + STATE_NAME_UNDEFINED + "), " + + "STATE_NAME_ON(" + STATE_NAME_ON + "), " + + "STATE_NAME_OFF(" + STATE_NAME_OFF + ")"); + } + + AnnotationValidations.validate( + NonNull.class, null, mStateName); + this.mFlags = flags; + + Preconditions.checkFlagsArgument( + mFlags, + FLAG_MANUAL_REQUEST + | FLAG_COMPATIBILITY_MODE_REQUEST + | FLAG_AUGMENTED_REQUEST); + this.mState = state; + + if (!(mState == STATE_UNDEFINED) + && !(mState == STATE_ON) + && !(mState == STATE_OFF)) { + throw new java.lang.IllegalArgumentException( + "state was " + mState + " but must be one of: " + + "STATE_UNDEFINED(" + STATE_UNDEFINED + "), " + + "STATE_ON(" + STATE_ON + "), " + + "STATE_OFF(" + STATE_OFF + ")"); + } + + this.charSeq = charSeq; + AnnotationValidations.validate( + NonNull.class, null, charSeq); + this.mLinkAddresses5 = linkAddresses5; + this.mStringRes = stringRes; + AnnotationValidations.validate( + StringRes.class, null, mStringRes); + this.mDayOfWeek = dayOfWeek; + AnnotationValidations.validate( + android.annotation.IntRange.class, null, mDayOfWeek, + "from", 0, + "to", 6); + this.mCoords = coords; + AnnotationValidations.validate( + Size.class, null, mCoords.length, + "value", 2); + AnnotationValidations.validate( + NonNull.class, null, mCoords); + int coordsSize = mCoords.length; + for (int i = 0; i < coordsSize; i++) { + AnnotationValidations.validate( + FloatRange.class, null, mCoords[i], + "from", 0f); + } + + + onConstructed(); + } + + /** + * Any property javadoc should go onto the field, and will be copied where appropriate, + * including getters, constructor parameters, builder setters, etc. + * + * <p> + * This allows to avoid the burden of maintaining copies of the same documentation + * pieces in multiple places for each field. + */ + @DataClass.Generated.Member + public int getNum() { + return mNum; + } + + /** + * Various javadoc features should work as expected when copied, e.g {@code code}, + * {@link #mName links}, <a href="https://google.com">html links</a>, etc. + * + * @see #mNum2 ..and so should blocks at the bottom, e.g. {@code @see} blocks. + */ + @DataClass.Generated.Member + public int getNum2() { + return mNum2; + } + + /** + * {@code @hide} javadoc annotation is also propagated, which can be used to adjust the + * desired public API surface. + * + * @see #getNum4() is hidden + * @see Builder#setNum4(int) also hidden + * @hide + */ + @DataClass.Generated.Member + public int getNum4() { + return mNum4; + } + + /** + * {@link Nullable} or {@link NonNull} annotation is required on all non-primitive fields. + */ + @DataClass.Generated.Member + public @Nullable String getName() { + return mName; + } + + /** + * Fields with default value expressions ("mFoo = ...") are optional, and are automatically + * initialized to the provided default expression, unless explicitly set. + * + * When using a {@link Builder} optional fields are passed via a {@link Builder#setName2 setter} + * while mandatory fields are passed via {@link Builder#Builder constructor}. + */ + @DataClass.Generated.Member + public @NonNull String getName2() { + return mName2; + } + + /** + * Alternatively, when default value computation is expensive, + * {@link #defaultName4 defaultFieldName()} can be defined to compute the default value. + */ + @DataClass.Generated.Member + public @NonNull String getName4() { + return mName4; + } + + /** + * For parcelling, any field type supported by {@link Parcel} is supported out of the box. + * E.g. {@link Parcelable} subclasses, {@link String}, {@link int}, {@link boolean}, etc. + */ + @DataClass.Generated.Member + public @Nullable AccessibilityNodeInfo getOtherParcelable() { + return mOtherParcelable; + } + + /** + * Additionally, support for parcelling other types can be added by implementing a + * {@link Parcelling}, and referencing it in the {@link DataClass.ParcelWith} field annotation. + * + * @see MyDateParcelling an example {@link Parcelling} implementation + */ + @DataClass.Generated.Member + public @NonNull Date getDate() { + return mDate; + } + + /** + * If a {@link Parcelling} is fairly common, consider putting it in {@link Parcelling.BuiltIn} + * to encourage its reuse. + */ + @DataClass.Generated.Member + public @NonNull Pattern getPattern() { + return mPattern; + } + + /** + * For lists, when using a {@link Builder}, other than a regular + * {@link Builder#setLinkAddresses2(List) setter}, and additional + * {@link Builder#addLinkAddresses2(LinkAddress) add} method is generated for convenience. + */ + @DataClass.Generated.Member + public @NonNull List<LinkAddress> getLinkAddresses2() { + return mLinkAddresses2; + } + + /** + * For aesthetics, you may want to consider providing a singular version of the plural field + * name, which would be used for the {@link #mLinkAddresses2 above mentioned} "add" method. + * + * @see Builder#addLinkAddress(LinkAddress) + */ + @DataClass.Generated.Member + public @NonNull ArrayList<LinkAddress> getLinkAddresses() { + return mLinkAddresses; + } + + /** + * {@link IntDef}/{@link StringDef}-annotated fields propagate the annotation to + * getter/constructor/setter/builder parameters, making for a nicer api. + * + * @see #getStateName + * @see Builder#setStateName + */ + @DataClass.Generated.Member + public @StateName @NonNull String getStateName() { + return mStateName; + } + + /** + * Fields annotated with {@link IntDef} annotations also get a proper {@link #toString()} value. + */ + @DataClass.Generated.Member + public @RequestFlags int getFlags() { + return mFlags; + } + + /** + * Above is true for both {@link IntDef#flag flags} and enum-like {@link IntDef}s + */ + @DataClass.Generated.Member + public @State int getState() { + return mState; + } + + /** + * Final fields suppress generating a setter (when setters are requested). + */ + @DataClass.Generated.Member + public @Nullable LinkAddress[] getLinkAddresses5() { + return mLinkAddresses5; + } + + /** + * Fields with certain annotations are automatically validated in constructor + * + * You can see overloads in {@link AnnotationValidations} for a list of currently + * supported ones. + * + * You can also extend support to your custom annotations by creating another corresponding + * overloads like + * {@link AnnotationValidations#validate(Class, UserIdInt, int)}. + * + * @see #SampleDataClass + */ + @DataClass.Generated.Member + public @StringRes int getStringRes() { + return mStringRes; + } + + /** + * Validation annotations may also have parameters. + * + * Parameter values will be supplied to validation method as name-value pairs. + * + * @see AnnotationValidations#validate(Class, Size, int, String, int, String, int) + */ + @DataClass.Generated.Member + public @android.annotation.IntRange(from = 0, to = 6) int getDayOfWeek() { + return mDayOfWeek; + } + + /** + * Unnamed validation annotation parameter gets supplied to the validating method named as + * "value". + * + * Validation annotations following {@link Each} annotation, will be applied for each + * array/collection element instead. + * + * @see AnnotationValidations#validate(Class, Size, int, String, int) + */ + @DataClass.Generated.Member + public @Size(2) @NonNull @FloatRange(from = 0f) float[] getCoords() { + return mCoords; + } + + /** + * When using transient fields for caching it's often also a good idea to initialize them + * lazily. + * + * You can declare a special method like {@link #lazyInitTmpStorage()}, to let the + * {@link #getTmpStorage getter} lazily-initialize the value on demand. + */ + @DataClass.Generated.Member + public int[] getTmpStorage() { + int[] tmpStorage = mTmpStorage; + if (tmpStorage == null) { + // You can mark field as volatile for thread-safe double-check init + tmpStorage = mTmpStorage = lazyInitTmpStorage(); + } + return tmpStorage; + } + + /** + * Any property javadoc should go onto the field, and will be copied where appropriate, + * including getters, constructor parameters, builder setters, etc. + * + * <p> + * This allows to avoid the burden of maintaining copies of the same documentation + * pieces in multiple places for each field. + */ + @DataClass.Generated.Member + public SampleDataClass setNum( int value) { + mNum = value; + return this; + } + + /** + * Various javadoc features should work as expected when copied, e.g {@code code}, + * {@link #mName links}, <a href="https://google.com">html links</a>, etc. + * + * @see #mNum2 ..and so should blocks at the bottom, e.g. {@code @see} blocks. + */ + @DataClass.Generated.Member + public SampleDataClass setNum2( int value) { + mNum2 = value; + return this; + } + + /** + * {@code @hide} javadoc annotation is also propagated, which can be used to adjust the + * desired public API surface. + * + * @see #getNum4() is hidden + * @see Builder#setNum4(int) also hidden + * @hide + */ + @DataClass.Generated.Member + public SampleDataClass setNum4( int value) { + mNum4 = value; + return this; + } + + /** + * {@link Nullable} or {@link NonNull} annotation is required on all non-primitive fields. + */ + @DataClass.Generated.Member + public SampleDataClass setName(@NonNull String value) { + mName = value; + return this; + } + + /** + * Fields with default value expressions ("mFoo = ...") are optional, and are automatically + * initialized to the provided default expression, unless explicitly set. + * + * When using a {@link Builder} optional fields are passed via a {@link Builder#setName2 setter} + * while mandatory fields are passed via {@link Builder#Builder constructor}. + */ + @DataClass.Generated.Member + public SampleDataClass setName2(@NonNull String value) { + mName2 = value; + AnnotationValidations.validate( + NonNull.class, null, mName2); + return this; + } + + /** + * Alternatively, when default value computation is expensive, + * {@link #defaultName4 defaultFieldName()} can be defined to compute the default value. + */ + @DataClass.Generated.Member + public SampleDataClass setName4(@NonNull String value) { + mName4 = value; + AnnotationValidations.validate( + NonNull.class, null, mName4); + return this; + } + + /** + * For parcelling, any field type supported by {@link Parcel} is supported out of the box. + * E.g. {@link Parcelable} subclasses, {@link String}, {@link int}, {@link boolean}, etc. + */ + @DataClass.Generated.Member + public SampleDataClass setOtherParcelable(@NonNull AccessibilityNodeInfo value) { + mOtherParcelable = value; + return this; + } + + /** + * Additionally, support for parcelling other types can be added by implementing a + * {@link Parcelling}, and referencing it in the {@link DataClass.ParcelWith} field annotation. + * + * @see MyDateParcelling an example {@link Parcelling} implementation + */ + @DataClass.Generated.Member + public SampleDataClass setDate(@NonNull Date value) { + mDate = value; + AnnotationValidations.validate( + NonNull.class, null, mDate); + return this; + } + + /** + * If a {@link Parcelling} is fairly common, consider putting it in {@link Parcelling.BuiltIn} + * to encourage its reuse. + */ + @DataClass.Generated.Member + public SampleDataClass setPattern(@NonNull Pattern value) { + mPattern = value; + AnnotationValidations.validate( + NonNull.class, null, mPattern); + return this; + } + + /** + * For lists, when using a {@link Builder}, other than a regular + * {@link Builder#setLinkAddresses2(List) setter}, and additional + * {@link Builder#addLinkAddresses2(LinkAddress) add} method is generated for convenience. + */ + @DataClass.Generated.Member + public SampleDataClass setLinkAddresses2(@NonNull List<LinkAddress> value) { + mLinkAddresses2 = value; + AnnotationValidations.validate( + NonNull.class, null, mLinkAddresses2); + return this; + } + + /** + * For aesthetics, you may want to consider providing a singular version of the plural field + * name, which would be used for the {@link #mLinkAddresses2 above mentioned} "add" method. + * + * @see Builder#addLinkAddress(LinkAddress) + */ + @DataClass.Generated.Member + public SampleDataClass setLinkAddresses(@NonNull ArrayList<LinkAddress> value) { + mLinkAddresses = value; + AnnotationValidations.validate( + NonNull.class, null, mLinkAddresses); + return this; + } + + /** + * For array fields, when using a {@link Builder}, vararg argument format is used for + * convenience. + * + * @see Builder#setLinkAddresses4(LinkAddress...) + */ + @DataClass.Generated.Member + public SampleDataClass setLinkAddresses4(@NonNull LinkAddress... value) { + mLinkAddresses4 = value; + return this; + } + + /** + * {@link IntDef}/{@link StringDef}-annotated fields propagate the annotation to + * getter/constructor/setter/builder parameters, making for a nicer api. + * + * @see #getStateName + * @see Builder#setStateName + */ + @DataClass.Generated.Member + public SampleDataClass setStateName(@StateName @NonNull String value) { + mStateName = value; + + if (!(Objects.equals(mStateName, STATE_NAME_UNDEFINED)) + && !(Objects.equals(mStateName, STATE_NAME_ON)) + && !(Objects.equals(mStateName, STATE_NAME_OFF))) { + throw new java.lang.IllegalArgumentException( + "stateName was " + mStateName + " but must be one of: " + + "STATE_NAME_UNDEFINED(" + STATE_NAME_UNDEFINED + "), " + + "STATE_NAME_ON(" + STATE_NAME_ON + "), " + + "STATE_NAME_OFF(" + STATE_NAME_OFF + ")"); + } + + AnnotationValidations.validate( + NonNull.class, null, mStateName); + return this; + } + + /** + * Fields annotated with {@link IntDef} annotations also get a proper {@link #toString()} value. + */ + @DataClass.Generated.Member + public SampleDataClass setFlags(@RequestFlags int value) { + mFlags = value; + + Preconditions.checkFlagsArgument( + mFlags, + FLAG_MANUAL_REQUEST + | FLAG_COMPATIBILITY_MODE_REQUEST + | FLAG_AUGMENTED_REQUEST); + return this; + } + + /** + * Above is true for both {@link IntDef#flag flags} and enum-like {@link IntDef}s + */ + @DataClass.Generated.Member + public SampleDataClass setState(@State int value) { + mState = value; + + if (!(mState == STATE_UNDEFINED) + && !(mState == STATE_ON) + && !(mState == STATE_OFF)) { + throw new java.lang.IllegalArgumentException( + "state was " + mState + " but must be one of: " + + "STATE_UNDEFINED(" + STATE_UNDEFINED + "), " + + "STATE_ON(" + STATE_ON + "), " + + "STATE_OFF(" + STATE_OFF + ")"); + } + + return this; + } + + /** + * Fields with certain annotations are automatically validated in constructor + * + * You can see overloads in {@link AnnotationValidations} for a list of currently + * supported ones. + * + * You can also extend support to your custom annotations by creating another corresponding + * overloads like + * {@link AnnotationValidations#validate(Class, UserIdInt, int)}. + * + * @see #SampleDataClass + */ + @DataClass.Generated.Member + public SampleDataClass setStringRes(@StringRes int value) { + mStringRes = value; + AnnotationValidations.validate( + StringRes.class, null, mStringRes); + return this; + } + + /** + * Validation annotations may also have parameters. + * + * Parameter values will be supplied to validation method as name-value pairs. + * + * @see AnnotationValidations#validate(Class, Size, int, String, int, String, int) + */ + @DataClass.Generated.Member + public SampleDataClass setDayOfWeek(@android.annotation.IntRange(from = 0, to = 6) int value) { + mDayOfWeek = value; + AnnotationValidations.validate( + android.annotation.IntRange.class, null, mDayOfWeek, + "from", 0, + "to", 6); + return this; + } + + /** + * Unnamed validation annotation parameter gets supplied to the validating method named as + * "value". + * + * Validation annotations following {@link Each} annotation, will be applied for each + * array/collection element instead. + * + * @see AnnotationValidations#validate(Class, Size, int, String, int) + */ + @DataClass.Generated.Member + public SampleDataClass setCoords(@Size(2) @NonNull @FloatRange(from = 0f) float... value) { + mCoords = value; + AnnotationValidations.validate( + Size.class, null, mCoords.length, + "value", 2); + AnnotationValidations.validate( + NonNull.class, null, mCoords); + int coordsSize = mCoords.length; + for (int i = 0; i < coordsSize; i++) { + AnnotationValidations.validate( + FloatRange.class, null, mCoords[i], + "from", 0f); + } + + return this; + } + + @Override + @DataClass.Generated.Member + public String toString() { + // You can override field toString logic by defining methods like: + // String fieldNameToString() { ... } + + return "SampleDataClass { " + + "num = " + mNum + ", " + + "num2 = " + mNum2 + ", " + + "num4 = " + mNum4 + ", " + + "name = " + mName + ", " + + "name2 = " + mName2 + ", " + + "name4 = " + mName4 + ", " + + "otherParcelable = " + mOtherParcelable + ", " + + "date = " + mDate + ", " + + "pattern = " + mPattern + ", " + + "linkAddresses2 = " + mLinkAddresses2 + ", " + + "linkAddresses = " + mLinkAddresses + ", " + + "linkAddresses4 = " + java.util.Arrays.toString(mLinkAddresses4) + ", " + + "stateName = " + mStateName + ", " + + "flags = " + requestFlagsToString(mFlags) + ", " + + "state = " + stateToString(mState) + ", " + + "charSeq = " + charSeq + ", " + + "linkAddresses5 = " + java.util.Arrays.toString(mLinkAddresses5) + ", " + + "stringRes = " + mStringRes + ", " + + "dayOfWeek = " + mDayOfWeek + ", " + + "coords = " + java.util.Arrays.toString(mCoords) + + " }"; + } + + @Override + @DataClass.Generated.Member + public boolean equals(@Nullable Object o) { + // You can override field equality logic by defining either of the methods like: + // boolean fieldNameEquals(SampleDataClass other) { ... } + // boolean fieldNameEquals(FieldType otherValue) { ... } + + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unchecked") + SampleDataClass that = (SampleDataClass) o; + //noinspection PointlessBooleanExpression + return true + && mNum == that.mNum + && mNum2 == that.mNum2 + && mNum4 == that.mNum4 + && Objects.equals(mName, that.mName) + && Objects.equals(mName2, that.mName2) + && Objects.equals(mName4, that.mName4) + && Objects.equals(mOtherParcelable, that.mOtherParcelable) + && Objects.equals(mDate, that.mDate) + && patternEquals(that.mPattern) + && Objects.equals(mLinkAddresses2, that.mLinkAddresses2) + && Objects.equals(mLinkAddresses, that.mLinkAddresses) + && java.util.Arrays.equals(mLinkAddresses4, that.mLinkAddresses4) + && Objects.equals(mStateName, that.mStateName) + && mFlags == that.mFlags + && mState == that.mState + && Objects.equals(charSeq, that.charSeq) + && java.util.Arrays.equals(mLinkAddresses5, that.mLinkAddresses5) + && mStringRes == that.mStringRes + && mDayOfWeek == that.mDayOfWeek + && java.util.Arrays.equals(mCoords, that.mCoords); + } + + @Override + @DataClass.Generated.Member + public int hashCode() { + // You can override field hashCode logic by defining methods like: + // int fieldNameHashCode() { ... } + + int _hash = 1; + _hash = 31 * _hash + mNum; + _hash = 31 * _hash + mNum2; + _hash = 31 * _hash + mNum4; + _hash = 31 * _hash + Objects.hashCode(mName); + _hash = 31 * _hash + Objects.hashCode(mName2); + _hash = 31 * _hash + Objects.hashCode(mName4); + _hash = 31 * _hash + Objects.hashCode(mOtherParcelable); + _hash = 31 * _hash + Objects.hashCode(mDate); + _hash = 31 * _hash + patternHashCode(); + _hash = 31 * _hash + Objects.hashCode(mLinkAddresses2); + _hash = 31 * _hash + Objects.hashCode(mLinkAddresses); + _hash = 31 * _hash + java.util.Arrays.hashCode(mLinkAddresses4); + _hash = 31 * _hash + Objects.hashCode(mStateName); + _hash = 31 * _hash + mFlags; + _hash = 31 * _hash + mState; + _hash = 31 * _hash + Objects.hashCode(charSeq); + _hash = 31 * _hash + java.util.Arrays.hashCode(mLinkAddresses5); + _hash = 31 * _hash + mStringRes; + _hash = 31 * _hash + mDayOfWeek; + _hash = 31 * _hash + java.util.Arrays.hashCode(mCoords); + return _hash; + } + + @DataClass.Generated.Member + void forEachField( + @NonNull DataClass.PerIntFieldAction<SampleDataClass> actionInt, + @NonNull DataClass.PerObjectFieldAction<SampleDataClass> actionObject) { + actionInt.acceptInt(this, "num", mNum); + actionInt.acceptInt(this, "num2", mNum2); + actionInt.acceptInt(this, "num4", mNum4); + actionObject.acceptObject(this, "name", mName); + actionObject.acceptObject(this, "name2", mName2); + actionObject.acceptObject(this, "name4", mName4); + actionObject.acceptObject(this, "otherParcelable", mOtherParcelable); + actionObject.acceptObject(this, "date", mDate); + actionObject.acceptObject(this, "pattern", mPattern); + actionObject.acceptObject(this, "linkAddresses2", mLinkAddresses2); + actionObject.acceptObject(this, "linkAddresses", mLinkAddresses); + actionObject.acceptObject(this, "linkAddresses4", mLinkAddresses4); + actionObject.acceptObject(this, "stateName", mStateName); + actionInt.acceptInt(this, "flags", mFlags); + actionInt.acceptInt(this, "state", mState); + actionObject.acceptObject(this, "charSeq", charSeq); + actionObject.acceptObject(this, "linkAddresses5", mLinkAddresses5); + actionInt.acceptInt(this, "stringRes", mStringRes); + actionInt.acceptInt(this, "dayOfWeek", mDayOfWeek); + actionObject.acceptObject(this, "coords", mCoords); + } + + /** @deprecated May cause boxing allocations - use with caution! */ + @Deprecated + @DataClass.Generated.Member + void forEachField(@NonNull DataClass.PerObjectFieldAction<SampleDataClass> action) { + action.acceptObject(this, "num", mNum); + action.acceptObject(this, "num2", mNum2); + action.acceptObject(this, "num4", mNum4); + action.acceptObject(this, "name", mName); + action.acceptObject(this, "name2", mName2); + action.acceptObject(this, "name4", mName4); + action.acceptObject(this, "otherParcelable", mOtherParcelable); + action.acceptObject(this, "date", mDate); + action.acceptObject(this, "pattern", mPattern); + action.acceptObject(this, "linkAddresses2", mLinkAddresses2); + action.acceptObject(this, "linkAddresses", mLinkAddresses); + action.acceptObject(this, "linkAddresses4", mLinkAddresses4); + action.acceptObject(this, "stateName", mStateName); + action.acceptObject(this, "flags", mFlags); + action.acceptObject(this, "state", mState); + action.acceptObject(this, "charSeq", charSeq); + action.acceptObject(this, "linkAddresses5", mLinkAddresses5); + action.acceptObject(this, "stringRes", mStringRes); + action.acceptObject(this, "dayOfWeek", mDayOfWeek); + action.acceptObject(this, "coords", mCoords); + } + + @DataClass.Generated.Member + static Parcelling<Date> sParcellingForDate = + Parcelling.Cache.get( + MyDateParcelling.class); + static { + if (sParcellingForDate == null) { + sParcellingForDate = Parcelling.Cache.put( + new MyDateParcelling()); + } + } + + @DataClass.Generated.Member + static Parcelling<Pattern> sParcellingForPattern = + Parcelling.Cache.get( + Parcelling.BuiltIn.ForPattern.class); + static { + if (sParcellingForPattern == null) { + sParcellingForPattern = Parcelling.Cache.put( + new Parcelling.BuiltIn.ForPattern()); + } + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + long flg = 0; + if (mName != null) flg |= 0x8; + if (mOtherParcelable != null) flg |= 0x40; + if (mLinkAddresses4 != null) flg |= 0x800; + if (mLinkAddresses5 != null) flg |= 0x10000; + dest.writeLong(flg); + dest.writeInt(mNum); + dest.writeInt(mNum2); + dest.writeInt(mNum4); + if (mName != null) dest.writeString(mName); + dest.writeString(mName2); + dest.writeString(mName4); + if (mOtherParcelable != null) dest.writeTypedObject(mOtherParcelable, flags); + sParcellingForDate.parcel(mDate, dest, flags); + sParcellingForPattern.parcel(mPattern, dest, flags); + dest.writeParcelableList(mLinkAddresses2, flags); + dest.writeParcelableList(mLinkAddresses, flags); + if (mLinkAddresses4 != null) dest.writeTypedArray(mLinkAddresses4, flags); + dest.writeString(mStateName); + dest.writeInt(mFlags); + dest.writeInt(mState); + dest.writeCharSequence(charSeq); + if (mLinkAddresses5 != null) dest.writeTypedArray(mLinkAddresses5, flags); + dest.writeInt(mStringRes); + dest.writeInt(mDayOfWeek); + dest.writeFloatArray(mCoords); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + /* package-private */ SampleDataClass(@NonNull Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + long flg = in.readLong(); + int num = in.readInt(); + int num2 = in.readInt(); + int num4 = in.readInt(); + String name = (flg & 0x8) == 0 ? null : in.readString(); + String name2 = in.readString(); + String name4 = in.readString(); + AccessibilityNodeInfo otherParcelable = (flg & 0x40) == 0 ? null : (AccessibilityNodeInfo) in.readTypedObject(AccessibilityNodeInfo.CREATOR); + Date date = sParcellingForDate.unparcel(in); + Pattern pattern = sParcellingForPattern.unparcel(in); + List<LinkAddress> linkAddresses2 = new ArrayList<>(); + in.readParcelableList(linkAddresses2, LinkAddress.class.getClassLoader()); + ArrayList<LinkAddress> linkAddresses = new ArrayList<>(); + in.readParcelableList(linkAddresses, LinkAddress.class.getClassLoader()); + LinkAddress[] linkAddresses4 = (flg & 0x800) == 0 ? null : (LinkAddress[]) in.createTypedArray(LinkAddress.CREATOR); + String stateName = in.readString(); + int flags = in.readInt(); + int state = in.readInt(); + CharSequence _charSeq = (CharSequence) in.readCharSequence(); + LinkAddress[] linkAddresses5 = (flg & 0x10000) == 0 ? null : (LinkAddress[]) in.createTypedArray(LinkAddress.CREATOR); + int stringRes = in.readInt(); + int dayOfWeek = in.readInt(); + float[] coords = in.createFloatArray(); + + this.mNum = num; + this.mNum2 = num2; + this.mNum4 = num4; + this.mName = name; + this.mName2 = name2; + AnnotationValidations.validate( + NonNull.class, null, mName2); + this.mName4 = name4; + AnnotationValidations.validate( + NonNull.class, null, mName4); + this.mOtherParcelable = otherParcelable; + this.mDate = date; + AnnotationValidations.validate( + NonNull.class, null, mDate); + this.mPattern = pattern; + AnnotationValidations.validate( + NonNull.class, null, mPattern); + this.mLinkAddresses2 = linkAddresses2; + AnnotationValidations.validate( + NonNull.class, null, mLinkAddresses2); + this.mLinkAddresses = linkAddresses; + AnnotationValidations.validate( + NonNull.class, null, mLinkAddresses); + this.mLinkAddresses4 = linkAddresses4; + this.mStateName = stateName; + + if (!(Objects.equals(mStateName, STATE_NAME_UNDEFINED)) + && !(Objects.equals(mStateName, STATE_NAME_ON)) + && !(Objects.equals(mStateName, STATE_NAME_OFF))) { + throw new java.lang.IllegalArgumentException( + "stateName was " + mStateName + " but must be one of: " + + "STATE_NAME_UNDEFINED(" + STATE_NAME_UNDEFINED + "), " + + "STATE_NAME_ON(" + STATE_NAME_ON + "), " + + "STATE_NAME_OFF(" + STATE_NAME_OFF + ")"); + } + + AnnotationValidations.validate( + NonNull.class, null, mStateName); + this.mFlags = flags; + + Preconditions.checkFlagsArgument( + mFlags, + FLAG_MANUAL_REQUEST + | FLAG_COMPATIBILITY_MODE_REQUEST + | FLAG_AUGMENTED_REQUEST); + this.mState = state; + + if (!(mState == STATE_UNDEFINED) + && !(mState == STATE_ON) + && !(mState == STATE_OFF)) { + throw new java.lang.IllegalArgumentException( + "state was " + mState + " but must be one of: " + + "STATE_UNDEFINED(" + STATE_UNDEFINED + "), " + + "STATE_ON(" + STATE_ON + "), " + + "STATE_OFF(" + STATE_OFF + ")"); + } + + this.charSeq = _charSeq; + AnnotationValidations.validate( + NonNull.class, null, charSeq); + this.mLinkAddresses5 = linkAddresses5; + this.mStringRes = stringRes; + AnnotationValidations.validate( + StringRes.class, null, mStringRes); + this.mDayOfWeek = dayOfWeek; + AnnotationValidations.validate( + android.annotation.IntRange.class, null, mDayOfWeek, + "from", 0, + "to", 6); + this.mCoords = coords; + AnnotationValidations.validate( + Size.class, null, mCoords.length, + "value", 2); + AnnotationValidations.validate( + NonNull.class, null, mCoords); + int coordsSize = mCoords.length; + for (int i = 0; i < coordsSize; i++) { + AnnotationValidations.validate( + FloatRange.class, null, mCoords[i], + "from", 0f); + } + + + onConstructed(); + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<SampleDataClass> CREATOR + = new Parcelable.Creator<SampleDataClass>() { + @Override + public SampleDataClass[] newArray(int size) { + return new SampleDataClass[size]; + } + + @Override + public SampleDataClass createFromParcel(@NonNull Parcel in) { + return new SampleDataClass(in); + } + }; + + /** + * A builder for {@link SampleDataClass} + */ + @SuppressWarnings("WeakerAccess") + @DataClass.Generated.Member + public static final class Builder { + + private int mNum; + private int mNum2; + private int mNum4; + private @Nullable String mName; + private @NonNull String mName2; + private @NonNull String mName4; + private @Nullable AccessibilityNodeInfo mOtherParcelable; + private @NonNull Date mDate; + private @NonNull Pattern mPattern; + private @NonNull List<LinkAddress> mLinkAddresses2; + private @NonNull ArrayList<LinkAddress> mLinkAddresses; + private @Nullable LinkAddress[] mLinkAddresses4; + private @StateName @NonNull String mStateName; + private @RequestFlags int mFlags; + private @State int mState; + private @NonNull CharSequence charSeq; + private @Nullable LinkAddress[] mLinkAddresses5; + private @StringRes int mStringRes; + private @android.annotation.IntRange(from = 0, to = 6) int mDayOfWeek; + private @Size(2) @NonNull @FloatRange(from = 0f) float[] mCoords; + + private long mBuilderFieldsSet = 0L; + + /** + * Creates a new Builder. + * + * @param num + * Any property javadoc should go onto the field, and will be copied where appropriate, + * including getters, constructor parameters, builder setters, etc. + * + * <p> + * This allows to avoid the burden of maintaining copies of the same documentation + * pieces in multiple places for each field. + * @param num2 + * Various javadoc features should work as expected when copied, e.g {@code code}, + * {@link #mName links}, <a href="https://google.com">html links</a>, etc. + * @param num4 + * {@code @hide} javadoc annotation is also propagated, which can be used to adjust the + * desired public API surface. + * @param name + * {@link Nullable} or {@link NonNull} annotation is required on all non-primitive fields. + * @param flags + * Fields annotated with {@link IntDef} annotations also get a proper {@link #toString()} value. + * @param linkAddresses5 + * Final fields suppress generating a setter (when setters are requested). + */ + public Builder( + int num, + int num2, + int num4, + @Nullable String name, + @RequestFlags int flags, + @Nullable LinkAddress[] linkAddresses5) { + mNum = num; + mNum2 = num2; + mNum4 = num4; + mName = name; + mFlags = flags; + + Preconditions.checkFlagsArgument( + mFlags, + FLAG_MANUAL_REQUEST + | FLAG_COMPATIBILITY_MODE_REQUEST + | FLAG_AUGMENTED_REQUEST); + mLinkAddresses5 = linkAddresses5; + } + + /** + * Any property javadoc should go onto the field, and will be copied where appropriate, + * including getters, constructor parameters, builder setters, etc. + * + * <p> + * This allows to avoid the burden of maintaining copies of the same documentation + * pieces in multiple places for each field. + */ + @DataClass.Generated.Member + public @NonNull Builder setNum(int value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x1; + mNum = value; + return this; + } + + /** + * Various javadoc features should work as expected when copied, e.g {@code code}, + * {@link #mName links}, <a href="https://google.com">html links</a>, etc. + * + * @see #mNum2 ..and so should blocks at the bottom, e.g. {@code @see} blocks. + */ + @DataClass.Generated.Member + public @NonNull Builder setNum2(int value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x2; + mNum2 = value; + return this; + } + + /** + * {@code @hide} javadoc annotation is also propagated, which can be used to adjust the + * desired public API surface. + * + * @see #getNum4() is hidden + * @see Builder#setNum4(int) also hidden + * @hide + */ + @DataClass.Generated.Member + public @NonNull Builder setNum4(int value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x4; + mNum4 = value; + return this; + } + + /** + * {@link Nullable} or {@link NonNull} annotation is required on all non-primitive fields. + */ + @DataClass.Generated.Member + public @NonNull Builder setName(@NonNull String value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x8; + mName = value; + return this; + } + + /** + * Fields with default value expressions ("mFoo = ...") are optional, and are automatically + * initialized to the provided default expression, unless explicitly set. + * + * When using a {@link Builder} optional fields are passed via a {@link Builder#setName2 setter} + * while mandatory fields are passed via {@link Builder#Builder constructor}. + */ + @DataClass.Generated.Member + public @NonNull Builder setName2(@NonNull String value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x10; + mName2 = value; + return this; + } + + /** + * Alternatively, when default value computation is expensive, + * {@link #defaultName4 defaultFieldName()} can be defined to compute the default value. + */ + @DataClass.Generated.Member + public @NonNull Builder setName4(@NonNull String value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x20; + mName4 = value; + return this; + } + + /** + * For parcelling, any field type supported by {@link Parcel} is supported out of the box. + * E.g. {@link Parcelable} subclasses, {@link String}, {@link int}, {@link boolean}, etc. + */ + @DataClass.Generated.Member + public @NonNull Builder setOtherParcelable(@NonNull AccessibilityNodeInfo value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x40; + mOtherParcelable = value; + return this; + } + + /** + * Additionally, support for parcelling other types can be added by implementing a + * {@link Parcelling}, and referencing it in the {@link DataClass.ParcelWith} field annotation. + * + * @see MyDateParcelling an example {@link Parcelling} implementation + */ + @DataClass.Generated.Member + public @NonNull Builder setDate(@NonNull Date value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x80; + mDate = value; + return this; + } + + /** + * If a {@link Parcelling} is fairly common, consider putting it in {@link Parcelling.BuiltIn} + * to encourage its reuse. + */ + @DataClass.Generated.Member + public @NonNull Builder setPattern(@NonNull Pattern value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x100; + mPattern = value; + return this; + } + + /** + * For lists, when using a {@link Builder}, other than a regular + * {@link Builder#setLinkAddresses2(List) setter}, and additional + * {@link Builder#addLinkAddresses2(LinkAddress) add} method is generated for convenience. + */ + @DataClass.Generated.Member + public @NonNull Builder setLinkAddresses2(@NonNull List<LinkAddress> value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x200; + mLinkAddresses2 = value; + return this; + } + + /** @see #setLinkAddresses2 */ + @DataClass.Generated.Member + public @NonNull Builder addLinkAddresses2(@NonNull LinkAddress value) { + // You can refine this method's name by providing item's singular name, e.g.: + // @DataClass.PluralOf("item")) mItems = ... + + if (mLinkAddresses2 == null) setLinkAddresses2(new ArrayList<>()); + mLinkAddresses2.add(value); + return this; + } + + /** + * For aesthetics, you may want to consider providing a singular version of the plural field + * name, which would be used for the {@link #mLinkAddresses2 above mentioned} "add" method. + * + * @see Builder#addLinkAddress(LinkAddress) + */ + @DataClass.Generated.Member + public @NonNull Builder setLinkAddresses(@NonNull ArrayList<LinkAddress> value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x400; + mLinkAddresses = value; + return this; + } + + /** @see #setLinkAddresses */ + @DataClass.Generated.Member + public @NonNull Builder addLinkAddress(@NonNull LinkAddress value) { + if (mLinkAddresses == null) setLinkAddresses(new ArrayList<>()); + mLinkAddresses.add(value); + return this; + } + + /** + * For array fields, when using a {@link Builder}, vararg argument format is used for + * convenience. + * + * @see Builder#setLinkAddresses4(LinkAddress...) + */ + @DataClass.Generated.Member + public @NonNull Builder setLinkAddresses4(@NonNull LinkAddress... value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x800; + mLinkAddresses4 = value; + return this; + } + + /** + * {@link IntDef}/{@link StringDef}-annotated fields propagate the annotation to + * getter/constructor/setter/builder parameters, making for a nicer api. + * + * @see #getStateName + * @see Builder#setStateName + */ + @DataClass.Generated.Member + public @NonNull Builder setStateName(@StateName @NonNull String value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x1000; + mStateName = value; + return this; + } + + /** + * Fields annotated with {@link IntDef} annotations also get a proper {@link #toString()} value. + */ + @DataClass.Generated.Member + public @NonNull Builder setFlags(@RequestFlags int value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x2000; + mFlags = value; + return this; + } + + /** + * Above is true for both {@link IntDef#flag flags} and enum-like {@link IntDef}s + */ + @DataClass.Generated.Member + public @NonNull Builder setState(@State int value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x4000; + mState = value; + return this; + } + + /** + * Making a field public will suppress getter generation in favor of accessing it directly. + */ + @DataClass.Generated.Member + public @NonNull Builder setCharSeq(@NonNull CharSequence value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x8000; + charSeq = value; + return this; + } + + /** + * Final fields suppress generating a setter (when setters are requested). + */ + @DataClass.Generated.Member + public @NonNull Builder setLinkAddresses5(@NonNull LinkAddress... value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x10000; + mLinkAddresses5 = value; + return this; + } + + /** + * Fields with certain annotations are automatically validated in constructor + * + * You can see overloads in {@link AnnotationValidations} for a list of currently + * supported ones. + * + * You can also extend support to your custom annotations by creating another corresponding + * overloads like + * {@link AnnotationValidations#validate(Class, UserIdInt, int)}. + * + * @see #SampleDataClass + */ + @DataClass.Generated.Member + public @NonNull Builder setStringRes(@StringRes int value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x20000; + mStringRes = value; + return this; + } + + /** + * Validation annotations may also have parameters. + * + * Parameter values will be supplied to validation method as name-value pairs. + * + * @see AnnotationValidations#validate(Class, Size, int, String, int, String, int) + */ + @DataClass.Generated.Member + public @NonNull Builder setDayOfWeek(@android.annotation.IntRange(from = 0, to = 6) int value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x40000; + mDayOfWeek = value; + return this; + } + + /** + * Unnamed validation annotation parameter gets supplied to the validating method named as + * "value". + * + * Validation annotations following {@link Each} annotation, will be applied for each + * array/collection element instead. + * + * @see AnnotationValidations#validate(Class, Size, int, String, int) + */ + @DataClass.Generated.Member + public @NonNull Builder setCoords(@Size(2) @NonNull @FloatRange(from = 0f) float... value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x80000; + mCoords = value; + return this; + } + + /** Builds the instance. This builder should not be touched after calling this! */ + public @NonNull SampleDataClass build() { + checkNotUsed(); + mBuilderFieldsSet |= 0x100000; // Mark builder used + + if ((mBuilderFieldsSet & 0x10) == 0) { + mName2 = "Bob"; + } + if ((mBuilderFieldsSet & 0x20) == 0) { + mName4 = defaultName4(); + } + if ((mBuilderFieldsSet & 0x40) == 0) { + mOtherParcelable = null; + } + if ((mBuilderFieldsSet & 0x80) == 0) { + mDate = new Date(42 * 42); + } + if ((mBuilderFieldsSet & 0x100) == 0) { + mPattern = Pattern.compile(""); + } + if ((mBuilderFieldsSet & 0x200) == 0) { + mLinkAddresses2 = new ArrayList<>(); + } + if ((mBuilderFieldsSet & 0x400) == 0) { + mLinkAddresses = new ArrayList<>(); + } + if ((mBuilderFieldsSet & 0x800) == 0) { + mLinkAddresses4 = null; + } + if ((mBuilderFieldsSet & 0x1000) == 0) { + mStateName = STATE_NAME_UNDEFINED; + } + if ((mBuilderFieldsSet & 0x4000) == 0) { + mState = STATE_UNDEFINED; + } + if ((mBuilderFieldsSet & 0x8000) == 0) { + charSeq = ""; + } + if ((mBuilderFieldsSet & 0x20000) == 0) { + mStringRes = 0; + } + if ((mBuilderFieldsSet & 0x40000) == 0) { + mDayOfWeek = 3; + } + if ((mBuilderFieldsSet & 0x80000) == 0) { + mCoords = new float[] { 0f, 0f }; + } + SampleDataClass o = new SampleDataClass( + mNum, + mNum2, + mNum4, + mName, + mName2, + mName4, + mOtherParcelable, + mDate, + mPattern, + mLinkAddresses2, + mLinkAddresses, + mLinkAddresses4, + mStateName, + mFlags, + mState, + charSeq, + mLinkAddresses5, + mStringRes, + mDayOfWeek, + mCoords); + return o; + } + + private void checkNotUsed() { + if ((mBuilderFieldsSet & 0x100000) != 0) { + throw new IllegalStateException( + "This Builder should not be reused. Use a new Builder instance instead"); + } + } + } + + @DataClass.Generated( + time = 1582685647656L, + codegenVersion = "1.0.15", + sourceFile = "frameworks/base/tests/Codegen/src/com/android/codegentest/SampleDataClass.java", + inputSignatures = "public static final java.lang.String STATE_NAME_UNDEFINED\npublic static final java.lang.String STATE_NAME_ON\npublic static final java.lang.String STATE_NAME_OFF\npublic static final int STATE_UNDEFINED\npublic static final int STATE_ON\npublic static final int STATE_OFF\npublic static final @com.android.codegentest.SampleDataClass.RequestFlags int FLAG_MANUAL_REQUEST\npublic static final @com.android.codegentest.SampleDataClass.RequestFlags int FLAG_COMPATIBILITY_MODE_REQUEST\npublic static final @com.android.codegentest.SampleDataClass.RequestFlags int FLAG_AUGMENTED_REQUEST\nprivate int mNum\nprivate int mNum2\nprivate int mNum4\nprivate @android.annotation.Nullable java.lang.String mName\nprivate @android.annotation.NonNull java.lang.String mName2\nprivate @android.annotation.NonNull java.lang.String mName4\nprivate @android.annotation.Nullable android.view.accessibility.AccessibilityNodeInfo mOtherParcelable\nprivate @com.android.internal.util.DataClass.ParcelWith(com.android.codegentest.MyDateParcelling.class) @android.annotation.NonNull java.util.Date mDate\nprivate @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForPattern.class) @android.annotation.NonNull java.util.regex.Pattern mPattern\nprivate @android.annotation.NonNull java.util.List<android.net.LinkAddress> mLinkAddresses2\nprivate @com.android.internal.util.DataClass.PluralOf(\"linkAddress\") @android.annotation.NonNull java.util.ArrayList<android.net.LinkAddress> mLinkAddresses\nprivate @android.annotation.Nullable android.net.LinkAddress[] mLinkAddresses4\nprivate @com.android.codegentest.SampleDataClass.StateName @android.annotation.NonNull java.lang.String mStateName\nprivate @com.android.codegentest.SampleDataClass.RequestFlags int mFlags\nprivate @com.android.codegentest.SampleDataClass.State int mState\npublic @android.annotation.NonNull java.lang.CharSequence charSeq\nprivate final @android.annotation.Nullable android.net.LinkAddress[] mLinkAddresses5\nprivate transient android.net.LinkAddress[] mLinkAddresses6\ntransient int[] mTmpStorage\nprivate @android.annotation.StringRes int mStringRes\nprivate @android.annotation.IntRange(from=0L, to=6L) int mDayOfWeek\nprivate @android.annotation.Size(2L) @android.annotation.NonNull @com.android.internal.util.DataClass.Each @android.annotation.FloatRange(from=0.0) float[] mCoords\nprivate static java.lang.String defaultName4()\nprivate int[] lazyInitTmpStorage()\npublic android.net.LinkAddress[] getLinkAddresses4()\nprivate boolean patternEquals(java.util.regex.Pattern)\nprivate int patternHashCode()\nprivate void onConstructed()\npublic void dump(java.io.PrintWriter)\nclass SampleDataClass extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genBuilder=true, genConstructor=true, genEqualsHashCode=true, genToString=true, genForEachField=true, genSetters=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + +} diff --git a/tests/Codegen/src/com/android/codegentest/SampleDataClassTest.java b/tests/Codegen/src/com/android/codegentest/SampleDataClassTest.java new file mode 100644 index 000000000000..d13257743e21 --- /dev/null +++ b/tests/Codegen/src/com/android/codegentest/SampleDataClassTest.java @@ -0,0 +1,277 @@ +/* + * 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.codegentest; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import android.net.LinkAddress; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.SparseArray; +import android.util.SparseIntArray; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Tests {@link SampleDataClass} after it's augmented with dataclass codegen. + * + * Use {@code $ . runTest.sh} to run. + */ +@RunWith(AndroidJUnit4.class) +public class SampleDataClassTest { + + private SampleDataClass mSpecimen = newBuilder().build(); + + private static SampleDataClass.Builder newBuilder() { + return newInvalidBuilder() + .setNum(42) + .setNum2(42) + .setNum4(42) + .setName4("foobar") + .setLinkAddresses5(); + } + + private static SampleDataClass.Builder newInvalidBuilder() { + return new SampleDataClass.Builder(1, 2, 3, "a", 0, null) + .setName("some parcelable") + .setFlags(SampleDataClass.FLAG_MANUAL_REQUEST); + } + + @Test + public void testParcelling_producesEqualInstance() { + SampleDataClass copy = parcelAndUnparcel(mSpecimen, SampleDataClass.CREATOR); + assertEquals(mSpecimen, copy); + assertEquals(mSpecimen.hashCode(), copy.hashCode()); + } + + @Test + public void testParcelling_producesInstanceWithEqualFields() { + SampleDataClass copy = parcelAndUnparcel(mSpecimen, SampleDataClass.CREATOR); + copy.forEachField((self, copyFieldName, copyFieldValue) -> { + mSpecimen.forEachField((self2, specimenFieldName, specimenFieldValue) -> { + if (copyFieldName.equals(specimenFieldName) + && !copyFieldName.equals("pattern") + && (specimenFieldValue == null + || !specimenFieldValue.getClass().isArray())) { + assertEquals("Mismatched field values for " + copyFieldName, + specimenFieldValue, copyFieldValue); + } + }); + }); + } + + @Test + public void testCustomParcelling_instanceIsCached() { + parcelAndUnparcel(mSpecimen, SampleDataClass.CREATOR); + parcelAndUnparcel(mSpecimen, SampleDataClass.CREATOR); + assertEquals(1, MyDateParcelling.sInstanceCount.get()); + } + + @Test + public void testDefaultFieldValue_isPropagated() { + assertEquals(new Date(42 * 42), mSpecimen.getDate()); + } + + @Test + public void testForEachField_avoidsBoxing() { + AtomicInteger intFieldCount = new AtomicInteger(0); + mSpecimen.forEachField( + (self, name, intValue) -> intFieldCount.getAndIncrement(), + (self, name, objectValue) -> { + if (objectValue != null) { + assertThat("Boxed field " + name, + objectValue, not(instanceOf(Integer.class))); + } + }); + assertThat(intFieldCount.get(), greaterThanOrEqualTo(1)); + } + + @Test + public void testToString_containsEachField() { + String toString = mSpecimen.toString(); + + mSpecimen.forEachField((self, name, value) -> { + assertThat(toString, containsString(name)); + if (value instanceof Integer) { + // Could be flags, their special toString tested separately + } else if (value instanceof Object[]) { + assertThat(toString, containsString(Arrays.toString((Object[]) value))); + } else if (value != null && value.getClass().isArray()) { + // Primitive array, uses multiple specialized Arrays.toString overloads + } else { + assertThat(toString, containsString("" + value)); + } + }); + } + + @Test + public void testBuilder_propagatesValuesToInstance() { + assertEquals(43, newBuilder().setNum(43).build().getNum()); + } + + @Test + public void testPluralFields_canHaveCustomSingularBuilderName() { + newBuilder().addLinkAddress(new LinkAddress("127.0.0.1/24")); + } + + @Test(expected = IllegalStateException.class) + public void testBuilder_usableOnlyOnce() { + SampleDataClass.Builder builder = newBuilder(); + builder.build(); + builder.build(); + } + + @Test(expected = IllegalStateException.class) + public void testBuilder_performsValidation() { + newInvalidBuilder().build(); + } + + @Test + public void testIntDefs_haveCorrectToString() { + int flagsAsInt = SampleDataClass.FLAG_MANUAL_REQUEST + | SampleDataClass.FLAG_COMPATIBILITY_MODE_REQUEST; + String flagsAsString = SampleDataClass.requestFlagsToString(flagsAsInt); + + assertThat(flagsAsString, containsString("MANUAL_REQUEST")); + assertThat(flagsAsString, containsString("COMPATIBILITY_MODE_REQUEST")); + assertThat(flagsAsString, not(containsString("1"))); + assertThat(flagsAsString, not(containsString("" + flagsAsInt))); + + String dataclassToString = newBuilder() + .setFlags(flagsAsInt) + .setState(SampleDataClass.STATE_UNDEFINED) + .build() + .toString(); + assertThat(dataclassToString, containsString(flagsAsString)); + assertThat(dataclassToString, containsString("UNDEFINED")); + } + + @Test(expected = IllegalArgumentException.class) + public void testFlags_getValidated() { + newBuilder().setFlags(12345).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testIntEnums_getValidated() { + newBuilder().setState(12345).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testStringEnums_getValidated() { + newBuilder().setStateName("foo").build(); + } + + @Test(expected = IllegalStateException.class) + public void testCustomValidation_isTriggered() { + newBuilder().setNum2(-1).setNum4(1).build(); + } + + @Test + public void testLazyInit_isLazilyCalledOnce() { + assertNull(mSpecimen.mTmpStorage); + + int[] tmpStorage = mSpecimen.getTmpStorage(); + assertNotNull(tmpStorage); + assertSame(tmpStorage, mSpecimen.mTmpStorage); + + int[] tmpStorageAgain = mSpecimen.getTmpStorage(); + assertSame(tmpStorage, tmpStorageAgain); + } + + @Test(expected = IllegalStateException.class) + public void testCustomAnnotationValidation_isRun() { + newBuilder().setDayOfWeek(42).build(); + } + + @Test + public void testDataStructures_parcelCorrectly() { + SampleWithCustomBuilder otherParcelable = new SampleWithCustomBuilder.Builder().setDelay(3, SECONDS).build(); + + ParcelAllTheThingsDataClass instance = new ParcelAllTheThingsDataClass.Builder() + .setIntArray(40, 41) + .addMap("foo", otherParcelable) + .setSparseArray(new SparseArray<SampleWithCustomBuilder>() {{ + put(45, otherParcelable); + }}) + .setSparseIntArray(new SparseIntArray() {{ + put(48, 49); + }}) + .addStringMap("foo2", "fooValue") + .setStringArray("foo", "bar") + .addStringList("foo") + .build(); + + ParcelAllTheThingsDataClass unparceledInstance = + parcelAndUnparcel(instance, ParcelAllTheThingsDataClass.CREATOR); + + // SparseArray and friends don't implement equals + // so just compare string representations instead + assertEquals(instance.toString(), unparceledInstance.toString()); + } + + @Test + public void testNestedDataClasses_notMangledWhenParceled() { + assertEqualsAfterParcelling( + new SampleWithNestedDataClasses.NestedDataClass("1"), + SampleWithNestedDataClasses.NestedDataClass.CREATOR); + + assertEqualsAfterParcelling( + new SampleWithNestedDataClasses.NestedDataClass2("2"), + SampleWithNestedDataClasses.NestedDataClass2.CREATOR); + + assertEqualsAfterParcelling( + new SampleWithNestedDataClasses.NestedDataClass2.NestedDataClass3(3), + SampleWithNestedDataClasses.NestedDataClass2.NestedDataClass3.CREATOR); + } + + private static <T extends Parcelable> void assertEqualsAfterParcelling( + T p, Parcelable.Creator<T> creator) { + assertEquals(p, parcelAndUnparcel(p, creator)); + } + + private static <T extends Parcelable> T parcelAndUnparcel( + T original, Parcelable.Creator<T> creator) { + Parcel p = Parcel.obtain(); + try { + original.writeToParcel(p, 0); + p.setDataPosition(0); + return creator.createFromParcel(p); + } finally { + p.recycle(); + } + } +} diff --git a/tests/Codegen/src/com/android/codegentest/SampleWithCustomBuilder.java b/tests/Codegen/src/com/android/codegentest/SampleWithCustomBuilder.java new file mode 100644 index 000000000000..d9fe1fd5a465 --- /dev/null +++ b/tests/Codegen/src/com/android/codegentest/SampleWithCustomBuilder.java @@ -0,0 +1,267 @@ +/* + * 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.codegentest; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; + +import com.android.internal.util.DataClass; + +import java.util.concurrent.TimeUnit; + +@DataClass(genBuilder = true, genAidl = false, genToString = true) +public class SampleWithCustomBuilder implements Parcelable { + + long delayAmount = 0; + @NonNull + TimeUnit delayUnit = TimeUnit.MILLISECONDS; + + long creationTimestamp = SystemClock.uptimeMillis(); + + /** + * You can declare a class named {@code BaseBuilder} to have the generated builder extend from + * it instead. + * + * Same rules apply where defining a non-abstract method will suppress the generation of a + * method with the same signature. + * + * For abstract generatable methods, implementations are generated as normal, but original + * visibility is used, allowing you to hide methods. + * + * Here for example, we hide {@link #setDelayUnit} and {@link #setDelayAmount} from public API, + * replacing it with {@link #setDelay} instead. + */ + // Suppress setter generation for a field that is not supposed to come from user input. + @DataClass.Suppress("setCreationTimestamp") + static abstract class BaseBuilder { + + /** + * Hide methods by declaring them with reduced (package-private) visibility. + */ + abstract Builder setDelayAmount(long value); + + /** + * Alternatively, hide methods by using @hide, to hide them from public API only. + * + * @hide + */ + public abstract Builder setDelayUnit(TimeUnit value); + + /** + * Can provide additional method on the builder, e.g. as a replacement for the ones we've + * just hidden. + */ + public Builder setDelay(long amount, TimeUnit unit) { + setDelayAmount(amount); + setDelayUnit(unit); + return (Builder) this; + } + } + + + private static TimeUnit unparcelDelayUnit(Parcel p) { + return TimeUnit.values()[p.readInt()]; + } + + private void parcelDelayUnit(Parcel p, int flags) { + p.writeInt(delayUnit.ordinal()); + } + + + + // Code below generated by codegen v1.0.15. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/SampleWithCustomBuilder.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + @DataClass.Generated.Member + /* package-private */ SampleWithCustomBuilder( + long delayAmount, + @NonNull TimeUnit delayUnit, + long creationTimestamp) { + this.delayAmount = delayAmount; + this.delayUnit = delayUnit; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, delayUnit); + this.creationTimestamp = creationTimestamp; + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public long getDelayAmount() { + return delayAmount; + } + + @DataClass.Generated.Member + public @NonNull TimeUnit getDelayUnit() { + return delayUnit; + } + + @DataClass.Generated.Member + public long getCreationTimestamp() { + return creationTimestamp; + } + + @Override + @DataClass.Generated.Member + public String toString() { + // You can override field toString logic by defining methods like: + // String fieldNameToString() { ... } + + return "SampleWithCustomBuilder { " + + "delayAmount = " + delayAmount + ", " + + "delayUnit = " + delayUnit + ", " + + "creationTimestamp = " + creationTimestamp + + " }"; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + dest.writeLong(delayAmount); + parcelDelayUnit(dest, flags); + dest.writeLong(creationTimestamp); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + protected SampleWithCustomBuilder(@NonNull Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + long _delayAmount = in.readLong(); + TimeUnit _delayUnit = unparcelDelayUnit(in); + long _creationTimestamp = in.readLong(); + + this.delayAmount = _delayAmount; + this.delayUnit = _delayUnit; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, delayUnit); + this.creationTimestamp = _creationTimestamp; + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<SampleWithCustomBuilder> CREATOR + = new Parcelable.Creator<SampleWithCustomBuilder>() { + @Override + public SampleWithCustomBuilder[] newArray(int size) { + return new SampleWithCustomBuilder[size]; + } + + @Override + public SampleWithCustomBuilder createFromParcel(@NonNull Parcel in) { + return new SampleWithCustomBuilder(in); + } + }; + + /** + * A builder for {@link SampleWithCustomBuilder} + */ + @SuppressWarnings("WeakerAccess") + @DataClass.Generated.Member + public static class Builder extends BaseBuilder { + + private long delayAmount; + private @NonNull TimeUnit delayUnit; + private long creationTimestamp; + + private long mBuilderFieldsSet = 0L; + + public Builder() { + } + + @DataClass.Generated.Member + @Override + @NonNull Builder setDelayAmount(long value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x1; + delayAmount = value; + return this; + } + + @DataClass.Generated.Member + @Override + public @NonNull Builder setDelayUnit(@NonNull TimeUnit value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x2; + delayUnit = value; + return this; + } + + /** Builds the instance. This builder should not be touched after calling this! */ + public @NonNull SampleWithCustomBuilder build() { + checkNotUsed(); + mBuilderFieldsSet |= 0x8; // Mark builder used + + if ((mBuilderFieldsSet & 0x1) == 0) { + delayAmount = 0; + } + if ((mBuilderFieldsSet & 0x2) == 0) { + delayUnit = TimeUnit.MILLISECONDS; + } + if ((mBuilderFieldsSet & 0x4) == 0) { + creationTimestamp = SystemClock.uptimeMillis(); + } + SampleWithCustomBuilder o = new SampleWithCustomBuilder( + delayAmount, + delayUnit, + creationTimestamp); + return o; + } + + private void checkNotUsed() { + if ((mBuilderFieldsSet & 0x8) != 0) { + throw new IllegalStateException( + "This Builder should not be reused. Use a new Builder instance instead"); + } + } + } + + @DataClass.Generated( + time = 1582685648622L, + codegenVersion = "1.0.15", + sourceFile = "frameworks/base/tests/Codegen/src/com/android/codegentest/SampleWithCustomBuilder.java", + inputSignatures = " long delayAmount\n @android.annotation.NonNull java.util.concurrent.TimeUnit delayUnit\n long creationTimestamp\nprivate static java.util.concurrent.TimeUnit unparcelDelayUnit(android.os.Parcel)\nprivate void parcelDelayUnit(android.os.Parcel,int)\nclass SampleWithCustomBuilder extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genBuilder=true, genAidl=false, genToString=true)\nabstract com.android.codegentest.SampleWithCustomBuilder.Builder setDelayAmount(long)\npublic abstract com.android.codegentest.SampleWithCustomBuilder.Builder setDelayUnit(java.util.concurrent.TimeUnit)\npublic com.android.codegentest.SampleWithCustomBuilder.Builder setDelay(long,java.util.concurrent.TimeUnit)\nclass BaseBuilder extends java.lang.Object implements []") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + +} diff --git a/tests/Codegen/src/com/android/codegentest/SampleWithNestedDataClasses.java b/tests/Codegen/src/com/android/codegentest/SampleWithNestedDataClasses.java new file mode 100644 index 000000000000..f98d7b05c2d3 --- /dev/null +++ b/tests/Codegen/src/com/android/codegentest/SampleWithNestedDataClasses.java @@ -0,0 +1,390 @@ +/* + * 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.codegentest; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.DataClass; + +/** + * An example of deeply nested data classes + */ +public class SampleWithNestedDataClasses { + + int mFoo = 0; + + @DataClass(genEqualsHashCode = true) + public static class NestedDataClass implements Parcelable { + + @NonNull String mBar; + + + + // Code below generated by codegen v1.0.15. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/SampleWithNestedDataClasses.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + @DataClass.Generated.Member + public NestedDataClass( + @NonNull String bar) { + this.mBar = bar; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mBar); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public @NonNull String getBar() { + return mBar; + } + + @Override + @DataClass.Generated.Member + public boolean equals(@android.annotation.Nullable Object o) { + // You can override field equality logic by defining either of the methods like: + // boolean fieldNameEquals(NestedDataClass other) { ... } + // boolean fieldNameEquals(FieldType otherValue) { ... } + + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unchecked") + NestedDataClass that = (NestedDataClass) o; + //noinspection PointlessBooleanExpression + return true + && java.util.Objects.equals(mBar, that.mBar); + } + + @Override + @DataClass.Generated.Member + public int hashCode() { + // You can override field hashCode logic by defining methods like: + // int fieldNameHashCode() { ... } + + int _hash = 1; + _hash = 31 * _hash + java.util.Objects.hashCode(mBar); + return _hash; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + dest.writeString(mBar); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + protected NestedDataClass(@NonNull Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + String bar = in.readString(); + + this.mBar = bar; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mBar); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<NestedDataClass> CREATOR + = new Parcelable.Creator<NestedDataClass>() { + @Override + public NestedDataClass[] newArray(int size) { + return new NestedDataClass[size]; + } + + @Override + public NestedDataClass createFromParcel(@NonNull Parcel in) { + return new NestedDataClass(in); + } + }; + + @DataClass.Generated( + time = 1582685653406L, + codegenVersion = "1.0.15", + sourceFile = "frameworks/base/tests/Codegen/src/com/android/codegentest/SampleWithNestedDataClasses.java", + inputSignatures = " @android.annotation.NonNull java.lang.String mBar\nclass NestedDataClass extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + + } + + @DataClass(genEqualsHashCode = true) + public static class NestedDataClass2 implements Parcelable { + + @NonNull String mBaz; + + @DataClass(genEqualsHashCode = true) + public static class NestedDataClass3 implements Parcelable { + + @NonNull long mBaz2; + + + + // Code below generated by codegen v1.0.15. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/SampleWithNestedDataClasses.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + @DataClass.Generated.Member + public NestedDataClass3( + @NonNull long baz2) { + this.mBaz2 = baz2; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mBaz2); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public @NonNull long getBaz2() { + return mBaz2; + } + + @Override + @DataClass.Generated.Member + public boolean equals(@android.annotation.Nullable Object o) { + // You can override field equality logic by defining either of the methods like: + // boolean fieldNameEquals(NestedDataClass3 other) { ... } + // boolean fieldNameEquals(FieldType otherValue) { ... } + + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unchecked") + NestedDataClass3 that = (NestedDataClass3) o; + //noinspection PointlessBooleanExpression + return true + && mBaz2 == that.mBaz2; + } + + @Override + @DataClass.Generated.Member + public int hashCode() { + // You can override field hashCode logic by defining methods like: + // int fieldNameHashCode() { ... } + + int _hash = 1; + _hash = 31 * _hash + Long.hashCode(mBaz2); + return _hash; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + dest.writeLong(mBaz2); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + protected NestedDataClass3(@NonNull Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + long baz2 = in.readLong(); + + this.mBaz2 = baz2; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mBaz2); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<NestedDataClass3> CREATOR + = new Parcelable.Creator<NestedDataClass3>() { + @Override + public NestedDataClass3[] newArray(int size) { + return new NestedDataClass3[size]; + } + + @Override + public NestedDataClass3 createFromParcel(@NonNull Parcel in) { + return new NestedDataClass3(in); + } + }; + + @DataClass.Generated( + time = 1582685653415L, + codegenVersion = "1.0.15", + sourceFile = "frameworks/base/tests/Codegen/src/com/android/codegentest/SampleWithNestedDataClasses.java", + inputSignatures = " @android.annotation.NonNull long mBaz2\nclass NestedDataClass3 extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + + } + + + + // Code below generated by codegen v1.0.15. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/SampleWithNestedDataClasses.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + @DataClass.Generated.Member + public NestedDataClass2( + @NonNull String baz) { + this.mBaz = baz; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mBaz); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public @NonNull String getBaz() { + return mBaz; + } + + @Override + @DataClass.Generated.Member + public boolean equals(@android.annotation.Nullable Object o) { + // You can override field equality logic by defining either of the methods like: + // boolean fieldNameEquals(NestedDataClass2 other) { ... } + // boolean fieldNameEquals(FieldType otherValue) { ... } + + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unchecked") + NestedDataClass2 that = (NestedDataClass2) o; + //noinspection PointlessBooleanExpression + return true + && java.util.Objects.equals(mBaz, that.mBaz); + } + + @Override + @DataClass.Generated.Member + public int hashCode() { + // You can override field hashCode logic by defining methods like: + // int fieldNameHashCode() { ... } + + int _hash = 1; + _hash = 31 * _hash + java.util.Objects.hashCode(mBaz); + return _hash; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + dest.writeString(mBaz); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + protected NestedDataClass2(@NonNull Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + String baz = in.readString(); + + this.mBaz = baz; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mBaz); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<NestedDataClass2> CREATOR + = new Parcelable.Creator<NestedDataClass2>() { + @Override + public NestedDataClass2[] newArray(int size) { + return new NestedDataClass2[size]; + } + + @Override + public NestedDataClass2 createFromParcel(@NonNull Parcel in) { + return new NestedDataClass2(in); + } + }; + + @DataClass.Generated( + time = 1582685653420L, + codegenVersion = "1.0.15", + sourceFile = "frameworks/base/tests/Codegen/src/com/android/codegentest/SampleWithNestedDataClasses.java", + inputSignatures = " @android.annotation.NonNull java.lang.String mBaz\nclass NestedDataClass2 extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + + } + + void someCode() {} +} diff --git a/tests/Codegen/src/com/android/codegentest/StaleDataclassDetectorFalsePositivesTest.java b/tests/Codegen/src/com/android/codegentest/StaleDataclassDetectorFalsePositivesTest.java new file mode 100644 index 000000000000..6b4fc2fa157f --- /dev/null +++ b/tests/Codegen/src/com/android/codegentest/StaleDataclassDetectorFalsePositivesTest.java @@ -0,0 +1,79 @@ +/* + * 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.codegentest; + +import android.annotation.NonNull; + +import com.android.internal.util.DataClass; + +/** + * Test for some false positive pitfalls for + * {@link android.processor.staledataclass.StaleDataclassProcessor} + * + * Relies on the detector being run, failing the build should any of things here falsely + * register as stale. + */ +@DataClass(genConstructor = false, genBuilder = false) +public class StaleDataclassDetectorFalsePositivesTest { + + /** Interfaces should be ignored */ + public interface SomeListener { + void onEvent(); + } + + /** Enums should be ignored */ + private enum SomeEnum { ONE, TWO } + + /** Annotations should be ignored */ + public @interface SomeAnnotation {} + + /* Static initializers should be ignored */ + static {} + + /* Initializers should be ignored */ + {} + + /** Unrelated methods should be noted, without triggering staleness false positives */ + public @NonNull String someMethod(int param) { return null; } + + + + // Code below generated by codegen v1.0.15. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/StaleDataclassDetectorFalsePositivesTest.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + @DataClass.Generated( + time = 1582685652436L, + codegenVersion = "1.0.15", + sourceFile = "frameworks/base/tests/Codegen/src/com/android/codegentest/StaleDataclassDetectorFalsePositivesTest.java", + inputSignatures = "public @android.annotation.NonNull java.lang.String someMethod(int)\nclass StaleDataclassDetectorFalsePositivesTest extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=false)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + +} diff --git a/tests/Compatibility/Android.bp b/tests/Compatibility/Android.bp index 4ca406eba3cf..7dc44fa4a260 100644 --- a/tests/Compatibility/Android.bp +++ b/tests/Compatibility/Android.bp @@ -19,4 +19,7 @@ android_test { srcs: ["src/**/*.java"], platform_apis: true, certificate: "platform", + test_suites: [ + "csuite" + ], } diff --git a/tests/DozeTest/src/com/android/dreams/dozetest/DozeTestDream.java b/tests/DozeTest/src/com/android/dreams/dozetest/DozeTestDream.java index f4f610b1b280..fa292bd0d57a 100644 --- a/tests/DozeTest/src/com/android/dreams/dozetest/DozeTestDream.java +++ b/tests/DozeTest/src/com/android/dreams/dozetest/DozeTestDream.java @@ -100,7 +100,6 @@ public class DozeTestDream extends DreamService { public void onAttachedToWindow() { super.onAttachedToWindow(); setInteractive(false); - setLowProfile(true); setFullscreen(true); setContentView(R.layout.dream); setScreenBright(false); diff --git a/tests/DynamicCodeLoggerIntegrationTests/src/com/android/server/pm/dex/DynamicCodeLoggerIntegrationTests.java b/tests/DynamicCodeLoggerIntegrationTests/src/com/android/server/pm/dex/DynamicCodeLoggerIntegrationTests.java index db2f659c655b..883c172e4990 100644 --- a/tests/DynamicCodeLoggerIntegrationTests/src/com/android/server/pm/dex/DynamicCodeLoggerIntegrationTests.java +++ b/tests/DynamicCodeLoggerIntegrationTests/src/com/android/server/pm/dex/DynamicCodeLoggerIntegrationTests.java @@ -27,8 +27,8 @@ import android.os.SystemClock; import android.util.EventLog; import android.util.EventLog.Event; -import androidx.test.InstrumentationRegistry; import androidx.test.filters.LargeTest; +import androidx.test.platform.app.InstrumentationRegistry; import dalvik.system.DexClassLoader; @@ -91,7 +91,7 @@ public final class DynamicCodeLoggerIntegrationTests { @BeforeClass public static void setUpAll() { - sContext = InstrumentationRegistry.getTargetContext(); + sContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); sMyUid = android.os.Process.myUid(); } @@ -331,7 +331,7 @@ public final class DynamicCodeLoggerIntegrationTests { // 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 { + private abstract static class TestNativeCodeWithRetries { String mExpectedContentHash; String mExpectedNameHash; diff --git a/tests/FeatureSplit/feature1/Android.bp b/tests/FeatureSplit/feature1/Android.bp index 1a93e842cec5..706a4f544393 100644 --- a/tests/FeatureSplit/feature1/Android.bp +++ b/tests/FeatureSplit/feature1/Android.bp @@ -18,7 +18,7 @@ android_test { name: "FeatureSplit1", srcs: ["**/*.java"], sdk_version: "current", - libs: ["FeatureSplitBase"], + libs: ["FeatureSplitBase", "FeatureSplit2"], aaptflags: [ "--package-id", "0x80", diff --git a/tests/FeatureSplit/feature1/AndroidManifest.xml b/tests/FeatureSplit/feature1/AndroidManifest.xml index b87361faac62..086c2c33422d 100644 --- a/tests/FeatureSplit/feature1/AndroidManifest.xml +++ b/tests/FeatureSplit/feature1/AndroidManifest.xml @@ -19,6 +19,7 @@ featureSplit="feature1"> <uses-sdk android:minSdkVersion="21" /> + <uses-split android:name="feature2" /> <application> <activity android:name=".one.One" android:label="Feature One"> diff --git a/tests/FeatureSplit/feature1/res/layout/included.xml b/tests/FeatureSplit/feature1/res/layout/included.xml index c64bdb7ff85f..f0c56f872438 100644 --- a/tests/FeatureSplit/feature1/res/layout/included.xml +++ b/tests/FeatureSplit/feature1/res/layout/included.xml @@ -2,4 +2,5 @@ <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/text" android:layout_width="wrap_content" - android:layout_height="wrap_content" /> + android:layout_height="wrap_content" + android:text="@string/feature2_string" /> diff --git a/tests/FeatureSplit/feature1/res/values/values.xml b/tests/FeatureSplit/feature1/res/values/values.xml index 7d58865b546e..6a840df2c79c 100644 --- a/tests/FeatureSplit/feature1/res/values/values.xml +++ b/tests/FeatureSplit/feature1/res/values/values.xml @@ -20,7 +20,8 @@ <integer name="test_integer2">200</integer> <color name="test_color2">#00ff00</color> <string-array name="string_array2"> - <item>@string/app_title</item> + <item>@string/app_title</item> + <item>@string/feature2_string</item> </string-array> </resources> diff --git a/tests/FeatureSplit/feature2/res/values/values.xml b/tests/FeatureSplit/feature2/res/values/values.xml index af5ed1b79b26..70e772c0d019 100644 --- a/tests/FeatureSplit/feature2/res/values/values.xml +++ b/tests/FeatureSplit/feature2/res/values/values.xml @@ -15,10 +15,11 @@ --> <resources> + <string name="feature2_string">feature 2 string referenced from feature 1</string> <integer name="test_integer3">300</integer> <color name="test_color3">#0000ff</color> <string-array name="string_array3"> - <item>@string/app_title</item> + <item>@string/app_title</item> </string-array> </resources> diff --git a/tests/FlickerTests/TEST_MAPPING b/tests/FlickerTests/TEST_MAPPING index 55a61471dfb8..db251b907caa 100644 --- a/tests/FlickerTests/TEST_MAPPING +++ b/tests/FlickerTests/TEST_MAPPING @@ -1,5 +1,11 @@ { "postsubmit": [ + // Run tests on real device + { + "name": "FlickerTests", + "keywords": ["primary-device"] + }, + // Also run the tests in the cloud { "name": "FlickerTests" } diff --git a/tests/GamePerformance/AndroidManifest.xml b/tests/GamePerformance/AndroidManifest.xml index b331e2c07e14..2ff7fa65664e 100644 --- a/tests/GamePerformance/AndroidManifest.xml +++ b/tests/GamePerformance/AndroidManifest.xml @@ -16,7 +16,9 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="android.gameperformance"> + package="android.gameperformance" + android:versionCode="3" + android:versionName="3.0" > <uses-sdk android:minSdkVersion="25"/> <uses-feature android:glEsVersion="0x00020000" android:required="true" /> @@ -24,7 +26,8 @@ <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" > + <activity android:name="android.gameperformance.GamePerformanceActivity" + android:screenOrientation="landscape" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> diff --git a/tests/GamePerformance/res/drawable/animation.xml b/tests/GamePerformance/res/drawable/animation.xml new file mode 100644 index 000000000000..b423ff0d1ee4 --- /dev/null +++ b/tests/GamePerformance/res/drawable/animation.xml @@ -0,0 +1,29 @@ +<!-- + * 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. + --> + +<animation-list xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/animation" android:oneshot="false"> + <item android:drawable="@drawable/digit_0" android:duration="15" /> + <item android:drawable="@drawable/digit_1" android:duration="15" /> + <item android:drawable="@drawable/digit_2" android:duration="15" /> + <item android:drawable="@drawable/digit_3" android:duration="15" /> + <item android:drawable="@drawable/digit_4" android:duration="15" /> + <item android:drawable="@drawable/digit_5" android:duration="15" /> + <item android:drawable="@drawable/digit_6" android:duration="15" /> + <item android:drawable="@drawable/digit_7" android:duration="15" /> + <item android:drawable="@drawable/digit_8" android:duration="15" /> + <item android:drawable="@drawable/digit_9" android:duration="15" /> + </animation-list>
\ No newline at end of file diff --git a/tests/GamePerformance/res/drawable/digit_0.png b/tests/GamePerformance/res/drawable/digit_0.png Binary files differnew file mode 100644 index 000000000000..7264e3ee3771 --- /dev/null +++ b/tests/GamePerformance/res/drawable/digit_0.png diff --git a/tests/GamePerformance/res/drawable/digit_1.png b/tests/GamePerformance/res/drawable/digit_1.png Binary files differnew file mode 100644 index 000000000000..f098a71a4ab4 --- /dev/null +++ b/tests/GamePerformance/res/drawable/digit_1.png diff --git a/tests/GamePerformance/res/drawable/digit_2.png b/tests/GamePerformance/res/drawable/digit_2.png Binary files differnew file mode 100644 index 000000000000..f08cd31b4118 --- /dev/null +++ b/tests/GamePerformance/res/drawable/digit_2.png diff --git a/tests/GamePerformance/res/drawable/digit_3.png b/tests/GamePerformance/res/drawable/digit_3.png Binary files differnew file mode 100644 index 000000000000..497df8a9f473 --- /dev/null +++ b/tests/GamePerformance/res/drawable/digit_3.png diff --git a/tests/GamePerformance/res/drawable/digit_4.png b/tests/GamePerformance/res/drawable/digit_4.png Binary files differnew file mode 100644 index 000000000000..10efe8cf11b2 --- /dev/null +++ b/tests/GamePerformance/res/drawable/digit_4.png diff --git a/tests/GamePerformance/res/drawable/digit_5.png b/tests/GamePerformance/res/drawable/digit_5.png Binary files differnew file mode 100644 index 000000000000..1018a2fad733 --- /dev/null +++ b/tests/GamePerformance/res/drawable/digit_5.png diff --git a/tests/GamePerformance/res/drawable/digit_6.png b/tests/GamePerformance/res/drawable/digit_6.png Binary files differnew file mode 100644 index 000000000000..593c467d1529 --- /dev/null +++ b/tests/GamePerformance/res/drawable/digit_6.png diff --git a/tests/GamePerformance/res/drawable/digit_7.png b/tests/GamePerformance/res/drawable/digit_7.png Binary files differnew file mode 100644 index 000000000000..041b95fd8748 --- /dev/null +++ b/tests/GamePerformance/res/drawable/digit_7.png diff --git a/tests/GamePerformance/res/drawable/digit_8.png b/tests/GamePerformance/res/drawable/digit_8.png Binary files differnew file mode 100644 index 000000000000..f8fa4969cb28 --- /dev/null +++ b/tests/GamePerformance/res/drawable/digit_8.png diff --git a/tests/GamePerformance/res/drawable/digit_9.png b/tests/GamePerformance/res/drawable/digit_9.png Binary files differnew file mode 100644 index 000000000000..303b1da3d3f8 --- /dev/null +++ b/tests/GamePerformance/res/drawable/digit_9.png diff --git a/tests/GamePerformance/res/drawable/logo.png b/tests/GamePerformance/res/drawable/logo.png Binary files differnew file mode 100644 index 000000000000..61391df52077 --- /dev/null +++ b/tests/GamePerformance/res/drawable/logo.png diff --git a/tests/GamePerformance/src/android/gameperformance/BaseTest.java b/tests/GamePerformance/src/android/gameperformance/BaseTest.java new file mode 100644 index 000000000000..e7057565499c --- /dev/null +++ b/tests/GamePerformance/src/android/gameperformance/BaseTest.java @@ -0,0 +1,149 @@ +/* + * 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.gameperformance; + +import java.text.DecimalFormat; +import java.util.concurrent.TimeUnit; + +import android.annotation.NonNull; +import android.content.Context; +import android.util.Log; + +/** + * Base class for a test that performs bisection to determine maximum + * performance of a metric test measures. + */ +public abstract class BaseTest { + private final static String TAG = "BaseTest"; + + // Time to wait for render warm up. No statistics is collected during this pass. + private final static long WARM_UP_TIME = TimeUnit.SECONDS.toMillis(5); + + // Perform pass to probe the configuration using iterations. After each iteration current FPS is + // checked and if it looks obviously bad, pass gets stopped earlier. Once all iterations are + // done and final FPS is above PASS_THRESHOLD pass to probe is considered successful. + private final static long TEST_ITERATION_TIME = TimeUnit.SECONDS.toMillis(12); + private final static int TEST_ITERATION_COUNT = 5; + + // FPS pass test threshold, in ratio from ideal FPS, that matches device + // refresh rate. + private final static double PASS_THRESHOLD = 0.95; + // FPS threshold, in ratio from ideal FPS, to identify that current pass to probe is obviously + // bad and to stop pass earlier. + private final static double OBVIOUS_BAD_THRESHOLD = 0.90; + + private static DecimalFormat DOUBLE_FORMATTER = new DecimalFormat("#.##"); + + private final GamePerformanceActivity mActivity; + + // Device's refresh rate. + private final double mRefreshRate; + + public BaseTest(@NonNull GamePerformanceActivity activity) { + mActivity = activity; + mRefreshRate = activity.getDisplay().getRefreshRate(); + } + + @NonNull + public Context getContext() { + return mActivity; + } + + @NonNull + public GamePerformanceActivity getActivity() { + return mActivity; + } + + // Returns name of the test. + public abstract String getName(); + + // Returns unit name. + public abstract String getUnitName(); + + // Returns number of measured units per one bisection unit. + public abstract double getUnitScale(); + + // Initializes test. + public abstract void initUnits(double unitCount); + + // Initializes probe pass. + protected abstract void initProbePass(int probe); + + // Frees probe pass. + protected abstract void freeProbePass(); + + /** + * Performs the test and returns maximum number of measured units achieved. Unit is test + * specific and name is returned by getUnitName. Returns 0 in case of failure. + */ + public double run() { + try { + Log.i(TAG, "Test started " + getName()); + + final double passFps = PASS_THRESHOLD * mRefreshRate; + final double obviousBadFps = OBVIOUS_BAD_THRESHOLD * mRefreshRate; + + // Bisection bounds. Probe value is taken as middle point. Then it used to initialize + // test with probe * getUnitScale units. In case probe passed, lowLimit is updated to + // probe, otherwise upLimit is updated to probe. lowLimit contains probe that passes + // and upLimit contains the probe that fails. Each iteration narrows the range. + // Iterations continue until range is collapsed and lowLimit contains actual test + // result. + int lowLimit = 0; // Initially 0, that is recognized as failure. + int upLimit = 250; + + while (true) { + int probe = (lowLimit + upLimit) / 2; + if (probe == lowLimit) { + Log.i(TAG, "Test done: " + DOUBLE_FORMATTER.format(probe * getUnitScale()) + + " " + getUnitName()); + return probe * getUnitScale(); + } + + Log.i(TAG, "Start probe: " + DOUBLE_FORMATTER.format(probe * getUnitScale()) + " " + + getUnitName()); + initProbePass(probe); + + Thread.sleep(WARM_UP_TIME); + + getActivity().resetFrameTimes(); + + double fps = 0.0f; + for (int i = 0; i < TEST_ITERATION_COUNT; ++i) { + Thread.sleep(TEST_ITERATION_TIME); + fps = getActivity().getFps(); + if (fps < obviousBadFps) { + // Stop test earlier, we could not fit the loading. + break; + } + } + + freeProbePass(); + + Log.i(TAG, "Finish probe: " + DOUBLE_FORMATTER.format(probe * getUnitScale()) + + " " + getUnitName() + " - " + DOUBLE_FORMATTER.format(fps) + " FPS."); + if (fps < passFps) { + upLimit = probe; + } else { + lowLimit = probe; + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return 0; + } + } +}
\ No newline at end of file diff --git a/tests/GamePerformance/src/android/gameperformance/CPULoadThread.java b/tests/GamePerformance/src/android/gameperformance/CPULoadThread.java new file mode 100644 index 000000000000..fa6f03bf88cf --- /dev/null +++ b/tests/GamePerformance/src/android/gameperformance/CPULoadThread.java @@ -0,0 +1,61 @@ +/* + * 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.gameperformance; + +/** + * Ballast thread that emulates CPU load by performing heavy computation in loop. + */ +public class CPULoadThread extends Thread { + private boolean mStopRequest; + + public CPULoadThread() { + mStopRequest = false; + } + + private static double computePi() { + double accumulator = 0; + double prevAccumulator = -1; + int index = 1; + while (true) { + accumulator += ((1.0 / (2.0 * index - 1)) - (1.0 / (2.0 * index + 1))); + if (accumulator == prevAccumulator) { + break; + } + prevAccumulator = accumulator; + index += 2; + } + return 4 * accumulator; + } + + // Requests thread to stop. + public void issueStopRequest() { + synchronized (this) { + mStopRequest = true; + } + } + + @Override + public void run() { + // Load CPU by PI computation. + while (computePi() != 0) { + synchronized (this) { + if (mStopRequest) { + break; + } + } + } + } +} diff --git a/tests/GamePerformance/src/android/gameperformance/ControlsTest.java b/tests/GamePerformance/src/android/gameperformance/ControlsTest.java new file mode 100644 index 000000000000..6c36ddcc620d --- /dev/null +++ b/tests/GamePerformance/src/android/gameperformance/ControlsTest.java @@ -0,0 +1,73 @@ +/* + * 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.gameperformance; + +import android.annotation.NonNull; + +/** + * Tests that verifies how many UI controls can be handled to keep FPS close to device refresh rate. + * As a test UI control ImageView with an infinite animation is chosen. The animation has refresh + * rate ~67Hz that forces all devices to refresh UI at highest possible rate. + */ +public class ControlsTest extends BaseTest { + public ControlsTest(@NonNull GamePerformanceActivity activity) { + super(activity); + } + + @NonNull + public CustomControlView getView() { + return getActivity().getControlView(); + } + + @Override + protected void initProbePass(int probe) { + try { + getActivity().attachControlView(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + initUnits(probe * getUnitScale()); + } + + @Override + protected void freeProbePass() { + } + + @Override + public String getName() { + return "control_count"; + } + + @Override + public String getUnitName() { + return "controls"; + } + + @Override + public double getUnitScale() { + return 5.0; + } + + @Override + public void initUnits(double controlCount) { + try { + getView().createControls(getActivity(), (int)Math.round(controlCount)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +}
\ No newline at end of file diff --git a/tests/GamePerformance/src/android/gameperformance/CustomControlView.java b/tests/GamePerformance/src/android/gameperformance/CustomControlView.java new file mode 100644 index 000000000000..8d11a416c8a8 --- /dev/null +++ b/tests/GamePerformance/src/android/gameperformance/CustomControlView.java @@ -0,0 +1,129 @@ +/* + * 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.gameperformance; + +import android.annotation.MainThread; +import android.annotation.NonNull; +import android.annotation.WorkerThread; +import android.app.Activity; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.drawable.AnimationDrawable; +import android.view.WindowManager; +import android.widget.AbsoluteLayout; +import android.widget.ImageView; +import android.window.WindowMetricsHelper; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +/** + * View that holds requested number of UI controls as ImageView with an infinite animation. + */ +public class CustomControlView extends AbsoluteLayout { + private final static int CONTROL_DIMENSION = 48; + + private final int mPerRowControlCount; + private List<Long> mFrameTimes = new ArrayList<>(); + + public CustomControlView(@NonNull Context context) { + super(context); + + final WindowManager wm = context.getSystemService(WindowManager.class); + final int width = WindowMetricsHelper.getBoundsExcludingNavigationBarAndCutout( + wm.getCurrentWindowMetrics()).width(); + mPerRowControlCount = width / CONTROL_DIMENSION; + } + + /** + * Helper class that overrides ImageView and observes draw requests. Only + * one such control is created which is the first control in the view. + */ + class ReferenceImageView extends ImageView { + public ReferenceImageView(Context context) { + super(context); + } + @Override + public void draw(Canvas canvas) { + reportFrame(); + super.draw(canvas); + } + } + + @WorkerThread + public void createControls( + @NonNull Activity activity, int controlCount) throws InterruptedException { + synchronized (this) { + final CountDownLatch latch = new CountDownLatch(1); + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + removeAllViews(); + + for (int i = 0; i < controlCount; ++i) { + final ImageView image = (i == 0) ? + new ReferenceImageView(activity) : new ImageView(activity); + final int x = (i % mPerRowControlCount) * CONTROL_DIMENSION; + final int y = (i / mPerRowControlCount) * CONTROL_DIMENSION; + final AbsoluteLayout.LayoutParams layoutParams = + new AbsoluteLayout.LayoutParams( + CONTROL_DIMENSION, CONTROL_DIMENSION, x, y); + image.setLayoutParams(layoutParams); + image.setBackgroundResource(R.drawable.animation); + final AnimationDrawable animation = + (AnimationDrawable)image.getBackground(); + animation.start(); + addView(image); + } + + latch.countDown(); + } + }); + latch.await(); + } + } + + @MainThread + private void reportFrame() { + final long time = System.currentTimeMillis(); + synchronized (mFrameTimes) { + mFrameTimes.add(time); + } + } + + /** + * Resets frame times in order to calculate FPS for the 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)); + } + } +}
\ No newline at end of file diff --git a/tests/GamePerformance/src/android/gameperformance/CustomOpenGLView.java b/tests/GamePerformance/src/android/gameperformance/CustomOpenGLView.java index 2b37280ae9b5..08697ae95376 100644 --- a/tests/GamePerformance/src/android/gameperformance/CustomOpenGLView.java +++ b/tests/GamePerformance/src/android/gameperformance/CustomOpenGLView.java @@ -17,23 +17,36 @@ 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.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.opengl.GLES20; import android.opengl.GLSurfaceView; +import android.util.Log; public class CustomOpenGLView extends GLSurfaceView { - private Random mRandom; - private List<Long> mFrameTimes; + public final static String TAG = "CustomOpenGLView"; - public CustomOpenGLView(Context context) { + private final List<Long> mFrameTimes; + private final Object mLock = new Object(); + private boolean mRenderReady = false; + private FrameDrawer mFrameDrawer = null; + + private float mRenderRatio; + private int mRenderWidth; + private int mRenderHeight; + + public interface FrameDrawer { + public void drawFrame(@NonNull GL10 gl); + } + + public CustomOpenGLView(@NonNull Context context) { super(context); - mRandom = new Random(); mFrameTimes = new ArrayList<Long>(); setEGLContextClientVersion(2); @@ -41,25 +54,35 @@ public class CustomOpenGLView extends GLSurfaceView { setRenderer(new GLSurfaceView.Renderer() { @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { + Log.i(TAG, "SurfaceCreated: " + config); GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f); gl.glClearDepthf(1.0f); - gl.glEnable(GL10.GL_DEPTH_TEST); + gl.glDisable(GL10.GL_DEPTH_TEST); gl.glDepthFunc(GL10.GL_LEQUAL); gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, - GL10.GL_NICEST); } + GL10.GL_NICEST); + synchronized (mLock) { + mRenderReady = true; + mLock.notify(); + } + } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { + Log.i(TAG, "SurfaceChanged: " + width + "x" + height); GLES20.glViewport(0, 0, width, height); + setRenderBounds(width, height); } @Override public void onDrawFrame(GL10 gl) { - GLES20.glClearColor( - mRandom.nextFloat(), mRandom.nextFloat(), mRandom.nextFloat(), 1.0f); + GLES20.glClearColor(0.25f, 0.25f, 0.25f, 1.0f); gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); - synchronized (mFrameTimes) { + synchronized (mLock) { + if (mFrameDrawer != null) { + mFrameDrawer.drawFrame(gl); + } mFrameTimes.add(System.currentTimeMillis()); } } @@ -67,20 +90,38 @@ public class CustomOpenGLView extends GLSurfaceView { setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); } + public void setRenderBounds(int width, int height) { + mRenderWidth = width; + mRenderHeight = height; + mRenderRatio = (float) mRenderWidth / mRenderHeight; + } + + public float getRenderRatio() { + return mRenderRatio; + } + + public int getRenderWidth() { + return mRenderWidth; + } + + public int getRenderHeight() { + return mRenderHeight; + } + /** - * Resets frame times in order to calculate fps for different test pass. + * Resets frame times in order to calculate FPS for the different test pass. */ public void resetFrameTimes() { - synchronized (mFrameTimes) { + synchronized (mLock) { mFrameTimes.clear(); } } /** - * Returns current fps based on collected frame times. + * Returns current FPS based on collected frame times. */ public double getFps() { - synchronized (mFrameTimes) { + synchronized (mLock) { if (mFrameTimes.size() < 2) { return 0.0f; } @@ -88,4 +129,26 @@ public class CustomOpenGLView extends GLSurfaceView { (mFrameTimes.get(mFrameTimes.size() - 1) - mFrameTimes.get(0)); } } + + /** + * Waits for render attached to the view. + */ + public void waitRenderReady() { + synchronized (mLock) { + while (!mRenderReady) { + try { + mLock.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + + /** + * Sets/resets frame drawer. + */ + public void setFrameDrawer(@Nullable FrameDrawer frameDrawer) { + mFrameDrawer = frameDrawer; + } } diff --git a/tests/GamePerformance/src/android/gameperformance/DeviceCallsOpenGLTest.java b/tests/GamePerformance/src/android/gameperformance/DeviceCallsOpenGLTest.java new file mode 100644 index 000000000000..df2ae5cf670a --- /dev/null +++ b/tests/GamePerformance/src/android/gameperformance/DeviceCallsOpenGLTest.java @@ -0,0 +1,60 @@ +/* + * 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.gameperformance; + +import java.util.ArrayList; +import java.util.List; + +import android.annotation.NonNull; + +/** + * Tests that verifies maximum number of device calls to render the geometry to keep FPS close to + * the device refresh rate. This uses trivial one triangle patch that is rendered multiple times. + */ +public class DeviceCallsOpenGLTest extends RenderPatchOpenGLTest { + + public DeviceCallsOpenGLTest(@NonNull GamePerformanceActivity activity) { + super(activity); + } + + @Override + public String getName() { + return "device_calls"; + } + + @Override + public String getUnitName() { + return "calls"; + } + + @Override + public double getUnitScale() { + return 25.0; + } + + @Override + public void initUnits(double deviceCallsD) { + final List<RenderPatchAnimation> renderPatches = new ArrayList<>(); + final RenderPatch renderPatch = new RenderPatch(1 /* triangleCount */, + 0.05f /* dimension */, + RenderPatch.TESSELLATION_BASE); + final int deviceCalls = (int)Math.round(deviceCallsD); + for (int i = 0; i < deviceCalls; ++i) { + renderPatches.add(new RenderPatchAnimation(renderPatch, getView().getRenderRatio())); + } + setRenderPatches(renderPatches); + } +}
\ No newline at end of file diff --git a/tests/GamePerformance/src/android/gameperformance/FillRateOpenGLTest.java b/tests/GamePerformance/src/android/gameperformance/FillRateOpenGLTest.java new file mode 100644 index 000000000000..9b2619372d16 --- /dev/null +++ b/tests/GamePerformance/src/android/gameperformance/FillRateOpenGLTest.java @@ -0,0 +1,93 @@ +/* + * 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.gameperformance; + +import java.util.ArrayList; +import java.util.List; + +import javax.microedition.khronos.opengles.GL; + +import android.annotation.NonNull; +import android.opengl.GLES20; + +/** + * Tests that verifies maximum fill rate per frame can be used to keep FPS close to the device + * refresh rate. It works in two modes, blend disabled and blend enabled. This uses few big simple + * quad patches. + */ +public class FillRateOpenGLTest extends RenderPatchOpenGLTest { + private final float[] BLEND_COLOR = new float[] { 1.0f, 1.0f, 1.0f, 0.2f }; + + private final boolean mTestBlend; + + public FillRateOpenGLTest(@NonNull GamePerformanceActivity activity, boolean testBlend) { + super(activity); + mTestBlend = testBlend; + } + + @Override + public String getName() { + return mTestBlend ? "blend_rate" : "fill_rate"; + } + + @Override + public String getUnitName() { + return "screens"; + } + + @Override + public double getUnitScale() { + return 0.2; + } + + @Override + public void initUnits(double screens) { + final CustomOpenGLView view = getView(); + final int pixelRate = (int)Math.round(screens * view.getHeight() * view.getWidth()); + final int maxPerPath = view.getHeight() * view.getHeight(); + + final int patchCount = (int)(pixelRate + maxPerPath -1) / maxPerPath; + final float patchDimension = + (float)((Math.sqrt(2.0f) * pixelRate / patchCount) / maxPerPath); + + final List<RenderPatchAnimation> renderPatches = new ArrayList<>(); + final RenderPatch renderPatch = new RenderPatch(2 /* triangleCount for quad */, + patchDimension, + RenderPatch.TESSELLATION_BASE); + for (int i = 0; i < patchCount; ++i) { + renderPatches.add(new RenderPatchAnimation(renderPatch, getView().getRenderRatio())); + } + setRenderPatches(renderPatches); + } + + @Override + public float[] getColor() { + return BLEND_COLOR; + } + + @Override + public void onBeforeDraw(GL gl) { + if (!mTestBlend) { + return; + } + + // Enable blend if needed. + GLES20.glEnable(GLES20.GL_BLEND); + OpenGLUtils.checkGlError("disableBlend"); + GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); + OpenGLUtils.checkGlError("blendFunction"); + } +}
\ No newline at end of file diff --git a/tests/GamePerformance/src/android/gameperformance/GamePerformanceActivity.java b/tests/GamePerformance/src/android/gameperformance/GamePerformanceActivity.java index b0e6196b53d7..dc745f17e698 100644 --- a/tests/GamePerformance/src/android/gameperformance/GamePerformanceActivity.java +++ b/tests/GamePerformance/src/android/gameperformance/GamePerformanceActivity.java @@ -25,14 +25,32 @@ import android.view.WindowManager; import android.widget.RelativeLayout; /** - * Minimal activity that holds SurfaceView or GLSurfaceView. - * call attachSurfaceView or attachOpenGLView to switch views. + * Minimal activity that holds different types of views. + * call attachSurfaceView, attachOpenGLView or attachControlView to switch + * the view. */ public class GamePerformanceActivity extends Activity { private CustomSurfaceView mSurfaceView = null; private CustomOpenGLView mOpenGLView = null; + private CustomControlView mControlView = null; + private RelativeLayout mRootLayout; + private void detachAllViews() { + if (mOpenGLView != null) { + mRootLayout.removeView(mOpenGLView); + mOpenGLView = null; + } + if (mSurfaceView != null) { + mRootLayout.removeView(mSurfaceView); + mSurfaceView = null; + } + if (mControlView != null) { + mRootLayout.removeView(mControlView); + mControlView = null; + } + } + public void attachSurfaceView() throws InterruptedException { synchronized (mRootLayout) { if (mSurfaceView != null) { @@ -42,10 +60,7 @@ public class GamePerformanceActivity extends Activity { runOnUiThread(new Runnable() { @Override public void run() { - if (mOpenGLView != null) { - mRootLayout.removeView(mOpenGLView); - mOpenGLView = null; - } + detachAllViews(); mSurfaceView = new CustomSurfaceView(GamePerformanceActivity.this); mRootLayout.addView(mSurfaceView); latch.countDown(); @@ -65,10 +80,7 @@ public class GamePerformanceActivity extends Activity { runOnUiThread(new Runnable() { @Override public void run() { - if (mSurfaceView != null) { - mRootLayout.removeView(mSurfaceView); - mSurfaceView = null; - } + detachAllViews(); mOpenGLView = new CustomOpenGLView(GamePerformanceActivity.this); mRootLayout.addView(mOpenGLView); latch.countDown(); @@ -78,6 +90,40 @@ public class GamePerformanceActivity extends Activity { } } + public void attachControlView() throws InterruptedException { + synchronized (mRootLayout) { + if (mControlView != null) { + return; + } + final CountDownLatch latch = new CountDownLatch(1); + runOnUiThread(new Runnable() { + @Override + public void run() { + detachAllViews(); + mControlView = new CustomControlView(GamePerformanceActivity.this); + mRootLayout.addView(mControlView); + latch.countDown(); + } + }); + latch.await(); + } + } + + + public CustomOpenGLView getOpenGLView() { + if (mOpenGLView == null) { + throw new RuntimeException("OpenGL view is not attached"); + } + return mOpenGLView; + } + + public CustomControlView getControlView() { + if (mControlView == null) { + throw new RuntimeException("Control view is not attached"); + } + return mControlView; + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -105,6 +151,8 @@ public class GamePerformanceActivity extends Activity { mSurfaceView.resetFrameTimes(); } else if (mOpenGLView != null) { mOpenGLView.resetFrameTimes(); + } else if (mControlView != null) { + mControlView.resetFrameTimes(); } else { throw new IllegalStateException("Nothing attached"); } @@ -115,6 +163,8 @@ public class GamePerformanceActivity extends Activity { return mSurfaceView.getFps(); } else if (mOpenGLView != null) { return mOpenGLView.getFps(); + } else if (mControlView != null) { + return mControlView.getFps(); } else { throw new IllegalStateException("Nothing attached"); } diff --git a/tests/GamePerformance/src/android/gameperformance/GamePerformanceTest.java b/tests/GamePerformance/src/android/gameperformance/GamePerformanceTest.java index e5de7d75886e..d6e2861c03a7 100644 --- a/tests/GamePerformance/src/android/gameperformance/GamePerformanceTest.java +++ b/tests/GamePerformance/src/android/gameperformance/GamePerformanceTest.java @@ -17,14 +17,18 @@ package android.gameperformance; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; +import android.annotation.NonNull; import android.app.Activity; import android.content.Context; import android.graphics.PixelFormat; import android.os.Build; import android.os.Bundle; +import android.os.Debug; import android.os.Trace; import android.test.ActivityInstrumentationTestCase2; import android.test.suitebuilder.annotation.SmallTest; @@ -84,4 +88,50 @@ public class GamePerformanceTest extends getInstrumentation().sendStatus(Activity.RESULT_OK, status); } + + @SmallTest + public void testPerformanceMetricsWithoutExtraLoad() throws IOException, InterruptedException { + final Bundle status = runPerformanceTests("no_extra_load_"); + getInstrumentation().sendStatus(Activity.RESULT_OK, status); + } + + @SmallTest + public void testPerformanceMetricsWithExtraLoad() throws IOException, InterruptedException { + // Start CPU ballast threads first. + CPULoadThread[] cpuLoadThreads = new CPULoadThread[2]; + for (int i = 0; i < cpuLoadThreads.length; ++i) { + cpuLoadThreads[i] = new CPULoadThread(); + cpuLoadThreads[i].start(); + } + + final Bundle status = runPerformanceTests("extra_load_"); + + for (int i = 0; i < cpuLoadThreads.length; ++i) { + cpuLoadThreads[i].issueStopRequest(); + cpuLoadThreads[i].join(); + } + + getInstrumentation().sendStatus(Activity.RESULT_OK, status); + } + + @NonNull + private Bundle runPerformanceTests(@NonNull String prefix) { + final Bundle status = new Bundle(); + + final GamePerformanceActivity activity = getActivity(); + + final List<BaseTest> tests = new ArrayList<>(); + tests.add(new TriangleCountOpenGLTest(activity)); + tests.add(new FillRateOpenGLTest(activity, false /* testBlend */)); + tests.add(new FillRateOpenGLTest(activity, true /* testBlend */)); + tests.add(new DeviceCallsOpenGLTest(activity)); + tests.add(new ControlsTest(activity)); + + for (BaseTest test : tests) { + final double result = test.run(); + status.putDouble(prefix + test.getName(), result); + } + + return status; + } } diff --git a/tests/GamePerformance/src/android/gameperformance/OpenGLTest.java b/tests/GamePerformance/src/android/gameperformance/OpenGLTest.java new file mode 100644 index 000000000000..1d3f95cdd5b8 --- /dev/null +++ b/tests/GamePerformance/src/android/gameperformance/OpenGLTest.java @@ -0,0 +1,76 @@ +/* + * 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.gameperformance; + +import javax.microedition.khronos.opengles.GL; +import javax.microedition.khronos.opengles.GL10; + +import android.annotation.NonNull; +import android.gameperformance.CustomOpenGLView.FrameDrawer; + +/** + * Base class for all OpenGL based tests. + */ +public abstract class OpenGLTest extends BaseTest { + public OpenGLTest(@NonNull GamePerformanceActivity activity) { + super(activity); + } + + @NonNull + public CustomOpenGLView getView() { + return getActivity().getOpenGLView(); + } + + // Performs test drawing. + protected abstract void draw(GL gl); + + // Initializes the test on first draw call. + private class ParamFrameDrawer implements FrameDrawer { + private final double mUnitCount; + private boolean mInited; + + public ParamFrameDrawer(double unitCount) { + mUnitCount = unitCount; + mInited = false; + } + + @Override + public void drawFrame(GL10 gl) { + if (!mInited) { + initUnits(mUnitCount); + mInited = true; + } + draw(gl); + } + } + + @Override + protected void initProbePass(int probe) { + try { + getActivity().attachOpenGLView(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + getView().waitRenderReady(); + getView().setFrameDrawer(new ParamFrameDrawer(probe * getUnitScale())); + } + + @Override + protected void freeProbePass() { + getView().setFrameDrawer(null); + } +}
\ No newline at end of file diff --git a/tests/GamePerformance/src/android/gameperformance/OpenGLUtils.java b/tests/GamePerformance/src/android/gameperformance/OpenGLUtils.java new file mode 100644 index 000000000000..4f98c52b4b39 --- /dev/null +++ b/tests/GamePerformance/src/android/gameperformance/OpenGLUtils.java @@ -0,0 +1,92 @@ +/* + * 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.gameperformance; + +import android.annotation.NonNull; +import android.content.Context; +import android.graphics.BitmapFactory; +import android.opengl.GLES20; +import android.opengl.GLUtils; +import android.util.Log; + +/** + * Helper class for OpenGL. + */ +public class OpenGLUtils { + private final static String TAG = "OpenGLUtils"; + + public static void checkGlError(String glOperation) { + final int error = GLES20.glGetError(); + if (error == GLES20.GL_NO_ERROR) { + return; + } + final String errorMessage = glOperation + ": glError " + error; + Log.e(TAG, errorMessage); + } + + public static int loadShader(int type, String shaderCode) { + final int shader = GLES20.glCreateShader(type); + checkGlError("createShader"); + + GLES20.glShaderSource(shader, shaderCode); + checkGlError("shaderSource"); + GLES20.glCompileShader(shader); + checkGlError("shaderCompile"); + + return shader; + } + + public static int createProgram(@NonNull String vertexShaderCode, + @NonNull String fragmentShaderCode) { + final int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode); + final int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode); + + final int program = GLES20.glCreateProgram(); + checkGlError("createProgram"); + GLES20.glAttachShader(program, vertexShader); + checkGlError("attachVertexShader"); + GLES20.glAttachShader(program, fragmentShader); + checkGlError("attachFragmentShader"); + GLES20.glLinkProgram(program); + checkGlError("linkProgram"); + + return program; + } + + public static int createTexture(@NonNull Context context, int resource) { + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inScaled = false; + + final int[] textureHandle = new int[1]; + GLES20.glGenTextures(1, textureHandle, 0); + OpenGLUtils.checkGlError("GenTextures"); + final int handle = textureHandle[0]; + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, handle); + GLES20.glTexParameteri( + GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameteri( + GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLUtils.texImage2D( + GLES20.GL_TEXTURE_2D, + 0, + BitmapFactory.decodeResource( + context.getResources(), resource, options), + 0); + + return handle; + } +}
\ No newline at end of file diff --git a/tests/GamePerformance/src/android/gameperformance/RenderPatch.java b/tests/GamePerformance/src/android/gameperformance/RenderPatch.java new file mode 100644 index 000000000000..2e69a61475db --- /dev/null +++ b/tests/GamePerformance/src/android/gameperformance/RenderPatch.java @@ -0,0 +1,150 @@ +/* + * 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.gameperformance; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Helper class that generates patch to render. Patch is a regular polygon with the center in 0. + * Regular polygon fits in circle with requested radius. + */ +public class RenderPatch { + public static final int FLOAT_SIZE = 4; + public static final int SHORT_SIZE = 2; + public static final int VERTEX_COORD_COUNT = 3; + public static final int VERTEX_STRIDE = VERTEX_COORD_COUNT * FLOAT_SIZE; + public static final int TEXTURE_COORD_COUNT = 2; + public static final int TEXTURE_STRIDE = TEXTURE_COORD_COUNT * FLOAT_SIZE; + + // Tessellation is done using points on circle. + public static final int TESSELLATION_BASE = 0; + // Tesselation is done using extra point in 0. + public static final int TESSELLATION_TO_CENTER = 1; + + // Radius of circle that fits polygon. + private final float mDimension; + + private final ByteBuffer mVertexBuffer; + private final ByteBuffer mTextureBuffer; + private final ByteBuffer mIndexBuffer; + + public RenderPatch(int triangleCount, float dimension, int tessellation) { + mDimension = dimension; + + int pointCount; + int externalPointCount; + + if (triangleCount < 1) { + throw new IllegalArgumentException("Too few triangles to perform tessellation"); + } + + switch (tessellation) { + case TESSELLATION_BASE: + externalPointCount = triangleCount + 2; + pointCount = externalPointCount; + break; + case TESSELLATION_TO_CENTER: + if (triangleCount < 3) { + throw new IllegalArgumentException( + "Too few triangles to perform tessellation to center"); + } + externalPointCount = triangleCount; + pointCount = triangleCount + 1; + break; + default: + throw new IllegalArgumentException("Wrong tesselation requested"); + } + + if (pointCount > Short.MAX_VALUE) { + throw new IllegalArgumentException("Number of requested triangles is too big"); + } + + mVertexBuffer = ByteBuffer.allocateDirect(pointCount * VERTEX_STRIDE); + mVertexBuffer.order(ByteOrder.nativeOrder()); + + mTextureBuffer = ByteBuffer.allocateDirect(pointCount * TEXTURE_STRIDE); + mTextureBuffer.order(ByteOrder.nativeOrder()); + + for (int i = 0; i < externalPointCount; ++i) { + // Use 45 degree rotation to make quad aligned along axises in case + // triangleCount is four. + final double angle = Math.PI * 0.25 + (Math.PI * 2.0 * i) / (externalPointCount); + // Positions + mVertexBuffer.putFloat((float) (dimension * Math.sin(angle))); + mVertexBuffer.putFloat((float) (dimension * Math.cos(angle))); + mVertexBuffer.putFloat(0.0f); + // Texture coordinates. + mTextureBuffer.putFloat((float) (0.5 + 0.5 * Math.sin(angle))); + mTextureBuffer.putFloat((float) (0.5 - 0.5 * Math.cos(angle))); + } + + if (tessellation == TESSELLATION_TO_CENTER) { + // Add center point. + mVertexBuffer.putFloat(0.0f); + mVertexBuffer.putFloat(0.0f); + mVertexBuffer.putFloat(0.0f); + mTextureBuffer.putFloat(0.5f); + mTextureBuffer.putFloat(0.5f); + } + + mIndexBuffer = + ByteBuffer.allocateDirect( + triangleCount * 3 /* indices per triangle */ * SHORT_SIZE); + mIndexBuffer.order(ByteOrder.nativeOrder()); + + switch (tessellation) { + case TESSELLATION_BASE: + for (int i = 0; i < triangleCount; ++i) { + mIndexBuffer.putShort((short) 0); + mIndexBuffer.putShort((short) (i + 1)); + mIndexBuffer.putShort((short) (i + 2)); + } + break; + case TESSELLATION_TO_CENTER: + for (int i = 0; i < triangleCount; ++i) { + mIndexBuffer.putShort((short)i); + mIndexBuffer.putShort((short)((i + 1) % externalPointCount)); + mIndexBuffer.putShort((short)externalPointCount); + } + break; + } + + if (mVertexBuffer.remaining() != 0 || mTextureBuffer.remaining() != 0 || mIndexBuffer.remaining() != 0) { + throw new RuntimeException("Failed to fill buffers"); + } + + mVertexBuffer.position(0); + mTextureBuffer.position(0); + mIndexBuffer.position(0); + } + + public float getDimension() { + return mDimension; + } + + public ByteBuffer getVertexBuffer() { + return mVertexBuffer; + } + + public ByteBuffer getTextureBuffer() { + return mTextureBuffer; + } + + public ByteBuffer getIndexBuffer() { + return mIndexBuffer; + } +}
\ No newline at end of file diff --git a/tests/GamePerformance/src/android/gameperformance/RenderPatchAnimation.java b/tests/GamePerformance/src/android/gameperformance/RenderPatchAnimation.java new file mode 100644 index 000000000000..7dcdb00e1014 --- /dev/null +++ b/tests/GamePerformance/src/android/gameperformance/RenderPatchAnimation.java @@ -0,0 +1,101 @@ +/* + * 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.gameperformance; + +import java.util.Random; + +import android.annotation.NonNull; +import android.opengl.Matrix; + +/** + * Class that performs bouncing animation for RenderPatch on the screen. + */ +public class RenderPatchAnimation { + private final static Random RANDOM = new Random(); + + private final RenderPatch mRenderPatch; + // Bounds of animation + private final float mAvailableX; + private final float mAvailableY; + + // Crurrent position. + private float mPosX; + private float mPosY; + // Direction of movement. + private float mDirX; + private float mDirY; + + private float[] mMatrix; + + public RenderPatchAnimation(@NonNull RenderPatch renderPatch, float ratio) { + mRenderPatch = renderPatch; + + mAvailableX = ratio - mRenderPatch.getDimension(); + mAvailableY = 1.0f - mRenderPatch.getDimension(); + + mPosX = 2.0f * mAvailableX * RANDOM.nextFloat() - mAvailableX; + mPosY = 2.0f * mAvailableY * RANDOM.nextFloat() - mAvailableY; + mMatrix = new float[16]; + + // Evenly distributed in cycle, normalized. + while (true) { + mDirX = 2.0f * RANDOM.nextFloat() - 1.0f; + mDirY = mRenderPatch.getDimension() < 1.0f ? 2.0f * RANDOM.nextFloat() - 1.0f : 0.0f; + + final float length = (float)Math.sqrt(mDirX * mDirX + mDirY * mDirY); + if (length <= 1.0f && length > 0.0f) { + mDirX /= length; + mDirY /= length; + break; + } + } + } + + @NonNull + public RenderPatch getRenderPatch() { + return mRenderPatch; + } + + /** + * Performs the next update. t specifies the distance to travel along the direction. This checks + * if patch goes out of screen and invert axis direction if needed. + */ + public void update(float t) { + mPosX += mDirX * t; + mPosY += mDirY * t; + if (mPosX < -mAvailableX) { + mDirX = Math.abs(mDirX); + } else if (mPosX > mAvailableX) { + mDirX = -Math.abs(mDirX); + } + if (mPosY < -mAvailableY) { + mDirY = Math.abs(mDirY); + } else if (mPosY > mAvailableY) { + mDirY = -Math.abs(mDirY); + } + } + + /** + * Returns Model/View/Projection transform for the patch. + */ + public float[] getTransform(@NonNull float[] vpMatrix) { + Matrix.setIdentityM(mMatrix, 0); + mMatrix[12] = mPosX; + mMatrix[13] = mPosY; + Matrix.multiplyMM(mMatrix, 0, vpMatrix, 0, mMatrix, 0); + return mMatrix; + } +}
\ No newline at end of file diff --git a/tests/GamePerformance/src/android/gameperformance/RenderPatchOpenGLTest.java b/tests/GamePerformance/src/android/gameperformance/RenderPatchOpenGLTest.java new file mode 100644 index 000000000000..7492cc034234 --- /dev/null +++ b/tests/GamePerformance/src/android/gameperformance/RenderPatchOpenGLTest.java @@ -0,0 +1,188 @@ +/* + * 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.gameperformance; + +import java.util.List; + +import javax.microedition.khronos.opengles.GL; + +import android.annotation.NonNull; +import android.content.Context; +import android.opengl.GLES20; +import android.opengl.Matrix; + +/** + * Base class for all OpenGL based tests that use RenderPatch as a base. + */ +public abstract class RenderPatchOpenGLTest extends OpenGLTest { + private final float[] COLOR = new float[] { 1.0f, 1.0f, 1.0f, 1.0f }; + + private final String VERTEX_SHADER = + "uniform mat4 uMVPMatrix;" + + "attribute vec4 vPosition;" + + "attribute vec2 vTexture;" + + "varying vec2 vTex;" + + "void main() {" + + " vTex = vTexture;" + + " gl_Position = uMVPMatrix * vPosition;" + + "}"; + + private final String FRAGMENT_SHADER = + "precision mediump float;" + + "uniform sampler2D uTexture;" + + "uniform vec4 uColor;" + + "varying vec2 vTex;" + + "void main() {" + + " vec4 color = texture2D(uTexture, vTex);" + + " gl_FragColor = uColor * color;" + + "}"; + + private List<RenderPatchAnimation> mRenderPatches; + + private int mProgram = -1; + private int mMVPMatrixHandle; + private int mTextureHandle; + private int mPositionHandle; + private int mColorHandle; + private int mTextureCoordHandle; + + private final float[] mVPMatrix = new float[16]; + + public RenderPatchOpenGLTest(@NonNull GamePerformanceActivity activity) { + super(activity); + } + + protected void setRenderPatches(@NonNull List<RenderPatchAnimation> renderPatches) { + mRenderPatches = renderPatches; + } + + private void ensureInited() { + if (mProgram >= 0) { + return; + } + + mProgram = OpenGLUtils.createProgram(VERTEX_SHADER, FRAGMENT_SHADER); + + // get handle to fragment shader's uColor member + GLES20.glUseProgram(mProgram); + OpenGLUtils.checkGlError("useProgram"); + + // get handle to shape's transformation matrix + mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix"); + OpenGLUtils.checkGlError("get uMVPMatrix"); + + mTextureHandle = GLES20.glGetUniformLocation(mProgram, "uTexture"); + OpenGLUtils.checkGlError("uTexture"); + // get handle to vertex shader's vPosition member + mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition"); + OpenGLUtils.checkGlError("vPosition"); + mTextureCoordHandle = GLES20.glGetAttribLocation(mProgram, "vTexture"); + OpenGLUtils.checkGlError("vTexture"); + mColorHandle = GLES20.glGetUniformLocation(mProgram, "uColor"); + OpenGLUtils.checkGlError("uColor"); + + mTextureHandle = OpenGLUtils.createTexture(getContext(), R.drawable.logo); + + final float[] projectionMatrix = new float[16]; + final float[] viewMatrix = new float[16]; + + final float ratio = getView().getRenderRatio(); + Matrix.orthoM(projectionMatrix, 0, -ratio, ratio, -1, 1, -1, 1); + Matrix.setLookAtM(viewMatrix, 0, 0, 0, -0.5f, 0f, 0f, 0f, 0f, 1.0f, 0.0f); + Matrix.multiplyMM(mVPMatrix, 0, projectionMatrix, 0, viewMatrix, 0); + } + + /** + * Returns global color for patch. + */ + public float[] getColor() { + return COLOR; + } + + /** + * Extra setup for particular tests. + */ + public void onBeforeDraw(GL gl) { + } + + @Override + public void draw(GL gl) { + ensureInited(); + + GLES20.glUseProgram(mProgram); + OpenGLUtils.checkGlError("useProgram"); + + GLES20.glDisable(GLES20.GL_BLEND); + OpenGLUtils.checkGlError("disableBlend"); + + GLES20.glEnableVertexAttribArray(mPositionHandle); + OpenGLUtils.checkGlError("enableVertexAttributes"); + + GLES20.glEnableVertexAttribArray(mTextureCoordHandle); + OpenGLUtils.checkGlError("enableTexturesAttributes"); + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureHandle); + OpenGLUtils.checkGlError("setTexture"); + + GLES20.glUniform4fv(mColorHandle, 1, getColor(), 0); + OpenGLUtils.checkGlError("setColor"); + + onBeforeDraw(gl); + + for (final RenderPatchAnimation renderPatchAnimation : mRenderPatches) { + + renderPatchAnimation.update(0.01f); + GLES20.glUniformMatrix4fv(mMVPMatrixHandle, + 1, + false, + renderPatchAnimation.getTransform(mVPMatrix), + 0); + OpenGLUtils.checkGlError("setTransform"); + + GLES20.glVertexAttribPointer( + mPositionHandle, + RenderPatch.VERTEX_COORD_COUNT, + GLES20.GL_FLOAT, + false /* normalized */, + RenderPatch.VERTEX_STRIDE, + renderPatchAnimation.getRenderPatch().getVertexBuffer()); + OpenGLUtils.checkGlError("setVertexAttribute"); + + GLES20.glVertexAttribPointer( + mTextureCoordHandle, + RenderPatch.TEXTURE_COORD_COUNT, + GLES20.GL_FLOAT, + false /* normalized */, + RenderPatch.TEXTURE_STRIDE, + renderPatchAnimation.getRenderPatch().getTextureBuffer()); + OpenGLUtils.checkGlError("setTextureAttribute"); + + // Draw the patch. + final int indicesCount = + renderPatchAnimation.getRenderPatch().getIndexBuffer().capacity() / + RenderPatch.SHORT_SIZE; + GLES20.glDrawElements( + GLES20.GL_TRIANGLES, + indicesCount, + GLES20.GL_UNSIGNED_SHORT, + renderPatchAnimation.getRenderPatch().getIndexBuffer()); + OpenGLUtils.checkGlError("drawPatch"); + } + + GLES20.glDisableVertexAttribArray(mPositionHandle); + GLES20.glDisableVertexAttribArray(mTextureCoordHandle); + } +}
\ No newline at end of file diff --git a/tests/GamePerformance/src/android/gameperformance/TriangleCountOpenGLTest.java b/tests/GamePerformance/src/android/gameperformance/TriangleCountOpenGLTest.java new file mode 100644 index 000000000000..593f37bf9128 --- /dev/null +++ b/tests/GamePerformance/src/android/gameperformance/TriangleCountOpenGLTest.java @@ -0,0 +1,66 @@ +/* + * 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.gameperformance; + +import java.util.ArrayList; +import java.util.List; + +import android.annotation.NonNull; + +/** + * Test that measures maximum amount of triangles can be rasterized keeping FPS close to the device + * refresh rate. It is has very few devices call and each call contains big amount of triangles. + * Total filling area is around one screen. + */ +public class TriangleCountOpenGLTest extends RenderPatchOpenGLTest { + // Based on index buffer of short values. + private final static int MAX_TRIANGLES_IN_PATCH = 32000; + + public TriangleCountOpenGLTest(@NonNull GamePerformanceActivity activity) { + super(activity); + } + + @Override + public String getName() { + return "triangle_count"; + } + + @Override + public String getUnitName() { + return "ktriangles"; + } + + @Override + public double getUnitScale() { + return 2.0; + } + + @Override + public void initUnits(double trianlgeCountD) { + final int triangleCount = (int)Math.round(trianlgeCountD * 1000.0); + final List<RenderPatchAnimation> renderPatches = new ArrayList<>(); + final int patchCount = + (triangleCount + MAX_TRIANGLES_IN_PATCH - 1) / MAX_TRIANGLES_IN_PATCH; + final int patchTriangleCount = triangleCount / patchCount; + for (int i = 0; i < patchCount; ++i) { + final RenderPatch renderPatch = new RenderPatch(patchTriangleCount, + 0.5f /* dimension */, + RenderPatch.TESSELLATION_TO_CENTER); + renderPatches.add(new RenderPatchAnimation(renderPatch, getView().getRenderRatio())); + } + setRenderPatches(renderPatches); + } +}
\ No newline at end of file diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/AlphaLayersActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/AlphaLayersActivity.java index 1a68a93eed19..37661828da22 100644 --- a/tests/HwAccelerationTest/src/com/android/test/hwui/AlphaLayersActivity.java +++ b/tests/HwAccelerationTest/src/com/android/test/hwui/AlphaLayersActivity.java @@ -50,7 +50,7 @@ public class AlphaLayersActivity extends Activity { setContentView(container); } - + @SuppressWarnings({"UnusedDeclaration"}) static int dipToPx(Context c, int dip) { return (int) (c.getResources().getDisplayMetrics().density * dip + 0.5f); @@ -86,30 +86,24 @@ public class AlphaLayersActivity extends Activity { canvas.save(); canvas.clipRect(20.0f, 0.0f, 40.0f, 20.0f); Log.d(LOG_TAG, "clipRect = " + canvas.getClipBounds()); - Log.d(LOG_TAG, "rejected = " + canvas.quickReject(100.0f, 100.0f, 110.0f, 110.0f, - Canvas.EdgeType.BW)); - Log.d(LOG_TAG, "rejected = " + canvas.quickReject(25.0f, 5.0f, 30.0f, 10.0f, - Canvas.EdgeType.BW)); + Log.d(LOG_TAG, "rejected = " + canvas.quickReject(100.0f, 100.0f, 110.0f, 110.0f)); + Log.d(LOG_TAG, "rejected = " + canvas.quickReject(25.0f, 5.0f, 30.0f, 10.0f)); canvas.restore(); - + canvas.save(); canvas.scale(2.0f, 2.0f); canvas.clipRect(20.0f, 0.0f, 40.0f, 20.0f); Log.d(LOG_TAG, "clipRect = " + canvas.getClipBounds()); - Log.d(LOG_TAG, "rejected = " + canvas.quickReject(50.0f, 50.0f, 60.0f, 60.0f, - Canvas.EdgeType.BW)); - Log.d(LOG_TAG, "rejected = " + canvas.quickReject(25.0f, 5.0f, 30.0f, 10.0f, - Canvas.EdgeType.BW)); + Log.d(LOG_TAG, "rejected = " + canvas.quickReject(50.0f, 50.0f, 60.0f, 60.0f)); + Log.d(LOG_TAG, "rejected = " + canvas.quickReject(25.0f, 5.0f, 30.0f, 10.0f)); canvas.restore(); canvas.save(); canvas.translate(20.0f, 20.0f); canvas.clipRect(20.0f, 0.0f, 40.0f, 20.0f); Log.d(LOG_TAG, "clipRect = " + canvas.getClipBounds()); - Log.d(LOG_TAG, "rejected = " + canvas.quickReject(80.0f, 80.0f, 90.0f, 90.0f, - Canvas.EdgeType.BW)); - Log.d(LOG_TAG, "rejected = " + canvas.quickReject(25.0f, 5.0f, 30.0f, 10.0f, - Canvas.EdgeType.BW)); + Log.d(LOG_TAG, "rejected = " + canvas.quickReject(80.0f, 80.0f, 90.0f, 90.0f)); + Log.d(LOG_TAG, "rejected = " + canvas.quickReject(25.0f, 5.0f, 30.0f, 10.0f)); canvas.restore(); canvas.save(); diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/ColorFiltersMutateActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/ColorFiltersMutateActivity.java index 0787d823756c..51bae3af3e9c 100644 --- a/tests/HwAccelerationTest/src/com/android/test/hwui/ColorFiltersMutateActivity.java +++ b/tests/HwAccelerationTest/src/com/android/test/hwui/ColorFiltersMutateActivity.java @@ -29,9 +29,13 @@ import android.graphics.LightingColorFilter; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; +import android.graphics.RuntimeShader; import android.os.Bundle; import android.view.View; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + @SuppressWarnings({"UnusedDeclaration"}) public class ColorFiltersMutateActivity extends Activity { @Override @@ -47,12 +51,21 @@ public class ColorFiltersMutateActivity extends Activity { private final Paint mColorMatrixPaint; private final Paint mLightingPaint; private final Paint mBlendPaint; + private final Paint mShaderPaint; private float mSaturation = 0.0f; private int mLightAdd = 0; private int mLightMul = 0; private int mPorterDuffColor = 0; + static final String sSkSL = + "uniform float param1;\n" + + "void main(float x, float y, inout half4 color) {\n" + + "color = half4(color.r, half(param1), color.b, 1.0);\n" + + "}\n"; + + private byte[] mUniforms = new byte[4]; + BitmapsView(Context c) { super(c); @@ -70,6 +83,10 @@ public class ColorFiltersMutateActivity extends Activity { mBlendPaint = new Paint(); mBlendPaint.setColorFilter(new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_OVER)); + mShaderPaint = new Paint(); + mShaderPaint.setShader(new RuntimeShader(sSkSL, mUniforms, true)); + setShaderParam1(0.0f); + ObjectAnimator sat = ObjectAnimator.ofFloat(this, "saturation", 1.0f); sat.setDuration(1000); sat.setRepeatCount(ObjectAnimator.INFINITE); @@ -96,6 +113,12 @@ public class ColorFiltersMutateActivity extends Activity { color.setRepeatCount(ObjectAnimator.INFINITE); color.setRepeatMode(ObjectAnimator.REVERSE); color.start(); + + ObjectAnimator shaderUniform = ObjectAnimator.ofFloat(this, "shaderParam1", 1.0f); + shaderUniform.setDuration(1000); + shaderUniform.setRepeatCount(ObjectAnimator.INFINITE); + shaderUniform.setRepeatMode(ObjectAnimator.REVERSE); + shaderUniform.start(); } public int getPorterDuffColor() { @@ -148,6 +171,23 @@ public class ColorFiltersMutateActivity extends Activity { return mSaturation; } + public void setShaderParam1(float value) { + RuntimeShader shader = (RuntimeShader) mShaderPaint.getShader(); + ByteBuffer buffer = ByteBuffer.wrap(mUniforms); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.putFloat(value); + shader.updateUniforms(mUniforms); + invalidate(); + } + + // If either valueFrom or valueTo is null, then a getter function will also be derived + // and called by the animator class. + public float getShaderParam1() { + ByteBuffer buffer = ByteBuffer.wrap(mUniforms); + buffer.order(ByteOrder.LITTLE_ENDIAN); + return buffer.getFloat(); + } + @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); @@ -163,6 +203,10 @@ public class ColorFiltersMutateActivity extends Activity { canvas.translate(0.0f, 50.0f + mBitmap1.getHeight()); canvas.drawBitmap(mBitmap1, 0.0f, 0.0f, mBlendPaint); + + canvas.translate(0.0f, 50.0f + mBitmap1.getHeight()); + canvas.drawRect(0.0f, 0.0f, mBitmap1.getWidth(), mBitmap1.getHeight(), + mShaderPaint); canvas.restore(); canvas.save(); @@ -174,6 +218,10 @@ public class ColorFiltersMutateActivity extends Activity { canvas.translate(0.0f, 50.0f + mBitmap2.getHeight()); canvas.drawBitmap(mBitmap2, 0.0f, 0.0f, mBlendPaint); + + canvas.translate(0.0f, 50.0f + mBitmap2.getHeight()); + canvas.drawRoundRect(0.0f, 0.0f, mBitmap2.getWidth(), mBitmap2.getHeight(), 20, 20, + mShaderPaint); canvas.restore(); } } diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/TextureViewActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/TextureViewActivity.java index 0f4c66817a05..6d8c43c00acf 100644 --- a/tests/HwAccelerationTest/src/com/android/test/hwui/TextureViewActivity.java +++ b/tests/HwAccelerationTest/src/com/android/test/hwui/TextureViewActivity.java @@ -122,7 +122,7 @@ public class TextureViewActivity extends Activity implements TextureView.Surface if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) break; } - int rotation = getWindowManager().getDefaultDisplay().getRotation(); + int rotation = getDisplay().getRotation(); int degrees = 0; switch (rotation) { diff --git a/tests/Internal/src/android/app/WallpaperColorsTest.java b/tests/Internal/src/android/app/WallpaperColorsTest.java index 65ff6eb1ba04..e9bac717daa1 100644 --- a/tests/Internal/src/android/app/WallpaperColorsTest.java +++ b/tests/Internal/src/android/app/WallpaperColorsTest.java @@ -87,6 +87,14 @@ public class WallpaperColorsTest { + "HINT_FROM_BITMAP.", fromBitmap); } + @Test + public void darkMainColorSupportsDarkTheme() { + final Color color = Color.valueOf(Color.BLACK); + WallpaperColors colors = new WallpaperColors(color, null, null); + Assert.assertTrue("Dark theme should be supported by dark main colors.", + (colors.getColorHints() & WallpaperColors.HINT_SUPPORTS_DARK_THEME) != 0); + } + /** * WallpaperColors should not recycle bitmaps that it didn't create. */ diff --git a/tests/Internal/src/android/service/wallpaper/WallpaperServiceTest.java b/tests/Internal/src/android/service/wallpaper/WallpaperServiceTest.java index 592aa3ac4a6b..153ca79e346b 100644 --- a/tests/Internal/src/android/service/wallpaper/WallpaperServiceTest.java +++ b/tests/Internal/src/android/service/wallpaper/WallpaperServiceTest.java @@ -58,4 +58,31 @@ public class WallpaperServiceTest { ambientModeChangedCount[0], 2); } + @Test + public void testDeliversZoomChanged() { + int[] zoomChangedCount = {0}; + WallpaperService service = new WallpaperService() { + @Override + public Engine onCreateEngine() { + return new Engine() { + @Override + public void onZoomChanged(float zoom) { + super.onZoomChanged(zoom); + zoomChangedCount[0]++; + } + }; + } + }; + WallpaperService.Engine engine = service.onCreateEngine(); + engine.setCreated(true); + + engine.setZoom(.5f); + assertEquals("engine scale was not updated", .5f, engine.getZoom(), .001f); + assertEquals("onZoomChanged should have been called", 1, zoomChangedCount[0]); + + engine.setZoom(0); + assertEquals("engine scale was not updated", 0, engine.getZoom(), .001f); + assertEquals("onAmbientModeChanged should have been called", 2, zoomChangedCount[0]); + } + } diff --git a/tests/JobSchedulerPerfTests/Android.bp b/tests/JobSchedulerPerfTests/Android.bp new file mode 100644 index 000000000000..2ae8c33b60a7 --- /dev/null +++ b/tests/JobSchedulerPerfTests/Android.bp @@ -0,0 +1,26 @@ +// 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 { + name: "JobSchedulerPerfTests", + srcs: ["src/**/*.java"], + static_libs: [ + "androidx.test.rules", + "apct-perftests-utils", + "services", + "service-jobscheduler", + ], + platform_apis: true, + certificate: "platform", +} diff --git a/tests/JobSchedulerPerfTests/AndroidManifest.xml b/tests/JobSchedulerPerfTests/AndroidManifest.xml new file mode 100644 index 000000000000..39e751ca2a0c --- /dev/null +++ b/tests/JobSchedulerPerfTests/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.frameworks.perftests.job"> + <uses-sdk + android:minSdkVersion="21" /> + + <application> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.frameworks.perftests.job"/> +</manifest> diff --git a/tests/JobSchedulerPerfTests/AndroidTest.xml b/tests/JobSchedulerPerfTests/AndroidTest.xml new file mode 100644 index 000000000000..ca4b6c83f788 --- /dev/null +++ b/tests/JobSchedulerPerfTests/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 JobScheduler Performance Tests"> + <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <option name="test-file-name" value="JobSchedulerPerfTests.apk"/> + <option name="cleanup-apks" value="true"/> + </target_preparer> + + <option name="test-suite-tag" value="apct"/> + <option name="test-tag" value="JobSchedulerPerfTests"/> + <test class="com.android.tradefed.testtype.AndroidJUnitTest"> + <option name="package" value="com.android.frameworks.perftests.job"/> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner"/> + </test> +</configuration> diff --git a/tests/JobSchedulerPerfTests/src/com/android/frameworks/perftests/job/JobStorePerfTests.java b/tests/JobSchedulerPerfTests/src/com/android/frameworks/perftests/job/JobStorePerfTests.java new file mode 100644 index 000000000000..e956be339bc4 --- /dev/null +++ b/tests/JobSchedulerPerfTests/src/com/android/frameworks/perftests/job/JobStorePerfTests.java @@ -0,0 +1,156 @@ +/* + * 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.frameworks.perftests.job; + + +import android.app.job.JobInfo; +import android.content.ComponentName; +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.job.JobStore; +import com.android.server.job.JobStore.JobSet; +import com.android.server.job.controllers.JobStatus; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class JobStorePerfTests { + private static final String SOURCE_PACKAGE = "com.android.frameworks.perftests.job"; + private static final int SOURCE_USER_ID = 0; + private static final int CALLING_UID = 10079; + + private static Context sContext; + private static File sTestDir; + private static JobStore sJobStore; + + private static List<JobStatus> sFewJobs = new ArrayList<>(); + private static List<JobStatus> sManyJobs = new ArrayList<>(); + + @Rule + public PerfManualStatusReporter mPerfManualStatusReporter = new PerfManualStatusReporter(); + + @BeforeClass + public static void setUpOnce() { + sContext = InstrumentationRegistry.getTargetContext(); + sTestDir = new File(sContext.getFilesDir(), "JobStorePerfTests"); + sJobStore = JobStore.initAndGetForTesting(sContext, sTestDir); + + for (int i = 0; i < 50; i++) { + sFewJobs.add(createJobStatus("fewJobs", i)); + } + for (int i = 0; i < 500; i++) { + sManyJobs.add(createJobStatus("manyJobs", i)); + } + } + + @AfterClass + public static void tearDownOnce() { + sTestDir.deleteOnExit(); + } + + private void runPersistedJobWriting(List<JobStatus> jobList) { + final ManualBenchmarkState benchmarkState = mPerfManualStatusReporter.getBenchmarkState(); + + long elapsedTimeNs = 0; + while (benchmarkState.keepRunning(elapsedTimeNs)) { + sJobStore.clear(); + for (JobStatus job : jobList) { + sJobStore.add(job); + } + sJobStore.waitForWriteToCompleteForTesting(10_000); + + final long startTime = SystemClock.elapsedRealtimeNanos(); + sJobStore.writeStatusToDiskForTesting(); + final long endTime = SystemClock.elapsedRealtimeNanos(); + elapsedTimeNs = endTime - startTime; + } + } + + @Test + public void testPersistedJobWriting_fewJobs() { + runPersistedJobWriting(sFewJobs); + } + + @Test + public void testPersistedJobWriting_manyJobs() { + runPersistedJobWriting(sManyJobs); + } + + private void runPersistedJobReading(List<JobStatus> jobList, boolean rtcIsGood) { + final ManualBenchmarkState benchmarkState = mPerfManualStatusReporter.getBenchmarkState(); + + long elapsedTimeNs = 0; + while (benchmarkState.keepRunning(elapsedTimeNs)) { + sJobStore.clear(); + for (JobStatus job : jobList) { + sJobStore.add(job); + } + sJobStore.waitForWriteToCompleteForTesting(10_000); + + JobSet jobSet = new JobSet(); + + final long startTime = SystemClock.elapsedRealtimeNanos(); + sJobStore.readJobMapFromDisk(jobSet, rtcIsGood); + final long endTime = SystemClock.elapsedRealtimeNanos(); + elapsedTimeNs = endTime - startTime; + } + } + + @Test + public void testPersistedJobReading_fewJobs_goodRTC() { + runPersistedJobReading(sFewJobs, true); + } + + @Test + public void testPersistedJobReading_fewJobs_badRTC() { + runPersistedJobReading(sFewJobs, false); + } + + @Test + public void testPersistedJobReading_manyJobs_goodRTC() { + runPersistedJobReading(sManyJobs, true); + } + + @Test + public void testPersistedJobReading_manyJobs_badRTC() { + runPersistedJobReading(sManyJobs, false); + } + + private static JobStatus createJobStatus(String testTag, int jobId) { + JobInfo jobInfo = new JobInfo.Builder(jobId, + new ComponentName(sContext, "JobStorePerfTestJobService")) + .setPersisted(true) + .build(); + return JobStatus.createFromJobInfo( + jobInfo, CALLING_UID, SOURCE_PACKAGE, SOURCE_USER_ID, testTag); + } +} diff --git a/tests/ManagedProfileLifecycleStressTest/Android.bp b/tests/ManagedProfileLifecycleStressTest/Android.bp new file mode 100644 index 000000000000..639ce3cfe935 --- /dev/null +++ b/tests/ManagedProfileLifecycleStressTest/Android.bp @@ -0,0 +1,23 @@ +// 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. + +java_test_host { + name: "ManagedProfileLifecycleStressTest", + srcs: ["src/**/*.java"], + libs: ["tradefed"], + test_suites: ["device-tests"], + target_required: [ + "DummyDPC", + ], +} diff --git a/tests/WindowManagerStressTest/res/values/dimens.xml b/tests/ManagedProfileLifecycleStressTest/AndroidTest.xml index ed4ccbcc700f..e7dbc5118457 100644 --- a/tests/WindowManagerStressTest/res/values/dimens.xml +++ b/tests/ManagedProfileLifecycleStressTest/AndroidTest.xml @@ -1,4 +1,5 @@ -<!-- Copyright (C) 2016 The Android Open Source Project +<?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. @@ -12,8 +13,8 @@ See the License for the specific language governing permissions and limitations under the License. --> -<resources> - <!-- Default screen margins, per the Android Design guidelines. --> - <dimen name="activity_horizontal_margin">16dp</dimen> - <dimen name="activity_vertical_margin">16dp</dimen> -</resources> +<configuration description="Stress test for managed profile lifecycle"> + <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" > + <option name="jar" value="ManagedProfileLifecycleStressTest.jar" /> + </test> +</configuration> diff --git a/tests/ManagedProfileLifecycleStressTest/app/DummyDPC/Android.bp b/tests/ManagedProfileLifecycleStressTest/app/DummyDPC/Android.bp new file mode 100644 index 000000000000..1f47b03d0074 --- /dev/null +++ b/tests/ManagedProfileLifecycleStressTest/app/DummyDPC/Android.bp @@ -0,0 +1,21 @@ +// 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 { + name: "DummyDPC", + defaults: ["cts_defaults"], + srcs: ["src/**/*.java"], + sdk_version: "current", + test_suites: ["device-tests"], +} diff --git a/tests/ManagedProfileLifecycleStressTest/app/DummyDPC/AndroidManifest.xml b/tests/ManagedProfileLifecycleStressTest/app/DummyDPC/AndroidManifest.xml new file mode 100644 index 000000000000..860940d4e025 --- /dev/null +++ b/tests/ManagedProfileLifecycleStressTest/app/DummyDPC/AndroidManifest.xml @@ -0,0 +1,32 @@ +<?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.dummydpc"> + + <application + android:testOnly="true"> + <receiver + android:name="com.android.dummydpc.DummyDeviceAdminReceiver" + android:permission="android.permission.BIND_DEVICE_ADMIN"> + <meta-data android:name="android.app.device_admin" + android:resource="@xml/device_admin" /> + <intent-filter> + <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" /> + </intent-filter> + </receiver> + </application> +</manifest> diff --git a/tests/ManagedProfileLifecycleStressTest/app/DummyDPC/res/xml/device_admin.xml b/tests/ManagedProfileLifecycleStressTest/app/DummyDPC/res/xml/device_admin.xml new file mode 100644 index 000000000000..4b3581e3e8da --- /dev/null +++ b/tests/ManagedProfileLifecycleStressTest/app/DummyDPC/res/xml/device_admin.xml @@ -0,0 +1,4 @@ +<device-admin> + <uses-policies> + </uses-policies> +</device-admin> diff --git a/tests/ManagedProfileLifecycleStressTest/app/DummyDPC/src/com/android/dummydpc/DummyDeviceAdminReceiver.java b/tests/ManagedProfileLifecycleStressTest/app/DummyDPC/src/com/android/dummydpc/DummyDeviceAdminReceiver.java new file mode 100644 index 000000000000..92190b73b0ff --- /dev/null +++ b/tests/ManagedProfileLifecycleStressTest/app/DummyDPC/src/com/android/dummydpc/DummyDeviceAdminReceiver.java @@ -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.dummydpc; + +import android.app.admin.DeviceAdminReceiver; + +/** + * Empty admin to use as a managed profile owner. + */ +public class DummyDeviceAdminReceiver extends DeviceAdminReceiver { +} + diff --git a/tests/ManagedProfileLifecycleStressTest/src/com/android/test/stress/ManagedProfileLifecycleStressTest.java b/tests/ManagedProfileLifecycleStressTest/src/com/android/test/stress/ManagedProfileLifecycleStressTest.java new file mode 100644 index 000000000000..026677e09bed --- /dev/null +++ b/tests/ManagedProfileLifecycleStressTest/src/com/android/test/stress/ManagedProfileLifecycleStressTest.java @@ -0,0 +1,97 @@ +/* + * 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.stress; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.android.tradefed.log.LogUtil.CLog; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; +import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A test to exercise Android Framework parts related to creating, starting, stopping, and deleting + * a managed profile as much as possible. The aim is to catch any issues in this code before it + * affects managed profile CTS tests. + */ +@RunWith(DeviceJUnit4ClassRunner.class) +public class ManagedProfileLifecycleStressTest extends BaseHostJUnit4Test { + // Stop the test once this time limit has been reached. 25 minutes used as a limit to make total + // test time less than 30 minutes, so that it can be put into presubmit. + private static final int TIME_LIMIT_MINUTES = 25; + + private static final String DUMMY_DPC_APK = "DummyDPC.apk"; + private static final String DUMMY_DPC_COMPONENT = + "com.android.dummydpc/com.android.dummydpc.DummyDeviceAdminReceiver"; + private static final Pattern CREATE_USER_OUTPUT_REGEX = + Pattern.compile("Success: created user id (\\d+)"); + + /** + * Create, start, and kill managed profiles in a loop. + */ + @Test + public void testCreateStartDelete() throws Exception { + // Disable package verifier for ADB installs. + getDevice().executeShellCommand("settings put global verifier_verify_adb_installs 0"); + int iteration = 0; + final long deadline = System.nanoTime() + TimeUnit.MINUTES.toNanos(TIME_LIMIT_MINUTES); + while (System.nanoTime() < deadline) { + iteration++; + CLog.w("Iteration N" + iteration); + final int userId = createManagedProfile(); + startUser(userId); + installPackageAsUser(DUMMY_DPC_APK, true /* grantPermissions */, userId, "-t"); + setProfileOwner(DUMMY_DPC_COMPONENT, userId); + removeUser(userId); + } + CLog.w("Completed " + iteration + " iterations."); + } + + private int createManagedProfile() throws Exception { + final String output = getDevice().executeShellCommand( + "pm create-user --profileOf 0 --managed TestProfile"); + final Matcher matcher = CREATE_USER_OUTPUT_REGEX.matcher(output.trim()); + if (!matcher.matches() || matcher.groupCount() != 1) { + fail("user creation failed, output: " + output); + } + return Integer.parseInt(matcher.group(1)); + } + + private void setProfileOwner(String componentName, int userId) throws Exception { + String command = "dpm set-profile-owner --user " + userId + " '" + componentName + "'"; + String commandOutput = getDevice().executeShellCommand(command); + assertTrue("Unexpected dpm output: " + commandOutput, commandOutput.startsWith("Success:")); + } + + private void removeUser(int userId) throws Exception { + final String output = getDevice().executeShellCommand("pm remove-user " + userId).trim(); + assertEquals("Unexpected pm output: " + output, "Success: removed user", output); + } + + private void startUser(int userId) throws Exception { + final String output = getDevice().executeShellCommand("am start-user -w " + userId).trim(); + assertEquals("Unexpected am output: " + output, "Success: user started", output); + } +} diff --git a/tests/MemoryUsage/src/com/android/tests/memoryusage/MemoryUsageTest.java b/tests/MemoryUsage/src/com/android/tests/memoryusage/MemoryUsageTest.java index 653282d0d365..c7e5a5ea3311 100644 --- a/tests/MemoryUsage/src/com/android/tests/memoryusage/MemoryUsageTest.java +++ b/tests/MemoryUsage/src/com/android/tests/memoryusage/MemoryUsageTest.java @@ -320,8 +320,10 @@ public class MemoryUsageTest extends InstrumentationTestCase { UserHandle.USER_CURRENT); } - mAtm.startActivityAndWait(null, null, mLaunchIntent, mimeType, - null, null, 0, mLaunchIntent.getFlags(), null, null, + mAtm.startActivityAndWait(null, + getInstrumentation().getContext().getBasePackageName(), + getInstrumentation().getContext().getAttributionTag(), mLaunchIntent, + mimeType, null, null, 0, mLaunchIntent.getFlags(), null, null, UserHandle.USER_CURRENT_OR_SELF); } catch (RemoteException e) { Log.w(TAG, "Error launching app", e); diff --git a/tests/MirrorSurfaceTest/Android.bp b/tests/MirrorSurfaceTest/Android.bp new file mode 100644 index 000000000000..e359c64cc982 --- /dev/null +++ b/tests/MirrorSurfaceTest/Android.bp @@ -0,0 +1,6 @@ +android_test { + name: "MirrorSurfaceTest", + srcs: ["src/**/*.java"], + platform_apis: true, + certificate: "platform", +} diff --git a/tests/MirrorSurfaceTest/AndroidManifest.xml b/tests/MirrorSurfaceTest/AndroidManifest.xml new file mode 100644 index 000000000000..123cd0f26ff3 --- /dev/null +++ b/tests/MirrorSurfaceTest/AndroidManifest.xml @@ -0,0 +1,34 @@ +<?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.google.android.test.mirrorsurface"> + <uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER"/> + <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> + + <application android:label="MirrorSurfaceTest"> + <activity android:name=".MirrorSurfaceActivity" + android:label="Mirror Surface" + android:configChanges="orientation|screenSize"> + <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> + </application> +</manifest> diff --git a/tests/MirrorSurfaceTest/res/layout/activity_mirror_surface.xml b/tests/MirrorSurfaceTest/res/layout/activity_mirror_surface.xml new file mode 100644 index 000000000000..73b509f743d1 --- /dev/null +++ b/tests/MirrorSurfaceTest/res/layout/activity_mirror_surface.xml @@ -0,0 +1,111 @@ +<?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:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:layout_marginTop="20dp" + android:orientation="horizontal"> + + <Button + android:id="@+id/mirror_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginRight="20dp" + android:text="Mirror" + android:textSize="20dp" /> + + <Button + android:id="@+id/remove_mirror_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginRight="20dp" + android:text="Remove Mirror" + android:textSize="20dp" /> + </LinearLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="40dp" + android:layout_marginRight="40dp" + android:layout_marginTop="10dp" + android:orientation="horizontal"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginRight="20dp" + android:text="SCALE: " /> + + <EditText + android:hint="0.5" + android:id="@+id/scale" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="numberDecimal" /> + </LinearLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="40dp" + android:layout_marginRight="40dp" + android:layout_marginTop="10dp" + android:orientation="horizontal"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginRight="20dp" + android:text="DISPLAY FRAME: " /> + + <EditText + android:hint="0, 0, 20, 20" + android:id="@+id/displayFrame" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="numberDecimal|text"/> + </LinearLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="40dp" + android:layout_marginRight="40dp" + android:layout_marginTop="10dp" + android:orientation="horizontal"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginRight="20dp" + android:text="SOURCE POSITION: " /> + + <TextView + android:id="@+id/sourcePosition" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="numberDecimal|text"/> + </LinearLayout> +</LinearLayout> diff --git a/tests/MirrorSurfaceTest/res/layout/move_view.xml b/tests/MirrorSurfaceTest/res/layout/move_view.xml new file mode 100644 index 000000000000..57077006765e --- /dev/null +++ b/tests/MirrorSurfaceTest/res/layout/move_view.xml @@ -0,0 +1,101 @@ +<?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. + --> + +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="20dp" + android:gravity="center"> + + <RelativeLayout + android:id="@+id/arrows" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <ImageButton + android:id="@+id/up_arrow" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_toEndOf="@+id/right_arrow" + android:background="@android:color/holo_green_light" + android:padding="10dp" + android:src="@android:drawable/arrow_up_float" /> + + <ImageButton + android:id="@+id/down_arrow" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/up_arrow" + android:layout_marginTop="80dp" + android:layout_toEndOf="@+id/right_arrow" + android:background="@android:color/holo_green_light" + android:padding="10dp" + android:src="@android:drawable/arrow_down_float" /> + + <ImageButton + android:id="@+id/right_arrow" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignTop="@+id/up_arrow" + android:layout_alignBottom="@+id/down_arrow" + android:layout_marginTop="55dp" + android:layout_marginEnd="15dp" + android:layout_marginBottom="55dp" + android:background="@android:color/holo_green_light" + android:paddingLeft="10dp" + android:paddingRight="10dp" + android:rotation="90" + android:src="@android:drawable/arrow_down_float" /> + + <ImageButton + android:id="@+id/left_arrow" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignTop="@+id/up_arrow" + android:layout_alignBottom="@+id/down_arrow" + android:layout_marginStart="15dp" + android:layout_marginTop="55dp" + android:layout_marginBottom="55dp" + android:layout_toEndOf="@+id/down_arrow" + android:background="@android:color/holo_green_light" + android:paddingLeft="10dp" + android:paddingRight="10dp" + android:rotation="-90" + android:src="@android:drawable/arrow_down_float" /> + </RelativeLayout> + + <RelativeLayout + + android:layout_gravity="center" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <Button + android:id="@+id/zoom_in_button" + android:layout_width="40dp" + android:layout_marginBottom="-8dp" + android:layout_height="40dp" + android:text="+" /> + + <Button + android:layout_below="@+id/zoom_in_button" + android:id="@+id/zoom_out_button" + android:layout_width="40dp" + android:layout_height="40dp" + android:text="-" /> + </RelativeLayout> +</FrameLayout>
\ No newline at end of file diff --git a/tests/MirrorSurfaceTest/src/com/google/android/test/mirrorsurface/MirrorSurfaceActivity.java b/tests/MirrorSurfaceTest/src/com/google/android/test/mirrorsurface/MirrorSurfaceActivity.java new file mode 100644 index 000000000000..8afe8411a790 --- /dev/null +++ b/tests/MirrorSurfaceTest/src/com/google/android/test/mirrorsurface/MirrorSurfaceActivity.java @@ -0,0 +1,445 @@ +/* + * 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.mirrorsurface; + +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + +import android.app.Activity; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Handler; +import android.os.RemoteException; +import android.view.Gravity; +import android.view.IWindowManager; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.SurfaceControl; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.WindowManagerGlobal; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.window.WindowMetricsHelper; + +public class MirrorSurfaceActivity extends Activity implements View.OnClickListener, + View.OnLongClickListener, View.OnTouchListener { + private static final int BORDER_SIZE = 10; + private static final int DEFAULT_SCALE = 2; + private static final int DEFAULT_BORDER_COLOR = Color.argb(255, 255, 153, 0); + private static final int MOVE_FRAME_AMOUNT = 20; + + private IWindowManager mIWm; + // An instance of WindowManager that is adjusted for adding windows with type + // TYPE_APPLICATION_OVERLAY. + private WindowManager mWm; + + private SurfaceControl mSurfaceControl = new SurfaceControl(); + private SurfaceControl mBorderSc; + + private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); + private View mOverlayView; + private View mArrowOverlay; + + private Rect mWindowBounds = new Rect(); + + private EditText mScaleText; + private EditText mDisplayFrameText; + private TextView mSourcePositionText; + + private Rect mTmpRect = new Rect(); + private final Surface mTmpSurface = new Surface(); + + private boolean mHasMirror; + + private Rect mCurrFrame = new Rect(); + private float mCurrScale = DEFAULT_SCALE; + + private final Handler mHandler = new Handler(); + + private MoveMirrorRunnable mMoveMirrorRunnable = new MoveMirrorRunnable(); + private boolean mIsPressedDown = false; + + private int mDisplayId; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_mirror_surface); + mWm = createWindowContext(TYPE_APPLICATION_OVERLAY, null /* options */) + .getSystemService(WindowManager.class); + mIWm = WindowManagerGlobal.getWindowManagerService(); + + Rect windowBounds = WindowMetricsHelper.getBoundsExcludingNavigationBarAndCutout( + mWm.getCurrentWindowMetrics()); + mWindowBounds.set(0, 0, windowBounds.width(), windowBounds.height()); + + mScaleText = findViewById(R.id.scale); + mDisplayFrameText = findViewById(R.id.displayFrame); + mSourcePositionText = findViewById(R.id.sourcePosition); + + mCurrFrame.set(0, 0, mWindowBounds.width() / 2, mWindowBounds.height() / 2); + mCurrScale = DEFAULT_SCALE; + + mDisplayId = getDisplay().getDisplayId(); + updateEditTexts(); + + findViewById(R.id.mirror_button).setOnClickListener(view -> { + if (mArrowOverlay == null) { + createArrowOverlay(); + } + createOrUpdateMirror(); + }); + + findViewById(R.id.remove_mirror_button).setOnClickListener(v -> { + removeMirror(); + removeArrowOverlay(); + }); + + createMirrorOverlay(); + } + + private void updateEditTexts() { + mDisplayFrameText.setText( + String.format("%s, %s, %s, %s", mCurrFrame.left, mCurrFrame.top, mCurrFrame.right, + mCurrFrame.bottom)); + mScaleText.setText(String.valueOf(mCurrScale)); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (mOverlayView != null) { + removeMirror(); + mWm.removeView(mOverlayView); + mOverlayView = null; + } + removeArrowOverlay(); + } + + private void createArrowOverlay() { + mArrowOverlay = getLayoutInflater().inflate(R.layout.move_view, null); + WindowManager.LayoutParams arrowParams = new WindowManager.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.RGBA_8888); + arrowParams.gravity = Gravity.RIGHT | Gravity.BOTTOM; + mWm.addView(mArrowOverlay, arrowParams); + + View leftArrow = mArrowOverlay.findViewById(R.id.left_arrow); + View topArrow = mArrowOverlay.findViewById(R.id.up_arrow); + View rightArrow = mArrowOverlay.findViewById(R.id.right_arrow); + View bottomArrow = mArrowOverlay.findViewById(R.id.down_arrow); + + leftArrow.setOnClickListener(this); + topArrow.setOnClickListener(this); + rightArrow.setOnClickListener(this); + bottomArrow.setOnClickListener(this); + + leftArrow.setOnLongClickListener(this); + topArrow.setOnLongClickListener(this); + rightArrow.setOnLongClickListener(this); + bottomArrow.setOnLongClickListener(this); + + leftArrow.setOnTouchListener(this); + topArrow.setOnTouchListener(this); + rightArrow.setOnTouchListener(this); + bottomArrow.setOnTouchListener(this); + + mArrowOverlay.findViewById(R.id.zoom_in_button).setOnClickListener(v -> { + if (mCurrScale <= 1) { + mCurrScale *= 2; + } else { + mCurrScale += 0.5; + } + + updateMirror(mCurrFrame, mCurrScale); + }); + mArrowOverlay.findViewById(R.id.zoom_out_button).setOnClickListener(v -> { + if (mCurrScale <= 1) { + mCurrScale /= 2; + } else { + mCurrScale -= 0.5; + } + + updateMirror(mCurrFrame, mCurrScale); + }); + } + + private void removeArrowOverlay() { + if (mArrowOverlay != null) { + mWm.removeView(mArrowOverlay); + mArrowOverlay = null; + } + } + + private void createMirrorOverlay() { + mOverlayView = new LinearLayout(this); + WindowManager.LayoutParams params = new WindowManager.LayoutParams(mWindowBounds.width(), + mWindowBounds.height(), + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.RGBA_8888); + params.gravity = Gravity.LEFT | Gravity.TOP; + params.setTitle("Mirror Overlay"); + mOverlayView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); + + mWm.addView(mOverlayView, params); + + } + + private void removeMirror() { + if (mSurfaceControl.isValid()) { + mTransaction.remove(mSurfaceControl).apply(); + } + mHasMirror = false; + } + + private void createOrUpdateMirror() { + if (mHasMirror) { + updateMirror(getDisplayFrame(), getScale()); + } else { + createMirror(getDisplayFrame(), getScale()); + } + + } + + private Rect getDisplayFrame() { + mTmpRect.setEmpty(); + String[] frameVals = mDisplayFrameText.getText().toString().split("\\s*,\\s*"); + if (frameVals.length != 4) { + return mTmpRect; + } + + try { + mTmpRect.set(Integer.parseInt(frameVals[0]), Integer.parseInt(frameVals[1]), + Integer.parseInt(frameVals[2]), Integer.parseInt(frameVals[3])); + } catch (Exception e) { + mTmpRect.setEmpty(); + } + + return mTmpRect; + } + + private float getScale() { + try { + return Float.parseFloat(mScaleText.getText().toString()); + } catch (Exception e) { + return -1; + } + } + + private void createMirror(Rect displayFrame, float scale) { + boolean success = false; + try { + success = mIWm.mirrorDisplay(mDisplayId, mSurfaceControl); + } catch (RemoteException e) { + } + + if (!success) { + return; + } + + if (!mSurfaceControl.isValid()) { + return; + } + + mHasMirror = true; + + mBorderSc = new SurfaceControl.Builder() + .setName("Mirror Border") + .setBufferSize(1, 1) + .setFormat(PixelFormat.TRANSLUCENT) + .build(); + + updateMirror(displayFrame, scale); + + mTransaction + .show(mSurfaceControl) + .reparent(mSurfaceControl, mOverlayView.getViewRootImpl().getSurfaceControl()) + .setLayer(mBorderSc, 1) + .show(mBorderSc) + .reparent(mBorderSc, mSurfaceControl) + .apply(); + } + + private void updateMirror(Rect displayFrame, float scale) { + if (displayFrame.isEmpty()) { + Rect bounds = mWindowBounds; + int defaultCropW = Math.round(bounds.width() / 2); + int defaultCropH = Math.round(bounds.height() / 2); + displayFrame.set(0, 0, defaultCropW, defaultCropH); + } + + if (scale <= 0) { + scale = DEFAULT_SCALE; + } + + mCurrFrame.set(displayFrame); + mCurrScale = scale; + + int width = (int) Math.ceil(displayFrame.width() / scale); + int height = (int) Math.ceil(displayFrame.height() / scale); + + Rect sourceBounds = getSourceBounds(displayFrame, scale); + + mTransaction.setGeometry(mSurfaceControl, sourceBounds, displayFrame, Surface.ROTATION_0) + .setPosition(mBorderSc, sourceBounds.left, sourceBounds.top) + .setBufferSize(mBorderSc, width, height) + .apply(); + + drawBorder(mBorderSc, width, height, (int) Math.ceil(BORDER_SIZE / scale)); + + mSourcePositionText.setText(sourceBounds.left + ", " + sourceBounds.top); + mDisplayFrameText.setText( + String.format("%s, %s, %s, %s", mCurrFrame.left, mCurrFrame.top, mCurrFrame.right, + mCurrFrame.bottom)); + mScaleText.setText(String.valueOf(mCurrScale)); + } + + private void drawBorder(SurfaceControl borderSc, int width, int height, int borderSize) { + mTmpSurface.copyFrom(borderSc); + + Canvas c = null; + try { + c = mTmpSurface.lockCanvas(null); + } catch (IllegalArgumentException | Surface.OutOfResourcesException e) { + } + if (c == null) { + return; + } + + // Top + c.save(); + c.clipRect(new Rect(0, 0, width, borderSize)); + c.drawColor(DEFAULT_BORDER_COLOR); + c.restore(); + // Left + c.save(); + c.clipRect(new Rect(0, 0, borderSize, height)); + c.drawColor(DEFAULT_BORDER_COLOR); + c.restore(); + // Right + c.save(); + c.clipRect(new Rect(width - borderSize, 0, width, height)); + c.drawColor(DEFAULT_BORDER_COLOR); + c.restore(); + // Bottom + c.save(); + c.clipRect(new Rect(0, height - borderSize, width, height)); + c.drawColor(DEFAULT_BORDER_COLOR); + c.restore(); + + mTmpSurface.unlockCanvasAndPost(c); + } + + @Override + public void onClick(View v) { + Point offset = findOffset(v); + moveMirrorForArrows(offset.x, offset.y); + } + + @Override + public boolean onLongClick(View v) { + mIsPressedDown = true; + Point point = findOffset(v); + mMoveMirrorRunnable.mXOffset = point.x; + mMoveMirrorRunnable.mYOffset = point.y; + mHandler.post(mMoveMirrorRunnable); + return false; + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mIsPressedDown = false; + break; + } + return false; + } + + private Point findOffset(View v) { + Point offset = new Point(0, 0); + + switch (v.getId()) { + case R.id.up_arrow: + offset.y = -MOVE_FRAME_AMOUNT; + break; + case R.id.down_arrow: + offset.y = MOVE_FRAME_AMOUNT; + break; + case R.id.right_arrow: + offset.x = -MOVE_FRAME_AMOUNT; + break; + case R.id.left_arrow: + offset.x = MOVE_FRAME_AMOUNT; + break; + } + + return offset; + } + + private void moveMirrorForArrows(int xOffset, int yOffset) { + mCurrFrame.offset(xOffset, yOffset); + + updateMirror(mCurrFrame, mCurrScale); + } + + /** + * Calculates the desired source bounds. This will be the area under from the center of the + * displayFrame, factoring in scale. + */ + private Rect getSourceBounds(Rect displayFrame, float scale) { + int halfWidth = displayFrame.width() / 2; + int halfHeight = displayFrame.height() / 2; + int left = displayFrame.left + (halfWidth - (int) (halfWidth / scale)); + int right = displayFrame.right - (halfWidth - (int) (halfWidth / scale)); + int top = displayFrame.top + (halfHeight - (int) (halfHeight / scale)); + int bottom = displayFrame.bottom - (halfHeight - (int) (halfHeight / scale)); + return new Rect(left, top, right, bottom); + } + + class MoveMirrorRunnable implements Runnable { + int mXOffset = 0; + int mYOffset = 0; + + @Override + public void run() { + if (mIsPressedDown) { + moveMirrorForArrows(mXOffset, mYOffset); + mHandler.postDelayed(mMoveMirrorRunnable, 150); + } + } + } +} diff --git a/tests/PackageWatchdog/Android.bp b/tests/PackageWatchdog/Android.bp index 88d92c4d94fe..0b75039cf69f 100644 --- a/tests/PackageWatchdog/Android.bp +++ b/tests/PackageWatchdog/Android.bp @@ -23,6 +23,7 @@ android_test { "androidx.test.rules", "services.core", "services.net", + "truth-prebuilt", ], libs: ["android.test.runner"], jni_libs: [ diff --git a/tests/PackageWatchdog/src/com/android/server/ExplicitHealthCheckServiceTest.java b/tests/PackageWatchdog/src/com/android/server/ExplicitHealthCheckServiceTest.java new file mode 100644 index 000000000000..2fbfeba47b13 --- /dev/null +++ b/tests/PackageWatchdog/src/com/android/server/ExplicitHealthCheckServiceTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2020 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 com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; + +import android.content.Intent; +import android.os.IBinder; +import android.os.RemoteCallback; +import android.service.watchdog.ExplicitHealthCheckService; +import android.service.watchdog.IExplicitHealthCheckService; + +import org.junit.Before; +import org.junit.Test; + +import java.util.concurrent.CountDownLatch; + +public class ExplicitHealthCheckServiceTest { + + private ExplicitHealthCheckService mExplicitHealthCheckService; + private static final String PACKAGE_NAME = "com.test.package"; + + @Before + public void setup() throws Exception { + mExplicitHealthCheckService = spy(ExplicitHealthCheckService.class); + } + + /** + * Test to verify that the correct information is sent in the callback when a package has + * passed an explicit health check. + */ + @Test + public void testNotifyHealthCheckPassed() throws Exception { + IBinder binder = mExplicitHealthCheckService.onBind(new Intent()); + CountDownLatch countDownLatch = new CountDownLatch(1); + RemoteCallback callback = new RemoteCallback(result -> { + assertThat(result.get(ExplicitHealthCheckService.EXTRA_HEALTH_CHECK_PASSED_PACKAGE)) + .isEqualTo(PACKAGE_NAME); + countDownLatch.countDown(); + }); + IExplicitHealthCheckService.Stub.asInterface(binder).setCallback(callback); + mExplicitHealthCheckService.notifyHealthCheckPassed(PACKAGE_NAME); + countDownLatch.await(); + } +} diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java index be22c9e4bfe6..d011dbbbe5db 100644 --- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java +++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java @@ -18,12 +18,13 @@ 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 com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; + +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; @@ -38,12 +39,15 @@ import android.content.pm.VersionedPackage; import android.net.ConnectivityModuleConnector; import android.net.ConnectivityModuleConnector.ConnectivityModuleHealthListener; import android.os.Handler; +import android.os.SystemProperties; import android.os.test.TestLooper; import android.provider.DeviceConfig; import android.util.AtomicFile; import androidx.test.InstrumentationRegistry; +import com.android.dx.mockito.inline.extended.ExtendedMockito; +import com.android.server.PackageWatchdog.HealthCheckState; import com.android.server.PackageWatchdog.MonitoredPackage; import com.android.server.PackageWatchdog.PackageHealthObserver; import com.android.server.PackageWatchdog.PackageHealthObserverImpact; @@ -55,19 +59,20 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; +import org.mockito.stubbing.Answer; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; 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. */ @@ -83,6 +88,7 @@ public class PackageWatchdogTest { 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 final TestClock mTestClock = new TestClock(); private TestLooper mTestLooper; private Context mSpyContext; @Mock @@ -91,6 +97,8 @@ public class PackageWatchdogTest { private PackageManager mMockPackageManager; @Captor private ArgumentCaptor<ConnectivityModuleHealthListener> mConnectivityModuleCallbackCaptor; + private MockitoSession mSession; + private HashMap<String, String> mSystemSettingsMap; @Before public void setUp() throws Exception { @@ -107,85 +115,154 @@ public class PackageWatchdogTest { res.setLongVersionCode(VERSION_CODE); return res; }); + mSession = ExtendedMockito.mockitoSession() + .initMocks(this) + .strictness(Strictness.LENIENT) + .spyStatic(SystemProperties.class) + .startMocking(); + mSystemSettingsMap = new HashMap<>(); + + + // Mock SystemProperties setter and various getters + doAnswer((Answer<Void>) invocationOnMock -> { + String key = invocationOnMock.getArgument(0); + String value = invocationOnMock.getArgument(1); + + mSystemSettingsMap.put(key, value); + return null; + } + ).when(() -> SystemProperties.set(anyString(), anyString())); + + doAnswer((Answer<Integer>) invocationOnMock -> { + String key = invocationOnMock.getArgument(0); + int defaultValue = invocationOnMock.getArgument(1); + + String storedValue = mSystemSettingsMap.get(key); + return storedValue == null ? defaultValue : Integer.parseInt(storedValue); + } + ).when(() -> SystemProperties.getInt(anyString(), anyInt())); + + doAnswer((Answer<Long>) invocationOnMock -> { + String key = invocationOnMock.getArgument(0); + long defaultValue = invocationOnMock.getArgument(1); + + String storedValue = mSystemSettingsMap.get(key); + return storedValue == null ? defaultValue : Long.parseLong(storedValue); + } + ).when(() -> SystemProperties.getLong(anyString(), anyLong())); } @After public void tearDown() throws Exception { dropShellPermissions(); + mSession.finishMocking(); + } + + @Test + public void testRegistration_singleObserver() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer = new TestObserver(OBSERVER_NAME_1); + + watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION); + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + + // The failed packages should be the same as the registered ones to ensure registration is + // done successfully + assertThat(observer.mHealthCheckFailedPackages).containsExactly(APP_A); } - /** - * Test registration, unregistration, package expiry and duration reduction - */ @Test - public void testRegistration() throws Exception { + public void testRegistration_multiObservers() { 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(); + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE), + new VersionedPackage(APP_B, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + + // The failed packages should be the same as the registered ones to ensure registration is + // done successfully + assertThat(observer1.mHealthCheckFailedPackages).containsExactly(APP_A); + assertThat(observer2.mHealthCheckFailedPackages).containsExactly(APP_A, APP_B); + } - // 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(); + @Test + public void testUnregistration_singleObserver() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer = new TestObserver(OBSERVER_NAME_1); + + watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.unregisterHealthObserver(observer); + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); - // Verify observer3 expired - // 1 - assertNull(watchdog.getPackages(observer1)); - // 2 - assertNull(watchdog.getPackages(observer2)); - // 3 - assertNull(watchdog.getPackages(observer3)); + // We should have no failed packages to ensure unregistration is done successfully + assertThat(observer.mHealthCheckFailedPackages).isEmpty(); + } + + @Test + public void testUnregistration_multiObservers() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer1 = new TestObserver(OBSERVER_NAME_1); + TestObserver observer2 = new TestObserver(OBSERVER_NAME_2); + + watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.unregisterHealthObserver(observer2); + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + + // observer1 should receive failed packages as intended. + assertThat(observer1.mHealthCheckFailedPackages).containsExactly(APP_A); + // observer2 should have no failed packages to ensure unregistration is done successfully + assertThat(observer2.mHealthCheckFailedPackages).isEmpty(); + } + + @Test + public void testExpiration_singleObserver() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer = new TestObserver(OBSERVER_NAME_1); + + watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION); + moveTimeForwardAndDispatch(SHORT_DURATION); + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + + // We should have no failed packages for the fatal failure is raised after expiration + assertThat(observer.mHealthCheckFailedPackages).isEmpty(); + } + + @Test + public void testExpiration_multiObservers() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer1 = new TestObserver(OBSERVER_NAME_1); + TestObserver observer2 = new TestObserver(OBSERVER_NAME_2); + + watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), LONG_DURATION); + moveTimeForwardAndDispatch(SHORT_DURATION); + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + + // We should have no failed packages for the fatal failure is raised after expiration + assertThat(observer1.mHealthCheckFailedPackages).isEmpty(); + // We should have failed packages since observer2 hasn't expired + assertThat(observer2.mHealthCheckFailedPackages).containsExactly(APP_A); } /** Observing already observed package extends the observation time. */ @Test - public void testObserveAlreadyObservedPackage() throws Exception { + public void testObserveAlreadyObservedPackage() { PackageWatchdog watchdog = createWatchdog(); TestObserver observer = new TestObserver(OBSERVER_NAME_1); @@ -193,66 +270,49 @@ public class PackageWatchdogTest { watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION); // Then advance time half-way - Thread.sleep(SHORT_DURATION / 2); - mTestLooper.dispatchAll(); + moveTimeForwardAndDispatch(SHORT_DURATION / 2); // 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(); + moveTimeForwardAndDispatch((SHORT_DURATION / 2) + 1); + + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); - // 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)); + // Verify that we receive failed packages as expected for APP_A not expired + assertThat(observer.mHealthCheckFailedPackages).containsExactly(APP_A); } /** * Test package observers are persisted and loaded on startup */ @Test - public void testPersistence() throws Exception { + public void testPersistence() { 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 + // Then resume observer1 and observer2 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)); + raiseFatalFailureAndDispatch(watchdog2, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE), + new VersionedPackage(APP_B, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + + // We should receive failed packages as expected to ensure observers are persisted and + // resumed correctly + assertThat(observer1.mHealthCheckFailedPackages).containsExactly(APP_A); + assertThat(observer2.mHealthCheckFailedPackages).containsExactly(APP_A, APP_B); } /** @@ -277,8 +337,8 @@ public class PackageWatchdogTest { mTestLooper.dispatchAll(); // Verify that observers are not notified - assertEquals(0, observer1.mFailedPackages.size()); - assertEquals(0, observer2.mFailedPackages.size()); + assertThat(observer1.mHealthCheckFailedPackages).isEmpty(); + assertThat(observer2.mHealthCheckFailedPackages).isEmpty(); } /** @@ -296,17 +356,13 @@ public class PackageWatchdogTest { 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)), - PackageWatchdog.FAILURE_REASON_UNKNOWN); - } - - // Run handler so package failures are dispatched to observers - mTestLooper.dispatchAll(); + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_C, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); // Verify that observers are not notified - assertEquals(0, observer1.mFailedPackages.size()); - assertEquals(0, observer2.mFailedPackages.size()); + assertThat(observer1.mHealthCheckFailedPackages).isEmpty(); + assertThat(observer2.mHealthCheckFailedPackages).isEmpty(); } /** @@ -319,7 +375,8 @@ public class PackageWatchdogTest { long differentVersionCode = 2L; TestObserver observer = new TestObserver(OBSERVER_NAME_1) { @Override - public int onHealthCheckFailed(VersionedPackage versionedPackage) { + public int onHealthCheckFailed(VersionedPackage versionedPackage, + int failureReason) { if (versionedPackage.getVersionCode() == VERSION_CODE) { // Only rollback for specific versionCode return PackageHealthObserverImpact.USER_IMPACT_MEDIUM; @@ -331,17 +388,12 @@ public class PackageWatchdogTest { 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)), - PackageWatchdog.FAILURE_REASON_UNKNOWN); - } - - // Run handler so package failures are dispatched to observers - mTestLooper.dispatchAll(); + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, differentVersionCode)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); // Verify that observers are not notified - assertEquals(0, observer.mFailedPackages.size()); + assertThat(observer.mHealthCheckFailedPackages).isEmpty(); } @@ -371,34 +423,27 @@ public class PackageWatchdogTest { 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)), - PackageWatchdog.FAILURE_REASON_UNKNOWN); - } - - // Run handler so package failures are dispatched to observers - mTestLooper.dispatchAll(); + raiseFatalFailureAndDispatch(watchdog, + 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)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); // 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; + List<String> observerNonePackages = observerNone.mMitigatedPackages; + List<String> observerHighPackages = observerHigh.mMitigatedPackages; + List<String> observerMidPackages = observerMid.mMitigatedPackages; + List<String> observerLowPackages = observerLow.mMitigatedPackages; // APP_D failure observed by only observerNone is not caught cos its impact is none - assertEquals(0, observerNonePackages.size()); + assertThat(observerNonePackages).isEmpty(); // APP_C failure is caught by observerHigh cos it's the lowest impact observer - assertEquals(1, observerHighPackages.size()); - assertEquals(APP_C, observerHighPackages.get(0)); + assertThat(observerHighPackages).containsExactly(APP_C); // APP_B failure is caught by observerMid cos it's the lowest impact observer - assertEquals(1, observerMidPackages.size()); - assertEquals(APP_B, observerMidPackages.get(0)); + assertThat(observerMidPackages).containsExactly(APP_B); // APP_A failure is caught by observerLow cos it's the lowest impact observer - assertEquals(1, observerLowPackages.size()); - assertEquals(APP_A, observerLowPackages.get(0)); + assertThat(observerLowPackages).containsExactly(APP_A); } /** @@ -425,70 +470,55 @@ public class PackageWatchdogTest { 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)), - PackageWatchdog.FAILURE_REASON_UNKNOWN); - } - // Run handler so package failures are dispatched to observers - mTestLooper.dispatchAll(); + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); // Verify only observerFirst is notifed - assertEquals(1, observerFirst.mFailedPackages.size()); - assertEquals(APP_A, observerFirst.mFailedPackages.get(0)); - assertEquals(0, observerSecond.mFailedPackages.size()); + assertThat(observerFirst.mMitigatedPackages).containsExactly(APP_A); + assertThat(observerSecond.mMitigatedPackages).isEmpty(); // After observerFirst handles failure, next action it has is high impact observerFirst.mImpact = PackageHealthObserverImpact.USER_IMPACT_HIGH; - observerFirst.mFailedPackages.clear(); - observerSecond.mFailedPackages.clear(); + observerFirst.mMitigatedPackages.clear(); + observerSecond.mMitigatedPackages.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)), - PackageWatchdog.FAILURE_REASON_UNKNOWN); - } - // Run handler so package failures are dispatched to observers - mTestLooper.dispatchAll(); + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); // 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()); + assertThat(observerSecond.mMitigatedPackages).containsExactly(APP_A); + assertThat(observerFirst.mMitigatedPackages).isEmpty(); // After observerSecond handles failure, it has no further actions observerSecond.mImpact = PackageHealthObserverImpact.USER_IMPACT_NONE; - observerFirst.mFailedPackages.clear(); - observerSecond.mFailedPackages.clear(); + observerFirst.mMitigatedPackages.clear(); + observerSecond.mMitigatedPackages.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)), - PackageWatchdog.FAILURE_REASON_UNKNOWN); - } - // Run handler so package failures are dispatched to observers - mTestLooper.dispatchAll(); + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); // 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()); + assertThat(observerFirst.mMitigatedPackages).containsExactly(APP_A); + assertThat(observerSecond.mMitigatedPackages).isEmpty(); // After observerFirst handles failure, it too has no further actions observerFirst.mImpact = PackageHealthObserverImpact.USER_IMPACT_NONE; - observerFirst.mFailedPackages.clear(); - observerSecond.mFailedPackages.clear(); + observerFirst.mMitigatedPackages.clear(); + observerSecond.mMitigatedPackages.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)), - PackageWatchdog.FAILURE_REASON_UNKNOWN); - } - // Run handler so package failures are dispatched to observers - mTestLooper.dispatchAll(); + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); // Verify no observer is notified cos no actions left - assertEquals(0, observerFirst.mFailedPackages.size()); - assertEquals(0, observerSecond.mFailedPackages.size()); + assertThat(observerFirst.mMitigatedPackages).isEmpty(); + assertThat(observerSecond.mMitigatedPackages).isEmpty(); } /** @@ -507,18 +537,13 @@ public class PackageWatchdogTest { 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)), - PackageWatchdog.FAILURE_REASON_UNKNOWN); - } - - // Run handler so package failures are dispatched to observers - mTestLooper.dispatchAll(); + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); // Verify only one observer is notifed - assertEquals(1, observer1.mFailedPackages.size()); - assertEquals(APP_A, observer1.mFailedPackages.get(0)); - assertEquals(0, observer2.mFailedPackages.size()); + assertThat(observer1.mMitigatedPackages).containsExactly(APP_A); + assertThat(observer2.mMitigatedPackages).isEmpty(); } /** @@ -547,9 +572,7 @@ public class PackageWatchdogTest { // 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)); + assertThat(requestedPackages).containsExactly(APP_A, APP_B); // Then health check passed for APP_A (observer1 is aware) controller.setPackagePassed(APP_A); @@ -561,23 +584,19 @@ public class PackageWatchdogTest { 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(); + moveTimeForwardAndDispatch(SHORT_DURATION); // Verify we cancelled all requests on expiry - assertEquals(0, controller.getRequestedPackages().size()); + assertThat(controller.getRequestedPackages()).isEmpty(); // Verify observer1 is not notified - assertEquals(0, observer1.mFailedPackages.size()); + assertThat(observer1.mMitigatedPackages).isEmpty(); // Verify observer2 is notifed because health checks for APP_B never passed - assertEquals(1, observer2.mFailedPackages.size()); - assertEquals(APP_B, observer2.mFailedPackages.get(0)); + assertThat(observer2.mMitigatedPackages).containsExactly(APP_B); // 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)); + assertThat(observer3.mMitigatedPackages).containsExactly(APP_A); } /** @@ -604,9 +623,7 @@ public class PackageWatchdogTest { // 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)); + assertThat(requestedPackages).containsExactly(APP_A, APP_B); // Disable explicit health checks (marks APP_A and APP_B as passed) setExplicitHealthCheckEnabled(false); @@ -615,14 +632,13 @@ public class PackageWatchdogTest { mTestLooper.dispatchAll(); // Verify all checks are cancelled - assertEquals(0, controller.getRequestedPackages().size()); + assertThat(controller.getRequestedPackages()).isEmpty(); // Then expire APP_A - Thread.sleep(SHORT_DURATION); - mTestLooper.dispatchAll(); + moveTimeForwardAndDispatch(SHORT_DURATION); // Verify APP_A is not failed (APP_B) is not expired yet - assertEquals(0, observer.mFailedPackages.size()); + assertThat(observer.mMitigatedPackages).isEmpty(); // Re-enable explicit health checks setExplicitHealthCheckEnabled(true); @@ -631,7 +647,7 @@ public class PackageWatchdogTest { mTestLooper.dispatchAll(); // Verify no requests are made cos APP_A is expired and APP_B was marked as passed - assertEquals(0, controller.getRequestedPackages().size()); + assertThat(controller.getRequestedPackages()).isEmpty(); // Then set new supported packages controller.setSupportedPackages(Arrays.asList(APP_C)); @@ -643,16 +659,13 @@ public class PackageWatchdogTest { // Verify requests are only made for APP_C requestedPackages = controller.getRequestedPackages(); - assertEquals(1, requestedPackages.size()); - assertEquals(APP_C, requestedPackages.get(0)); + assertThat(requestedPackages).containsExactly(APP_C); // Then expire APP_A and APP_C - Thread.sleep(SHORT_DURATION); - mTestLooper.dispatchAll(); + moveTimeForwardAndDispatch(SHORT_DURATION); // 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)); + assertThat(observer.mMitigatedPackages).containsExactly(APP_C); } /** @@ -673,21 +686,48 @@ public class PackageWatchdogTest { watchdog.startObservingHealth(observer, Arrays.asList(APP_A), LONG_DURATION); // Then APP_A has exceeded health check duration - Thread.sleep(SHORT_DURATION); - mTestLooper.dispatchAll(); + moveTimeForwardAndDispatch(SHORT_DURATION); // Verify that health check is failed - assertEquals(1, observer.mFailedPackages.size()); - assertEquals(APP_A, observer.mFailedPackages.get(0)); + assertThat(observer.mMitigatedPackages).containsExactly(APP_A); - // 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); + // Clear failed packages and forward time to expire the observation duration + observer.mMitigatedPackages.clear(); + moveTimeForwardAndDispatch(LONG_DURATION); // Verify that health check failure is not notified again - assertTrue(observer.mFailedPackages.isEmpty()); + assertThat(observer.mMitigatedPackages).isEmpty(); + } + + /** + * Tests failure when health check duration is different from package observation duration + * Failure is also notified only once. + */ + @Test + public void testExplicitHealthCheckFailureAfterExpiry() { + 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 == SHORT_DURATION / 2 + // health check duration == SHORT_DURATION (set by default in the TestController) + controller.setSupportedPackages(Arrays.asList(APP_A)); + watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION / 2); + + // Forward time to expire the observation duration + moveTimeForwardAndDispatch(SHORT_DURATION / 2); + + // Verify that health check is failed + assertThat(observer.mMitigatedPackages).containsExactly(APP_A); + + // Clear failed packages and forward time to expire the health check duration + observer.mMitigatedPackages.clear(); + moveTimeForwardAndDispatch(SHORT_DURATION); + + // Verify that health check failure is not notified again + assertThat(observer.mMitigatedPackages).isEmpty(); } /** Tests {@link MonitoredPackage} health check state transitions. */ @@ -695,44 +735,46 @@ public class PackageWatchdogTest { public void testPackageHealthCheckStateTransitions() { TestController controller = new TestController(); PackageWatchdog wd = createWatchdog(controller, true /* withPackagesReady */); - MonitoredPackage m1 = wd.new MonitoredPackage(APP_A, LONG_DURATION, + MonitoredPackage m1 = wd.newMonitoredPackage(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); + MonitoredPackage m2 = wd.newMonitoredPackage(APP_B, LONG_DURATION, false); + MonitoredPackage m3 = wd.newMonitoredPackage(APP_C, LONG_DURATION, false); + MonitoredPackage m4 = wd.newMonitoredPackage(APP_D, LONG_DURATION, SHORT_DURATION, true); // Verify transition: inactive -> active -> passed // Verify initially inactive - assertEquals(MonitoredPackage.STATE_INACTIVE, m1.getHealthCheckStateLocked()); + assertThat(m1.getHealthCheckStateLocked()).isEqualTo(HealthCheckState.INACTIVE); // Verify still inactive, until we #setHealthCheckActiveLocked - assertEquals(MonitoredPackage.STATE_INACTIVE, m1.handleElapsedTimeLocked(SHORT_DURATION)); + assertThat(m1.handleElapsedTimeLocked(SHORT_DURATION)).isEqualTo(HealthCheckState.INACTIVE); // Verify now active - assertEquals(MonitoredPackage.STATE_ACTIVE, m1.setHealthCheckActiveLocked(SHORT_DURATION)); + assertThat(m1.setHealthCheckActiveLocked(SHORT_DURATION)).isEqualTo( + HealthCheckState.ACTIVE); // Verify now passed - assertEquals(MonitoredPackage.STATE_PASSED, m1.tryPassHealthCheckLocked()); + assertThat(m1.tryPassHealthCheckLocked()).isEqualTo(HealthCheckState.PASSED); // Verify transition: inactive -> active -> failed // Verify initially inactive - assertEquals(MonitoredPackage.STATE_INACTIVE, m2.getHealthCheckStateLocked()); + assertThat(m2.getHealthCheckStateLocked()).isEqualTo(HealthCheckState.INACTIVE); // Verify now active - assertEquals(MonitoredPackage.STATE_ACTIVE, m2.setHealthCheckActiveLocked(SHORT_DURATION)); + assertThat(m2.setHealthCheckActiveLocked(SHORT_DURATION)).isEqualTo( + HealthCheckState.ACTIVE); // Verify now failed - assertEquals(MonitoredPackage.STATE_FAILED, m2.handleElapsedTimeLocked(SHORT_DURATION)); + assertThat(m2.handleElapsedTimeLocked(SHORT_DURATION)).isEqualTo(HealthCheckState.FAILED); // Verify transition: inactive -> failed // Verify initially inactive - assertEquals(MonitoredPackage.STATE_INACTIVE, m3.getHealthCheckStateLocked()); + assertThat(m3.getHealthCheckStateLocked()).isEqualTo(HealthCheckState.INACTIVE); // Verify now failed because package expired - assertEquals(MonitoredPackage.STATE_FAILED, m3.handleElapsedTimeLocked(LONG_DURATION)); + assertThat(m3.handleElapsedTimeLocked(LONG_DURATION)).isEqualTo(HealthCheckState.FAILED); // Verify remains failed even when asked to pass - assertEquals(MonitoredPackage.STATE_FAILED, m3.tryPassHealthCheckLocked()); + assertThat(m3.tryPassHealthCheckLocked()).isEqualTo(HealthCheckState.FAILED); // Verify transition: passed - assertEquals(MonitoredPackage.STATE_PASSED, m4.getHealthCheckStateLocked()); + assertThat(m4.getHealthCheckStateLocked()).isEqualTo(HealthCheckState.PASSED); // Verify remains passed even if health check fails - assertEquals(MonitoredPackage.STATE_PASSED, m4.handleElapsedTimeLocked(SHORT_DURATION)); + assertThat(m4.handleElapsedTimeLocked(SHORT_DURATION)).isEqualTo(HealthCheckState.PASSED); // Verify remains passed even if package expires - assertEquals(MonitoredPackage.STATE_PASSED, m4.handleElapsedTimeLocked(LONG_DURATION)); + assertThat(m4.handleElapsedTimeLocked(LONG_DURATION)).isEqualTo(HealthCheckState.PASSED); } @Test @@ -751,46 +793,356 @@ public class PackageWatchdogTest { mTestLooper.dispatchAll(); // Verify the NetworkStack observer is notified - assertEquals(1, observer.mFailedPackages.size()); - assertEquals(APP_A, observer.mFailedPackages.get(0)); + assertThat(observer.mMitigatedPackages).containsExactly(APP_A); } - /** Test that observers execute correctly for different failure reasons */ + /** Test default values are used when device property is invalid. */ @Test - public void testFailureReasons() { + public void testInvalidConfig_watchdogTriggerFailureCount() { + adoptShellPermissions( + Manifest.permission.WRITE_DEVICE_CONFIG, + Manifest.permission.READ_DEVICE_CONFIG); + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK, + PackageWatchdog.PROPERTY_WATCHDOG_TRIGGER_FAILURE_COUNT, + Integer.toString(-1), /*makeDefault*/false); + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer = new TestObserver(OBSERVER_NAME_1); + + watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION); + // Fail APP_A below the threshold which should not trigger package failures + for (int i = 0; i < PackageWatchdog.DEFAULT_TRIGGER_FAILURE_COUNT - 1; i++) { + watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + } + mTestLooper.dispatchAll(); + assertThat(observer.mHealthCheckFailedPackages).isEmpty(); + + // One more to trigger the package failure + watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + mTestLooper.dispatchAll(); + assertThat(observer.mHealthCheckFailedPackages).containsExactly(APP_A); + } + + /** Test default values are used when device property is invalid. */ + @Test + public void testInvalidConfig_watchdogTriggerDurationMillis() { + adoptShellPermissions( + Manifest.permission.WRITE_DEVICE_CONFIG, + Manifest.permission.READ_DEVICE_CONFIG); + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK, + PackageWatchdog.PROPERTY_WATCHDOG_TRIGGER_FAILURE_COUNT, + Integer.toString(2), /*makeDefault*/false); + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK, + PackageWatchdog.PROPERTY_WATCHDOG_TRIGGER_DURATION_MILLIS, + Integer.toString(-1), /*makeDefault*/false); + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer = new TestObserver(OBSERVER_NAME_1); + + watchdog.startObservingHealth(observer, Arrays.asList(APP_A, APP_B), Long.MAX_VALUE); + watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + mTestLooper.dispatchAll(); + moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_TRIGGER_FAILURE_DURATION_MS + 1); + watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + mTestLooper.dispatchAll(); + + // We shouldn't receive APP_A since the interval of 2 failures is greater than + // DEFAULT_TRIGGER_FAILURE_DURATION_MS. + assertThat(observer.mHealthCheckFailedPackages).isEmpty(); + + watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_B, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + mTestLooper.dispatchAll(); + moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_TRIGGER_FAILURE_DURATION_MS - 1); + watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_B, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + mTestLooper.dispatchAll(); + + // We should receive APP_B since the interval of 2 failures is less than + // DEFAULT_TRIGGER_FAILURE_DURATION_MS. + assertThat(observer.mHealthCheckFailedPackages).containsExactly(APP_B); + } + + /** + * Test default monitoring duration is used when PackageWatchdog#startObservingHealth is offered + * an invalid durationMs. + */ + @Test + public void testInvalidMonitoringDuration_beforeExpiry() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer = new TestObserver(OBSERVER_NAME_1); + + watchdog.startObservingHealth(observer, Arrays.asList(APP_A), -1); + // Note: Don't move too close to the expiration time otherwise the handler will be thrashed + // by PackageWatchdog#scheduleNextSyncStateLocked which keeps posting runnables with very + // small timeouts. + moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_OBSERVING_DURATION_MS - 100); + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + + // We should receive APP_A since the observer hasn't expired + assertThat(observer.mHealthCheckFailedPackages).containsExactly(APP_A); + } + + /** + * Test default monitoring duration is used when PackageWatchdog#startObservingHealth is offered + * an invalid durationMs. + */ + @Test + public void testInvalidMonitoringDuration_afterExpiry() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer = new TestObserver(OBSERVER_NAME_1); + + watchdog.startObservingHealth(observer, Arrays.asList(APP_A), -1); + moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_OBSERVING_DURATION_MS + 1); + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + + // We should receive nothing since the observer has expired + assertThat(observer.mHealthCheckFailedPackages).isEmpty(); + } + + /** Test we are notified when enough failures are triggered within any window. */ + @Test + public void testFailureTriggerWindow() { + adoptShellPermissions( + Manifest.permission.WRITE_DEVICE_CONFIG, + Manifest.permission.READ_DEVICE_CONFIG); + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK, + PackageWatchdog.PROPERTY_WATCHDOG_TRIGGER_FAILURE_COUNT, + Integer.toString(3), /*makeDefault*/false); + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK, + PackageWatchdog.PROPERTY_WATCHDOG_TRIGGER_DURATION_MILLIS, + Integer.toString(1000), /*makeDefault*/false); + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer = new TestObserver(OBSERVER_NAME_1); + + watchdog.startObservingHealth(observer, Arrays.asList(APP_A), Long.MAX_VALUE); + // Raise 2 failures at t=0 and t=900 respectively + watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + mTestLooper.dispatchAll(); + moveTimeForwardAndDispatch(900); + watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + mTestLooper.dispatchAll(); + + // Raise 2 failures at t=1100 + moveTimeForwardAndDispatch(200); + watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + mTestLooper.dispatchAll(); + + // We should receive APP_A since there are 3 failures within 1000ms window + assertThat(observer.mHealthCheckFailedPackages).containsExactly(APP_A); + } + + /** Test that observers execute correctly for failures reasons that go through thresholding. */ + @Test + public void testNonImmediateFailureReasons() { PackageWatchdog watchdog = createWatchdog(); TestObserver observer1 = new TestObserver(OBSERVER_NAME_1); TestObserver observer2 = new TestObserver(OBSERVER_NAME_2); - TestObserver observer3 = new TestObserver(OBSERVER_NAME_3); - TestObserver observer4 = new TestObserver(OBSERVER_NAME_4); watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); watchdog.startObservingHealth(observer2, Arrays.asList(APP_B), SHORT_DURATION); - watchdog.startObservingHealth(observer3, Arrays.asList(APP_C), SHORT_DURATION); - watchdog.startObservingHealth(observer4, Arrays.asList(APP_D), SHORT_DURATION); - for (int i = 0; i < watchdog.getTriggerFailureCount(); i++) { - watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), - PackageWatchdog.FAILURE_REASON_NATIVE_CRASH); - watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_B, VERSION_CODE)), - PackageWatchdog.FAILURE_REASON_EXPLICIT_HEALTH_CHECK); - watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_C, VERSION_CODE)), - PackageWatchdog.FAILURE_REASON_APP_CRASH); - watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_D, VERSION_CODE)), - PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING); + raiseFatalFailureAndDispatch(watchdog, Arrays.asList(new VersionedPackage(APP_A, + VERSION_CODE)), PackageWatchdog.FAILURE_REASON_APP_CRASH); + raiseFatalFailureAndDispatch(watchdog, Arrays.asList(new VersionedPackage(APP_B, + VERSION_CODE)), PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING); + + assertThat(observer1.getLastFailureReason()).isEqualTo( + PackageWatchdog.FAILURE_REASON_APP_CRASH); + assertThat(observer2.getLastFailureReason()).isEqualTo( + PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING); + } + + /** Test that observers execute correctly for failures reasons that skip thresholding. */ + @Test + public void testImmediateFailures() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer1 = new TestObserver(OBSERVER_NAME_1); + + watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); + + raiseFatalFailureAndDispatch(watchdog, Arrays.asList(new VersionedPackage(APP_A, + VERSION_CODE)), PackageWatchdog.FAILURE_REASON_NATIVE_CRASH); + raiseFatalFailureAndDispatch(watchdog, Arrays.asList(new VersionedPackage(APP_B, + VERSION_CODE)), PackageWatchdog.FAILURE_REASON_EXPLICIT_HEALTH_CHECK); + + assertThat(observer1.mMitigatedPackages).containsExactly(APP_A, APP_B); + } + + /** + * Test that a persistent observer will mitigate failures if it wishes to observe a package. + */ + @Test + public void testPersistentObserverWatchesPackage() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver persistentObserver = new TestObserver(OBSERVER_NAME_1); + persistentObserver.setPersistent(true); + persistentObserver.setMayObservePackages(true); + + watchdog.startObservingHealth(persistentObserver, Arrays.asList(APP_B), SHORT_DURATION); + + raiseFatalFailureAndDispatch(watchdog, Arrays.asList(new VersionedPackage(APP_A, + VERSION_CODE)), PackageWatchdog.FAILURE_REASON_UNKNOWN); + assertThat(persistentObserver.mHealthCheckFailedPackages).containsExactly(APP_A); + } + + /** + * Test that a persistent observer will not mitigate failures if it does not wish to observe + * a given package. + */ + @Test + public void testPersistentObserverDoesNotWatchPackage() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver persistentObserver = new TestObserver(OBSERVER_NAME_1); + persistentObserver.setPersistent(true); + persistentObserver.setMayObservePackages(false); + + watchdog.startObservingHealth(persistentObserver, Arrays.asList(APP_B), SHORT_DURATION); + + raiseFatalFailureAndDispatch(watchdog, Arrays.asList(new VersionedPackage(APP_A, + VERSION_CODE)), PackageWatchdog.FAILURE_REASON_UNKNOWN); + assertThat(persistentObserver.mHealthCheckFailedPackages).isEmpty(); + } + + + /** Ensure that boot loop mitigation is done when the number of boots meets the threshold. */ + @Test + public void testBootLoopDetection_meetsThreshold() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1); + watchdog.registerHealthObserver(bootObserver); + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) { + watchdog.noteBoot(); } + assertThat(bootObserver.mitigatedBootLoop()).isTrue(); + } - // Run handler so requests are dispatched to the controller + + /** + * Ensure that boot loop mitigation is not done when the number of boots does not meet the + * threshold. + */ + @Test + public void testBootLoopDetection_doesNotMeetThreshold() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1); + watchdog.registerHealthObserver(bootObserver); + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT - 1; i++) { + watchdog.noteBoot(); + } + assertThat(bootObserver.mitigatedBootLoop()).isFalse(); + } + + /** + * Ensure that boot loop mitigation is done for the observer with the lowest user impact + */ + @Test + public void testBootLoopMitigationDoneForLowestUserImpact() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver bootObserver1 = new TestObserver(OBSERVER_NAME_1); + bootObserver1.setImpact(PackageHealthObserverImpact.USER_IMPACT_LOW); + TestObserver bootObserver2 = new TestObserver(OBSERVER_NAME_2); + bootObserver2.setImpact(PackageHealthObserverImpact.USER_IMPACT_MEDIUM); + watchdog.registerHealthObserver(bootObserver1); + watchdog.registerHealthObserver(bootObserver2); + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) { + watchdog.noteBoot(); + } + assertThat(bootObserver1.mitigatedBootLoop()).isTrue(); + assertThat(bootObserver2.mitigatedBootLoop()).isFalse(); + } + + /** + * Ensure that passing a null list of failed packages does not cause any mitigation logic to + * execute. + */ + @Test + public void testNullFailedPackagesList() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer1 = new TestObserver(OBSERVER_NAME_1); + watchdog.startObservingHealth(observer1, List.of(APP_A), LONG_DURATION); + + raiseFatalFailureAndDispatch(watchdog, null, PackageWatchdog.FAILURE_REASON_APP_CRASH); + assertThat(observer1.mMitigatedPackages).isEmpty(); + } + + /** + * Test to verify that Package Watchdog syncs health check requests with the controller + * correctly, and that the requests are only synced when the set of observed packages + * changes. + */ + @Test + public void testSyncHealthCheckRequests() { + TestController testController = spy(TestController.class); + testController.setSupportedPackages(List.of(APP_A, APP_B, APP_C)); + PackageWatchdog watchdog = createWatchdog(testController, true); + + TestObserver testObserver1 = new TestObserver(OBSERVER_NAME_1); + watchdog.registerHealthObserver(testObserver1); + watchdog.startObservingHealth(testObserver1, List.of(APP_A), LONG_DURATION); + mTestLooper.dispatchAll(); + + TestObserver testObserver2 = new TestObserver(OBSERVER_NAME_2); + watchdog.registerHealthObserver(testObserver2); + watchdog.startObservingHealth(testObserver2, List.of(APP_B), LONG_DURATION); mTestLooper.dispatchAll(); - assertTrue(observer1.getLastFailureReason() - == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH); - assertTrue(observer2.getLastFailureReason() - == PackageWatchdog.FAILURE_REASON_EXPLICIT_HEALTH_CHECK); - assertTrue(observer3.getLastFailureReason() - == PackageWatchdog.FAILURE_REASON_APP_CRASH); - assertTrue(observer4.getLastFailureReason() - == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING); + TestObserver testObserver3 = new TestObserver(OBSERVER_NAME_3); + watchdog.registerHealthObserver(testObserver3); + watchdog.startObservingHealth(testObserver3, List.of(APP_C), LONG_DURATION); + mTestLooper.dispatchAll(); + + watchdog.unregisterHealthObserver(testObserver1); + mTestLooper.dispatchAll(); + + watchdog.unregisterHealthObserver(testObserver2); + mTestLooper.dispatchAll(); + + watchdog.unregisterHealthObserver(testObserver3); + mTestLooper.dispatchAll(); + + List<Set> expectedSyncRequests = List.of( + Set.of(APP_A), + Set.of(APP_A, APP_B), + Set.of(APP_A, APP_B, APP_C), + Set.of(APP_B, APP_C), + Set.of(APP_C), + Set.of() + ); + assertThat(testController.getSyncRequests()).isEqualTo(expectedSyncRequests); + } + + /** + * Ensure that the failure history of a package is preserved when making duplicate calls to + * observe the package. + */ + @Test + public void testFailureHistoryIsPreserved() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer = new TestObserver(OBSERVER_NAME_1); + watchdog.startObservingHealth(observer, List.of(APP_A), SHORT_DURATION); + for (int i = 0; i < PackageWatchdog.DEFAULT_TRIGGER_FAILURE_COUNT - 1; i++) { + watchdog.onPackageFailure(List.of(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + } + mTestLooper.dispatchAll(); + assertThat(observer.mMitigatedPackages).isEmpty(); + watchdog.startObservingHealth(observer, List.of(APP_A), LONG_DURATION); + watchdog.onPackageFailure(List.of(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + mTestLooper.dispatchAll(); + assertThat(observer.mMitigatedPackages).isEqualTo(List.of(APP_A)); } private void adoptShellPermissions(String... permissions) { @@ -819,6 +1171,26 @@ public class PackageWatchdogTest { } } + private void moveTimeForwardAndDispatch(long milliSeconds) { + mTestClock.moveTimeForward(milliSeconds); + mTestLooper.moveTimeForward(milliSeconds); + mTestLooper.dispatchAll(); + } + + /** Trigger package failures above the threshold. */ + private void raiseFatalFailureAndDispatch(PackageWatchdog watchdog, + List<VersionedPackage> packages, int failureReason) { + long triggerFailureCount = watchdog.getTriggerFailureCount(); + if (failureReason == PackageWatchdog.FAILURE_REASON_EXPLICIT_HEALTH_CHECK + || failureReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) { + triggerFailureCount = 1; + } + for (int i = 0; i < triggerFailureCount; i++) { + watchdog.onPackageFailure(packages, failureReason); + } + mTestLooper.dispatchAll(); + } + private PackageWatchdog createWatchdog() { return createWatchdog(new TestController(), true /* withPackagesReady */); } @@ -829,15 +1201,15 @@ public class PackageWatchdogTest { Handler handler = new Handler(mTestLooper.getLooper()); PackageWatchdog watchdog = new PackageWatchdog(mSpyContext, policyFile, handler, handler, controller, - mConnectivityModuleConnector); + mConnectivityModuleConnector, mTestClock); // Verify controller is not automatically started - assertFalse(controller.mIsEnabled); + assertThat(controller.mIsEnabled).isFalse(); 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); + assertThat(controller.mIsEnabled).isTrue(); verify(mConnectivityModuleConnector).registerHealthListener( mConnectivityModuleCallbackCaptor.capture()); @@ -849,7 +1221,11 @@ public class PackageWatchdogTest { private final String mName; private int mImpact; private int mLastFailureReason; - final List<String> mFailedPackages = new ArrayList<>(); + private boolean mIsPersistent = false; + private boolean mMayObservePackages = false; + private boolean mMitigatedBootLoop = false; + final List<String> mHealthCheckFailedPackages = new ArrayList<>(); + final List<String> mMitigatedPackages = new ArrayList<>(); TestObserver(String name) { mName = name; @@ -861,12 +1237,13 @@ public class PackageWatchdogTest { mImpact = impact; } - public int onHealthCheckFailed(VersionedPackage versionedPackage) { + public int onHealthCheckFailed(VersionedPackage versionedPackage, int failureReason) { + mHealthCheckFailedPackages.add(versionedPackage.getPackageName()); return mImpact; } public boolean execute(VersionedPackage versionedPackage, int failureReason) { - mFailedPackages.add(versionedPackage.getPackageName()); + mMitigatedPackages.add(versionedPackage.getPackageName()); mLastFailureReason = failureReason; return true; } @@ -875,9 +1252,42 @@ public class PackageWatchdogTest { return mName; } + public boolean isPersistent() { + return mIsPersistent; + } + + public boolean mayObservePackage(String packageName) { + return mMayObservePackages; + } + + public int onBootLoop() { + return mImpact; + } + + public boolean executeBootLoopMitigation() { + mMitigatedBootLoop = true; + return true; + } + + public boolean mitigatedBootLoop() { + return mMitigatedBootLoop; + } + public int getLastFailureReason() { return mLastFailureReason; } + + public void setPersistent(boolean persistent) { + mIsPersistent = persistent; + } + + public void setImpact(int impact) { + mImpact = impact; + } + + public void setMayObservePackages(boolean mayObservePackages) { + mMayObservePackages = mayObservePackages; + } } private static class TestController extends ExplicitHealthCheckController { @@ -891,6 +1301,7 @@ public class PackageWatchdogTest { private Consumer<String> mPassedConsumer; private Consumer<List<PackageConfig>> mSupportedConsumer; private Runnable mNotifySyncRunnable; + private List<Set> mSyncRequests = new ArrayList<>(); @Override public void setEnabled(boolean enabled) { @@ -910,6 +1321,7 @@ public class PackageWatchdogTest { @Override public void syncRequests(Set<String> packages) { + mSyncRequests.add(packages); mRequestedPackages.clear(); if (mIsEnabled) { packages.retainAll(mSupportedPackages); @@ -940,5 +1352,22 @@ public class PackageWatchdogTest { return Collections.emptyList(); } } + + public List<Set> getSyncRequests() { + return mSyncRequests; + } + } + + private static class TestClock implements PackageWatchdog.SystemClock { + // Note 0 is special to the internal clock of PackageWatchdog. We need to start from + // a non-zero value in order not to disrupt the logic of PackageWatchdog. + private long mUpTimeMillis = 1; + @Override + public long uptimeMillis() { + return mUpTimeMillis; + } + public void moveTimeForward(long milliSeconds) { + mUpTimeMillis += milliSeconds; + } } } diff --git a/tests/PlatformCompatGating/Android.bp b/tests/PlatformCompatGating/Android.bp index 342c47de755a..7d918cc4c18b 100644 --- a/tests/PlatformCompatGating/Android.bp +++ b/tests/PlatformCompatGating/Android.bp @@ -17,7 +17,10 @@ android_test { name: "PlatformCompatGating", // Only compile source java files in this apk. - srcs: ["src/**/*.java"], + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], test_suites: ["device-tests"], static_libs: [ "junit", @@ -25,7 +28,8 @@ android_test { "androidx.test.core", "androidx.test.ext.junit", "mockito-target-minus-junit4", + "testng", "truth-prebuilt", - "platform-compat-test-rules" + "platform-compat-test-rules", ], } diff --git a/tests/PlatformCompatGating/AndroidManifest.xml b/tests/PlatformCompatGating/AndroidManifest.xml index c24dc31b7bf3..0510db2e52aa 100644 --- a/tests/PlatformCompatGating/AndroidManifest.xml +++ b/tests/PlatformCompatGating/AndroidManifest.xml @@ -2,6 +2,11 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.tests.gating"> + + <queries> + <package android:name="com.android.tests.gating.app_not_installed" /> + </queries> + <application android:label="GatingTest"> <uses-library android:name="android.test.runner" /> </application> diff --git a/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatCommandNotInstalledTest.kt b/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatCommandNotInstalledTest.kt new file mode 100644 index 000000000000..0f62c4fa66a3 --- /dev/null +++ b/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatCommandNotInstalledTest.kt @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2020 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.gating + +import android.Manifest +import android.app.UiAutomation +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.parsing.result.ParseInput +import android.os.Build +import android.os.ParcelFileDescriptor +import android.os.ServiceManager +import androidx.test.platform.app.InstrumentationRegistry +import com.android.internal.compat.IPlatformCompat +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Assume.assumeTrue +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.testng.Assert.assertThrows +import java.io.FileReader + +/** + * Verifies the shell commands "am compat enable/disable/reset" against a real server change ID + * for a not installed package. + * + * This class intentionally does not use any PlatformCompat testing infrastructure since that could + * interfere with what it's testing. + */ +@RunWith(Parameterized::class) +class PlatformCompatCommandNotInstalledTest { + + companion object { + + private const val TEST_PKG = "com.android.tests.gating.app_not_installed" + private const val TEST_CHANGE_ID = ParseInput.DeferredError.MISSING_APP_TAG + + private val instrumentation = InstrumentationRegistry.getInstrumentation() + + @JvmStatic + @BeforeClass + fun assumeDebuggable() { + assumeTrue(Build.IS_DEBUGGABLE) + } + + @JvmStatic + @BeforeClass + fun assertNotInstalled() { + assertThrows(PackageManager.NameNotFoundException::class.java) { + instrumentation.context.packageManager + .getApplicationInfo(TEST_PKG, PackageManager.MATCH_ALL) + } + } + + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun parameters() = arrayOf( + Params(enableDisable = null, targetSdk = 29, result = false), + Params(enableDisable = null, targetSdk = 30, result = true), + + Params(enableDisable = true, targetSdk = 29, result = true), + Params(enableDisable = true, targetSdk = 30, result = true), + + Params(enableDisable = false, targetSdk = 29, result = false), + Params(enableDisable = false, targetSdk = 30, result = false) + ) + } + + data class Params(val enableDisable: Boolean?, val targetSdk: Int, val result: Boolean) + + @Parameterized.Parameter(0) + lateinit var params: Params + + private val uiAutomation: UiAutomation = instrumentation.getUiAutomation() + private val platformCompat = IPlatformCompat.Stub.asInterface( + ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)) + + @Before + fun resetChangeId() { + uiAutomation.adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE, + Manifest.permission.OVERRIDE_COMPAT_CHANGE_CONFIG, + Manifest.permission.READ_COMPAT_CHANGE_CONFIG) + + val result = command("am compat reset $TEST_CHANGE_ID $TEST_PKG") + assertThat(result.startsWith("Reset change") || result.startsWith("No override")) + .isTrue() + } + + fun ParcelFileDescriptor.text() = FileReader(fileDescriptor).readText() + + @After + fun resetIdentity() = uiAutomation.dropShellPermissionIdentity() + + @Test + fun execute() { + when (params.enableDisable) { + null -> { /* do nothing */ + } + true -> assertThat(command("am compat enable $TEST_CHANGE_ID $TEST_PKG")) + .startsWith("Enabled change") + false -> assertThat(command("am compat disable $TEST_CHANGE_ID $TEST_PKG")) + .startsWith("Disabled change") + } + + val appInfo = ApplicationInfo().apply { + this.packageName = TEST_PKG + this.targetSdkVersion = params.targetSdk + } + + assertThat(platformCompat.isChangeEnabled(TEST_CHANGE_ID, appInfo)).isEqualTo(params.result) + } + + private fun command(command: String) = + FileReader(uiAutomation.executeShellCommand(command).fileDescriptor).readText() +} diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamBoolTest.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamBoolTest.java index c21c4033a0ff..3415d2e13974 100644 --- a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamBoolTest.java +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamBoolTest.java @@ -384,55 +384,55 @@ public class ProtoInputStreamBoolTest extends TestCase { }; ProtoInputStream pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readFloat(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readDouble(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readInt(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readLong(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBytes(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readString(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamBytesTest.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamBytesTest.java index 09fe40edda6c..8796807c0521 100644 --- a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamBytesTest.java +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamBytesTest.java @@ -306,55 +306,55 @@ public class ProtoInputStreamBytesTest extends TestCase { }; ProtoInputStream pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readFloat(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readDouble(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readInt(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readLong(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBoolean(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readString(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamDoubleTest.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamDoubleTest.java index 118fe3431e01..2b54e960eabb 100644 --- a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamDoubleTest.java +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamDoubleTest.java @@ -611,55 +611,55 @@ public class ProtoInputStreamDoubleTest extends TestCase { }; ProtoInputStream pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readFloat(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBoolean(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readInt(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readLong(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBytes(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readString(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamEnumTest.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamEnumTest.java index f55d95129588..19bad7099622 100644 --- a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamEnumTest.java +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamEnumTest.java @@ -454,55 +454,55 @@ public class ProtoInputStreamEnumTest extends TestCase { }; ProtoInputStream pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readFloat(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readDouble(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBoolean(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readLong(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBytes(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readString(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamFixed32Test.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamFixed32Test.java index df68476f0c36..2bc61a0c7e8a 100644 --- a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamFixed32Test.java +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamFixed32Test.java @@ -431,55 +431,55 @@ public class ProtoInputStreamFixed32Test extends TestCase { }; ProtoInputStream pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readFloat(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readDouble(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBoolean(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readLong(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBytes(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readString(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamFixed64Test.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamFixed64Test.java index af4130b28cd8..a54ecf99d62c 100644 --- a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamFixed64Test.java +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamFixed64Test.java @@ -532,55 +532,55 @@ public class ProtoInputStreamFixed64Test extends TestCase { }; ProtoInputStream pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readFloat(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readDouble(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readInt(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBoolean(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBytes(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readString(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamFloatTest.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamFloatTest.java index 9bc07dc513e1..0477e9ea74e0 100644 --- a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamFloatTest.java +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamFloatTest.java @@ -563,55 +563,55 @@ public class ProtoInputStreamFloatTest extends TestCase { }; ProtoInputStream pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBoolean(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readDouble(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readInt(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readLong(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBytes(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readString(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamIncompleteValueTest.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamIncompleteValueTest.java new file mode 100644 index 000000000000..167d5a438302 --- /dev/null +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamIncompleteValueTest.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2020 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.ProtoParseException; +import android.util.proto.ProtoStream; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class ProtoInputStreamIncompleteValueTest extends TestCase { + + /** + * Test that an incomplete varint at the end of a stream throws an exception + */ + public void testIncompleteVarint() 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 : varint -> invalid varint value + (byte) 0x08, + (byte) 0xff, + }; + + 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 ProtoParseException"); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } catch (ProtoParseException ppe) { + // good + stream.close(); + return; + } + } + stream.close(); + fail("Test should not have reached this point..."); + } + + /** + * Test that an incomplete fixed64 at the end of a stream throws an exception + */ + public void testIncompleteFixed64() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_FIXED64; + + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 2 -> invalid fixed64 + (byte) 0x11, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (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) fieldId2: + pi.readLong(fieldId2); + fail("Should have thrown a ProtoParseException"); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } catch (ProtoParseException ppe) { + // good + stream.close(); + return; + } + } + stream.close(); + fail("Test should not have reached this point..."); + } + + /** + * Test that an incomplete length delimited value at the end of a stream throws an exception + */ + public void testIncompleteLengthDelimited() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_BYTES; + + final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 5 -> invalid byte array (has size 5 but only 4 values) + (byte) 0x2a, + (byte) 0x05, + (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03, + }; + + InputStream stream = new ByteArrayInputStream(protobuf); + final ProtoInputStream pi = new ProtoInputStream(stream); + + while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + try { + switch (pi.getFieldNumber()) { + case (int) fieldId5: + pi.readBytes(fieldId5); + fail("Should have thrown a ProtoParseException"); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } catch (ProtoParseException ppe) { + // good + stream.close(); + return; + } + } + stream.close(); + fail("Test should not have reached this point..."); + } + + /** + * Test that an incomplete fixed32 at the end of a stream throws an exception + */ + public void testIncompleteFixed32() throws IOException { + final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_FIXED32; + + final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL); + + final byte[] protobuf = new byte[]{ + // 2 -> invalid fixed32 + (byte) 0x15, + (byte) 0x01, (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) fieldId2: + pi.readInt(fieldId2); + fail("Should have thrown a ProtoParseException"); + break; + default: + fail("Unexpected field id " + pi.getFieldNumber()); + } + } catch (ProtoParseException ppe) { + // good + stream.close(); + return; + } + } + stream.close(); + fail("Test should not have reached this point..."); + } +} diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamInt32Test.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamInt32Test.java index 0065870486f2..a7f3f65e504d 100644 --- a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamInt32Test.java +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamInt32Test.java @@ -449,55 +449,55 @@ public class ProtoInputStreamInt32Test extends TestCase { }; ProtoInputStream pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readFloat(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readDouble(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBoolean(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readLong(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBytes(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readString(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamInt64Test.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamInt64Test.java index 4d6d105e60b0..dc42468e0605 100644 --- a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamInt64Test.java +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamInt64Test.java @@ -529,55 +529,55 @@ public class ProtoInputStreamInt64Test extends TestCase { }; ProtoInputStream pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readFloat(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readDouble(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readInt(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBoolean(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBytes(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readString(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamObjectTest.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamObjectTest.java index 5e49eeafb8af..1c0832e3e676 100644 --- a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamObjectTest.java +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamObjectTest.java @@ -391,55 +391,55 @@ public class ProtoInputStreamObjectTest extends TestCase { }; ProtoInputStream pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readFloat(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readDouble(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readInt(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readLong(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBoolean(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readString(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSFixed32Test.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSFixed32Test.java index 75c88a44614b..d349ea2baa67 100644 --- a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSFixed32Test.java +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSFixed32Test.java @@ -431,55 +431,55 @@ public class ProtoInputStreamSFixed32Test extends TestCase { }; ProtoInputStream pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readFloat(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readDouble(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBoolean(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readLong(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBytes(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readString(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSFixed64Test.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSFixed64Test.java index 4c65cf49318d..81a9c591b32e 100644 --- a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSFixed64Test.java +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSFixed64Test.java @@ -531,55 +531,55 @@ public class ProtoInputStreamSFixed64Test extends TestCase { }; ProtoInputStream pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readFloat(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readDouble(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readInt(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBoolean(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBytes(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readString(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSInt32Test.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSInt32Test.java index 6854cd8aad28..97194444deb2 100644 --- a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSInt32Test.java +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSInt32Test.java @@ -431,55 +431,55 @@ public class ProtoInputStreamSInt32Test extends TestCase { }; ProtoInputStream pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readFloat(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readDouble(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBoolean(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readLong(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBytes(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readString(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSInt64Test.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSInt64Test.java index c53e9d72562a..118476cdf235 100644 --- a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSInt64Test.java +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamSInt64Test.java @@ -506,55 +506,55 @@ public class ProtoInputStreamSInt64Test extends TestCase { }; ProtoInputStream pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readFloat(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readDouble(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readInt(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBoolean(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBytes(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readString(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamStringTest.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamStringTest.java index 816d5f900a3d..51ee78f32767 100644 --- a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamStringTest.java +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamStringTest.java @@ -287,55 +287,55 @@ public class ProtoInputStreamStringTest extends TestCase { }; ProtoInputStream pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readFloat(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readDouble(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readInt(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readLong(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBytes(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBoolean(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamUInt32Test.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamUInt32Test.java index 50fc537767a4..42f3e991f6e8 100644 --- a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamUInt32Test.java +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamUInt32Test.java @@ -448,55 +448,55 @@ public class ProtoInputStreamUInt32Test extends TestCase { }; ProtoInputStream pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readFloat(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readDouble(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBoolean(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readLong(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBytes(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readString(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamUInt64Test.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamUInt64Test.java index 20969e9056a9..8ba2c0ccaca9 100644 --- a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamUInt64Test.java +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoInputStreamUInt64Test.java @@ -525,55 +525,55 @@ public class ProtoInputStreamUInt64Test extends TestCase { }; ProtoInputStream pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readFloat(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readDouble(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readInt(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBoolean(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readBytes(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } pi = new ProtoInputStream(protobuf); - pi.isNextField(fieldId1); + pi.nextField(); try { pi.readString(fieldId1); - fail("Should have throw IllegalArgumentException"); + fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException iae) { // good } diff --git a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoTests.java b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoTests.java index cdf6ae20f370..685110c08147 100644 --- a/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoTests.java +++ b/tests/ProtoInputStreamTests/src/com/android/test/protoinputstream/ProtoTests.java @@ -39,6 +39,7 @@ public class ProtoTests { suite.addTestSuite(ProtoInputStreamBytesTest.class); suite.addTestSuite(ProtoInputStreamEnumTest.class); suite.addTestSuite(ProtoInputStreamObjectTest.class); + suite.addTestSuite(ProtoInputStreamIncompleteValueTest.class); return suite; } diff --git a/tests/RollbackTest/Android.bp b/tests/RollbackTest/Android.bp index 231d045bd817..2be4ae6bb214 100644 --- a/tests/RollbackTest/Android.bp +++ b/tests/RollbackTest/Android.bp @@ -19,15 +19,29 @@ android_test { static_libs: ["androidx.test.rules", "cts-rollback-lib", "cts-install-lib"], test_suites: ["general-tests"], test_config: "RollbackTest.xml", - // TODO: sdk_version: "test_current" when Intent#resolveSystemservice is TestApi + java_resources: [ + ":com.android.apex.apkrollback.test_v2", + ":com.android.apex.apkrollback.test_v2Crashing" + ], } java_test_host { name: "StagedRollbackTest", srcs: ["StagedRollbackTest/src/**/*.java"], libs: ["tradefed"], + static_libs: ["testng", "compatibility-tradefed", "RollbackTestLib"], test_suites: ["general-tests"], test_config: "StagedRollbackTest.xml", + data: [":com.android.apex.apkrollback.test_v1"], +} + +java_test_host { + name: "NetworkStagedRollbackTest", + srcs: ["NetworkStagedRollbackTest/src/**/*.java"], + libs: ["tradefed"], + static_libs: ["RollbackTestLib"], + test_suites: ["general-tests"], + test_config: "NetworkStagedRollbackTest.xml", } java_test_host { @@ -37,3 +51,60 @@ java_test_host { test_suites: ["general-tests"], test_config: "MultiUserRollbackTest.xml", } + +java_library_host { + name: "RollbackTestLib", + srcs: ["lib/src/**/*.java"], + libs: ["tradefed"], +} + +genrule { + name: "com.android.apex.apkrollback.test.pem", + out: ["com.android.apex.apkrollback.test.pem"], + cmd: "openssl genrsa -out $(out) 4096", +} + +genrule { + name: "com.android.apex.apkrollback.test.pubkey", + srcs: [":com.android.apex.apkrollback.test.pem"], + out: ["com.android.apex.apkrollback.test.pubkey"], + tools: ["avbtool"], + cmd: "$(location avbtool) extract_public_key --key $(in) --output $(out)", +} + +apex_key { + name: "com.android.apex.apkrollback.test.key", + private_key: ":com.android.apex.apkrollback.test.pem", + public_key: ":com.android.apex.apkrollback.test.pubkey", + installable: false, +} + +apex { + name: "com.android.apex.apkrollback.test_v1", + manifest: "testdata/manifest_v1.json", + androidManifest: "testdata/AndroidManifest.xml", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.apkrollback.test.key", + apps: ["TestAppAv1"], + installable: false, +} + +apex { + name: "com.android.apex.apkrollback.test_v2", + manifest: "testdata/manifest_v2.json", + androidManifest: "testdata/AndroidManifest.xml", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.apkrollback.test.key", + apps: ["TestAppAv2"], + installable: false, +} + +apex { + name: "com.android.apex.apkrollback.test_v2Crashing", + manifest: "testdata/manifest_v2.json", + androidManifest: "testdata/AndroidManifest.xml", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.apkrollback.test.key", + apps: ["TestAppACrashingV2"], + installable: false, +}
\ No newline at end of file diff --git a/tests/RollbackTest/MultiUserRollbackTest.xml b/tests/RollbackTest/MultiUserRollbackTest.xml index 41cec461c377..ba86c3ff6777 100644 --- a/tests/RollbackTest/MultiUserRollbackTest.xml +++ b/tests/RollbackTest/MultiUserRollbackTest.xml @@ -15,9 +15,6 @@ --> <configuration description="Runs rollback tests for multiple users"> <option name="test-suite-tag" value="MultiUserRollbackTest" /> - <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> - <option name="run-command" value="pm uninstall com.android.cts.install.lib.testapp.A" /> - </target_preparer> <test class="com.android.tradefed.testtype.HostTest" > <option name="class" value="com.android.tests.rollback.host.MultiUserRollbackTest" /> </test> diff --git a/tests/RollbackTest/MultiUserRollbackTest/src/com/android/tests/rollback/host/MultiUserRollbackTest.java b/tests/RollbackTest/MultiUserRollbackTest/src/com/android/tests/rollback/host/MultiUserRollbackTest.java index 52f6eba4072b..a4c81d577522 100644 --- a/tests/RollbackTest/MultiUserRollbackTest/src/com/android/tests/rollback/host/MultiUserRollbackTest.java +++ b/tests/RollbackTest/MultiUserRollbackTest/src/com/android/tests/rollback/host/MultiUserRollbackTest.java @@ -27,6 +27,8 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.concurrent.TimeUnit; + /** * Runs rollback tests for multiple users. */ @@ -38,20 +40,23 @@ public class MultiUserRollbackTest extends BaseHostJUnit4Test { private static final long SWITCH_USER_COMPLETED_NUMBER_OF_POLLS = 60; private static final long SWITCH_USER_COMPLETED_POLL_INTERVAL_IN_MILLIS = 1000; + private void cleanUp() throws Exception { + getDevice().executeShellCommand("pm rollback-app com.android.cts.install.lib.testapp.A"); + getDevice().executeShellCommand("pm uninstall com.android.cts.install.lib.testapp.A"); + } @After public void tearDown() throws Exception { - getDevice().switchUser(mOriginalUserId); - getDevice().executeShellCommand("pm uninstall com.android.cts.install.lib.testapp.A"); + cleanUp(); removeSecondaryUserIfNecessary(); } @Before public void setup() throws Exception { + cleanUp(); mOriginalUserId = getDevice().getCurrentUser(); - installPackageAsUser("RollbackTest.apk", true, mOriginalUserId); - createAndSwitchToSecondaryUserIfNecessary(); - installPackageAsUser("RollbackTest.apk", true, mSecondaryUserId); + createAndStartSecondaryUser(); + installPackage("RollbackTest.apk", "--user all"); } @Test @@ -64,7 +69,6 @@ public class MultiUserRollbackTest extends BaseHostJUnit4Test { runPhaseForUsers("testMultipleUsersInstallV1", mOriginalUserId, mSecondaryUserId); runPhaseForUsers("testMultipleUsersUpgradeToV2", mOriginalUserId); runPhaseForUsers("testMultipleUsersUpdateUserData", mOriginalUserId, mSecondaryUserId); - switchToUser(mOriginalUserId); getDevice().executeShellCommand("pm rollback-app com.android.cts.install.lib.testapp.A"); runPhaseForUsers("testMultipleUsersVerifyUserdataRollback", mOriginalUserId, mSecondaryUserId); @@ -74,11 +78,11 @@ public class MultiUserRollbackTest extends BaseHostJUnit4Test { * Run the phase for the given user ids, in the order they are given. */ private void runPhaseForUsers(String phase, int... userIds) throws Exception { + final long timeout = TimeUnit.MINUTES.toMillis(10); for (int userId: userIds) { - switchToUser(userId); - assertTrue(runDeviceTests("com.android.tests.rollback", + assertTrue(runDeviceTests(getDevice(), "com.android.tests.rollback", "com.android.tests.rollback.MultiUserRollbackTest", - phase)); + phase, userId, timeout)); } } @@ -89,21 +93,7 @@ public class MultiUserRollbackTest extends BaseHostJUnit4Test { } } - private void createAndSwitchToSecondaryUserIfNecessary() throws Exception { - if (mSecondaryUserId == -1) { - mOriginalUserId = getDevice().getCurrentUser(); - mSecondaryUserId = getDevice().createUser("MultiUserRollbackTest_User" - + System.currentTimeMillis()); - switchToUser(mSecondaryUserId); - } - } - - private void switchToUser(int userId) throws Exception { - if (getDevice().getCurrentUser() == userId) { - return; - } - - assertTrue(getDevice().switchUser(userId)); + private void awaitUserUnlocked(int userId) throws Exception { for (int i = 0; i < SWITCH_USER_COMPLETED_NUMBER_OF_POLLS; ++i) { String userState = getDevice().executeShellCommand("am get-started-user-state " + userId); @@ -112,6 +102,14 @@ public class MultiUserRollbackTest extends BaseHostJUnit4Test { } Thread.sleep(SWITCH_USER_COMPLETED_POLL_INTERVAL_IN_MILLIS); } - fail("User switch to user " + userId + " timed out"); + fail("Timed out in unlocking user: " + userId); + } + + private void createAndStartSecondaryUser() throws Exception { + String name = "MultiUserRollbackTest_User" + System.currentTimeMillis(); + mSecondaryUserId = getDevice().createUser(name); + getDevice().startUser(mSecondaryUserId); + // Note we can't install apps on a locked user + awaitUserUnlocked(mSecondaryUserId); } } diff --git a/tests/RollbackTest/NetworkStagedRollbackTest.xml b/tests/RollbackTest/NetworkStagedRollbackTest.xml new file mode 100644 index 000000000000..2ab907a59298 --- /dev/null +++ b/tests/RollbackTest/NetworkStagedRollbackTest.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2020 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 network staged rollback tests"> + <option name="test-suite-tag" value="NetworkStagedRollbackTest" /> + <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> + <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> + <option name="run-command" value="am broadcast -a 'com.google.android.gms.phenotype.FLAG_OVERRIDE' --es package "com.google.android.gms.platformconfigurator" --es user '\\*' --esa flags "ModuleConfig__immediate_commit_packages" --esa types "bytes" --esa values "CgA=" com.google.android.gms" /> + <option name="run-command" value="am broadcast -a 'com.google.android.gms.phenotype.FLAG_OVERRIDE' --es package "com.google.android.gms.platformconfigurator" --es user '\\*' --esa flags "ModuleConfig__versioned_immediate_commit_packages" --esa types "bytes" --esa values "Cm5vdGFwYWNrYWdlOgA=" com.google.android.gms" /> + <option name="teardown-command" value="am broadcast -a 'com.google.android.gms.phenotype.FLAG_OVERRIDE' --es action delete --es package "com.google.android.gms.platformconfigurator" --es user '\*' --esa flag "ModuleConfig__immediate_commit_packages" com.google.android.gms" /> + <option name="teardown-command" value="am broadcast -a 'com.google.android.gms.phenotype.FLAG_OVERRIDE' --es action delete --es package "com.google.android.gms.platformconfigurator" --es user '\*' --esa flag "ModuleConfig__versioned_immediate_commit_packages" com.google.android.gms" /> + </target_preparer> + <test class="com.android.tradefed.testtype.HostTest" > + <option name="class" value="com.android.tests.rollback.host.NetworkStagedRollbackTest" /> + </test> +</configuration> diff --git a/tests/RollbackTest/NetworkStagedRollbackTest/src/com/android/tests/rollback/host/NetworkStagedRollbackTest.java b/tests/RollbackTest/NetworkStagedRollbackTest/src/com/android/tests/rollback/host/NetworkStagedRollbackTest.java new file mode 100644 index 000000000000..57c52f9c3021 --- /dev/null +++ b/tests/RollbackTest/NetworkStagedRollbackTest/src/com/android/tests/rollback/host/NetworkStagedRollbackTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2020 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 com.android.tests.rollback.host.WatchdogEventLogger.watchdogEventOccurred; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +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; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Runs the network rollback tests. + */ +@RunWith(DeviceJUnit4ClassRunner.class) +public class NetworkStagedRollbackTest 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.NetworkStagedRollbackTest", + phase)); + } + + private static final String REASON_EXPLICIT_HEALTH_CHECK = "REASON_EXPLICIT_HEALTH_CHECK"; + + private static final String ROLLBACK_INITIATE = "ROLLBACK_INITIATE"; + private static final String ROLLBACK_BOOT_TRIGGERED = "ROLLBACK_BOOT_TRIGGERED"; + private static final String ROLLBACK_SUCCESS = "ROLLBACK_SUCCESS"; + + private WatchdogEventLogger mLogger = new WatchdogEventLogger(); + + @Before + public void setUp() throws Exception { + mLogger.start(getDevice()); + } + + @After + public void tearDown() throws Exception { + mLogger.stop(); + } + + /** + * Tests failed network health check triggers watchdog staged rollbacks. + */ + @Test + public void testNetworkFailedRollback() throws Exception { + try { + // Disconnect internet so we can test network health triggered rollbacks + getDevice().executeShellCommand("svc wifi disable"); + getDevice().executeShellCommand("svc data disable"); + + runPhase("testNetworkFailedRollback_Phase1"); + // Reboot device to activate staged package + getDevice().reboot(); + + // Verify rollback was enabled + runPhase("testNetworkFailedRollback_Phase2"); + // Wait for reboot to happen + assertTrue(getDevice().waitForDeviceNotAvailable(TimeUnit.MINUTES.toMillis(5))); + // Wait for reboot to complete and device to become available + getDevice().waitForDeviceAvailable(); + // Verify rollback was executed after health check deadline + runPhase("testNetworkFailedRollback_Phase3"); + + List<String> watchdogEvents = mLogger.getWatchdogLoggingEvents(); + assertTrue(watchdogEventOccurred(watchdogEvents, ROLLBACK_INITIATE, null, + REASON_EXPLICIT_HEALTH_CHECK, null)); + assertTrue(watchdogEventOccurred(watchdogEvents, ROLLBACK_BOOT_TRIGGERED, null, + null, null)); + assertTrue(watchdogEventOccurred(watchdogEvents, ROLLBACK_SUCCESS, null, null, null)); + } finally { + // Reconnect internet again so we won't break tests which assume internet available + getDevice().executeShellCommand("svc wifi enable"); + getDevice().executeShellCommand("svc data enable"); + } + } + + /** + * Tests passed network health check does not trigger watchdog staged rollbacks. + */ + @Test + public void testNetworkPassedDoesNotRollback() throws Exception { + runPhase("testNetworkPassedDoesNotRollback_Phase1"); + // Reboot device to activate staged package + getDevice().reboot(); + + // Verify rollback was enabled + runPhase("testNetworkPassedDoesNotRollback_Phase2"); + + // 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(); + + // Verify rollback was not executed after health check deadline + runPhase("testNetworkPassedDoesNotRollback_Phase3"); + + List<String> watchdogEvents = mLogger.getWatchdogLoggingEvents(); + assertEquals(watchdogEventOccurred(watchdogEvents, null, null, + REASON_EXPLICIT_HEALTH_CHECK, null), false); + } +} diff --git a/tests/RollbackTest/RollbackTest.xml b/tests/RollbackTest/RollbackTest.xml index a14b01c57b1b..7b85cc84f1f5 100644 --- a/tests/RollbackTest/RollbackTest.xml +++ b/tests/RollbackTest/RollbackTest.xml @@ -18,13 +18,23 @@ <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> <option name="test-file-name" value="RollbackTest.apk" /> </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> + <option name="run-command" value="am broadcast -a 'com.google.android.gms.phenotype.FLAG_OVERRIDE' --es package "com.google.android.gms.platformconfigurator" --es user '\\*' --esa flags "ModuleConfig__immediate_commit_packages" --esa types "bytes" --esa values "CgA=" com.google.android.gms" /> + <option name="run-command" value="am broadcast -a 'com.google.android.gms.phenotype.FLAG_OVERRIDE' --es package "com.google.android.gms.platformconfigurator" --es user '\\*' --esa flags "ModuleConfig__versioned_immediate_commit_packages" --esa types "bytes" --esa values "Cm5vdGFwYWNrYWdlOgA=" com.google.android.gms" /> + <option name="teardown-command" value="am broadcast -a 'com.google.android.gms.phenotype.FLAG_OVERRIDE' --es action delete --es package "com.google.android.gms.platformconfigurator" --es user '\*' --esa flag "ModuleConfig__immediate_commit_packages" com.google.android.gms" /> + <option name="teardown-command" value="am broadcast -a 'com.google.android.gms.phenotype.FLAG_OVERRIDE' --es action delete --es package "com.google.android.gms.platformconfigurator" --es user '\*' --esa flag "ModuleConfig__versioned_immediate_commit_packages" com.google.android.gms" /> + <option name="run-command" value="pm uninstall com.android.cts.install.lib.testapp.A" /> + <option name="run-command" value="pm uninstall com.android.cts.install.lib.testapp.B" /> + <option name="teardown-command" value="pm uninstall com.android.cts.install.lib.testapp.A" /> + <option name="teardown-command" value="pm uninstall com.android.cts.install.lib.testapp.B" /> + </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 and MultiUserRollbackTest tests, which need to be - specially driven from the StagedRollbackTest and MultiUserRollbackTest host test --> + <!-- Exclude the device tests which need to be specially driven from the host tests --> <option name="exclude-filter" value="com.android.tests.rollback.StagedRollbackTest" /> + <option name="exclude-filter" value="com.android.tests.rollback.NetworkStagedRollbackTest" /> <option name="exclude-filter" value="com.android.tests.rollback.MultiUserRollbackTest" /> </test> </configuration> diff --git a/tests/RollbackTest/RollbackTest/AndroidManifest.xml b/tests/RollbackTest/RollbackTest/AndroidManifest.xml index 2b8c96484210..9274da268735 100644 --- a/tests/RollbackTest/RollbackTest/AndroidManifest.xml +++ b/tests/RollbackTest/RollbackTest/AndroidManifest.xml @@ -17,6 +17,7 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.tests.rollback" > + <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <application> <receiver android:name="com.android.cts.install.lib.LocalIntentSender" android:exported="true" /> diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/MultiUserRollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/MultiUserRollbackTest.java index 0ffe041b0377..400bb04f0fab 100644 --- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/MultiUserRollbackTest.java +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/MultiUserRollbackTest.java @@ -17,13 +17,11 @@ package com.android.tests.rollback; import static com.android.cts.rollback.lib.RollbackInfoSubject.assertThat; -import static com.android.cts.rollback.lib.RollbackUtils.getUniqueRollbackInfoForPackage; import static com.google.common.truth.Truth.assertThat; import android.Manifest; import android.content.rollback.RollbackInfo; -import android.content.rollback.RollbackManager; import com.android.cts.install.lib.Install; import com.android.cts.install.lib.InstallUtils; @@ -77,13 +75,10 @@ public class MultiUserRollbackTest { */ @Test public void testMultipleUsersUpgradeToV2() throws Exception { - RollbackManager rm = RollbackUtils.getRollbackManager(); assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1); Install.single(TestApp.A2).setEnableRollback().commit(); assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2); - RollbackInfo rollback = getUniqueRollbackInfoForPackage( - rm.getAvailableRollbacks(), TestApp.A); - assertThat(rollback).isNotNull(); + RollbackInfo rollback = RollbackUtils.waitForAvailableRollback(TestApp.A); assertThat(rollback).packagesContainsExactly( Rollback.from(TestApp.A2).to(TestApp.A1)); } diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/NetworkStagedRollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/NetworkStagedRollbackTest.java new file mode 100644 index 000000000000..8fb59c7c3e2c --- /dev/null +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/NetworkStagedRollbackTest.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2020 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.cts.rollback.lib.RollbackInfoSubject.assertThat; +import static com.android.cts.rollback.lib.RollbackUtils.getUniqueRollbackInfoForPackage; + +import android.Manifest; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.rollback.RollbackManager; +import android.os.ParcelFileDescriptor; +import android.provider.DeviceConfig; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.cts.install.lib.Install; +import com.android.cts.install.lib.InstallUtils; +import com.android.cts.install.lib.TestApp; +import com.android.cts.rollback.lib.RollbackUtils; + +import libcore.io.IoUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.File; +import java.util.concurrent.TimeUnit; + +@RunWith(JUnit4.class) +public class NetworkStagedRollbackTest { + private static final String NETWORK_STACK_CONNECTOR_CLASS = + "android.net.INetworkStackConnector"; + private static final String PROPERTY_WATCHDOG_REQUEST_TIMEOUT_MILLIS = + "watchdog_request_timeout_millis"; + + private static final String[] NETWORK_STACK_APK_NAMES = { + "NetworkStack", "NetworkStackGoogle", "NetworkStackNext", "NetworkStackNextGoogle" + }; + + private static final TestApp NETWORK_STACK = new TestApp("NetworkStack", + getNetworkStackPackageName(), -1, false, findNetworkStackApk()); + + private static File findNetworkStackApk() { + for (String name : NETWORK_STACK_APK_NAMES) { + final File apk = new File("/system/priv-app/" + name + "/" + name + ".apk"); + if (apk.isFile()) { + return apk; + } + } + throw new RuntimeException("Can't find NetworkStackApk"); + } + + /** + * Adopts common shell permissions needed for rollback tests. + */ + @Before + public void adoptShellPermissions() { + InstallUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.TEST_MANAGE_ROLLBACKS, + Manifest.permission.FORCE_STOP_PACKAGES, + Manifest.permission.WRITE_DEVICE_CONFIG); + } + + /** + * Drops shell permissions needed for rollback tests. + */ + @After + public void dropShellPermissions() { + InstallUtils.dropShellPermissionIdentity(); + } + + @Test + public void testNetworkFailedRollback_Phase1() throws Exception { + // Remove available rollbacks and uninstall NetworkStack on /data/ + RollbackManager rm = RollbackUtils.getRollbackManager(); + String networkStack = getNetworkStackPackageName(); + + rm.expireRollbackForPackage(networkStack); + uninstallNetworkStackPackage(); + + assertThat(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), + networkStack)).isNull(); + + // Reduce health check deadline + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK, + PROPERTY_WATCHDOG_REQUEST_TIMEOUT_MILLIS, + Integer.toString(120000), false); + // Simulate re-installation of new NetworkStack with rollbacks enabled + installNetworkStackPackage(); + } + + @Test + public void testNetworkFailedRollback_Phase2() throws Exception { + RollbackManager rm = RollbackUtils.getRollbackManager(); + assertThat(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), + getNetworkStackPackageName())).isNotNull(); + + // Sleep for < health check deadline + Thread.sleep(TimeUnit.SECONDS.toMillis(5)); + // Verify rollback was not executed before health check deadline + assertThat(getUniqueRollbackInfoForPackage(rm.getRecentlyCommittedRollbacks(), + getNetworkStackPackageName())).isNull(); + } + + @Test + public void testNetworkFailedRollback_Phase3() throws Exception { + RollbackManager rm = RollbackUtils.getRollbackManager(); + assertThat(getUniqueRollbackInfoForPackage(rm.getRecentlyCommittedRollbacks(), + getNetworkStackPackageName())).isNotNull(); + } + + private static String getNetworkStackPackageName() { + Intent intent = new Intent(NETWORK_STACK_CONNECTOR_CLASS); + ComponentName comp = intent.resolveSystemService( + InstrumentationRegistry.getInstrumentation().getContext().getPackageManager(), 0); + return comp.getPackageName(); + } + + private static void installNetworkStackPackage() throws Exception { + Install.single(NETWORK_STACK).setStaged().setEnableRollback() + .addInstallFlags(PackageManager.INSTALL_REPLACE_EXISTING).commit(); + } + + private static void uninstallNetworkStackPackage() { + // Uninstall the package as a privileged user so we won't fail due to permission. + runShellCommand("pm uninstall " + getNetworkStackPackageName()); + } + + @Test + public void testNetworkPassedDoesNotRollback_Phase1() throws Exception { + // Remove available rollbacks and uninstall NetworkStack on /data/ + RollbackManager rm = RollbackUtils.getRollbackManager(); + String networkStack = getNetworkStackPackageName(); + + rm.expireRollbackForPackage(networkStack); + uninstallNetworkStackPackage(); + + assertThat(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), + networkStack)).isNull(); + + // 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' + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK, + PROPERTY_WATCHDOG_REQUEST_TIMEOUT_MILLIS, + Integer.toString(300000), false); + // Simulate re-installation of new NetworkStack with rollbacks enabled + installNetworkStackPackage(); + } + + @Test + public void testNetworkPassedDoesNotRollback_Phase2() throws Exception { + RollbackManager rm = RollbackUtils.getRollbackManager(); + assertThat(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), + getNetworkStackPackageName())).isNotNull(); + } + + @Test + public void testNetworkPassedDoesNotRollback_Phase3() throws Exception { + // Sleep for > health check deadline. We expect no rollback should happen during sleeping. + // If the device reboots for rollback, this device test will fail as well as the host test. + Thread.sleep(TimeUnit.SECONDS.toMillis(310)); + RollbackManager rm = RollbackUtils.getRollbackManager(); + assertThat(getUniqueRollbackInfoForPackage(rm.getRecentlyCommittedRollbacks(), + getNetworkStackPackageName())).isNull(); + } + + private static void runShellCommand(String cmd) { + ParcelFileDescriptor pfd = InstrumentationRegistry.getInstrumentation().getUiAutomation() + .executeShellCommand(cmd); + IoUtils.closeQuietly(pfd); + } +} diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java index 277c04f99137..48b5bed609d1 100644 --- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java @@ -31,8 +31,10 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.content.rollback.RollbackInfo; import android.content.rollback.RollbackManager; +import android.os.UserManager; import android.provider.DeviceConfig; import android.util.Log; @@ -52,7 +54,9 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.util.Collections; +import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; /** * Test system Rollback APIs. @@ -71,6 +75,12 @@ public class RollbackTest { private static final String PROPERTY_ENABLE_ROLLBACK_TIMEOUT_MILLIS = "enable_rollback_timeout"; + private static boolean hasRollbackInclude(List<RollbackInfo> rollbacks, String packageName) { + return rollbacks.stream().anyMatch( + ri -> ri.getPackages().stream().anyMatch( + pri -> packageName.equals(pri.getPackageName()))); + } + /** * Test basic rollbacks. */ @@ -96,7 +106,10 @@ public class RollbackTest { Manifest.permission.INSTALL_PACKAGES, Manifest.permission.DELETE_PACKAGES, Manifest.permission.TEST_MANAGE_ROLLBACKS, - Manifest.permission.MANAGE_ROLLBACKS); + Manifest.permission.MANAGE_ROLLBACKS, + Manifest.permission.MANAGE_USERS, + Manifest.permission.CREATE_USERS, + Manifest.permission.INTERACT_ACROSS_USERS); // Register a broadcast receiver for notification when the // rollback has been committed. @@ -106,19 +119,14 @@ public class RollbackTest { // Uninstall TestApp.A Uninstall.packages(TestApp.A); assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(-1); - - // 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(), TestApp.A); - if (rollback != null) { + if (hasRollbackInclude(rm.getRecentlyCommittedRollbacks(), TestApp.A)) { Log.i(TAG, "Sleeping 1 second to wait for uninstall to take effect."); Thread.sleep(1000); } } + assertThat(hasRollbackInclude(rm.getRecentlyCommittedRollbacks(), TestApp.A)).isFalse(); // The app should not be available for rollback. waitForUnavailableRollback(TestApp.A); @@ -149,6 +157,12 @@ public class RollbackTest { RollbackUtils.rollback(available.getRollbackId()); assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1); + UserManager um = (UserManager) context.getSystemService(context.USER_SERVICE); + List<Integer> userIds = um.getUsers(true) + .stream().map(user -> user.id).collect(Collectors.toList()); + assertThat(InstallUtils.isOnlyInstalledForUser(TestApp.A, + context.getUserId(), userIds)).isTrue(); + // 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. @@ -380,9 +394,6 @@ public class RollbackTest { RollbackManager.PROPERTY_ROLLBACK_LIFETIME_MILLIS, Long.toString(expirationTime), false /* makeDefault*/); - // Pull the new expiration time from DeviceConfig - rm.reloadPersistedData(); - // Uninstall TestApp.A Uninstall.packages(TestApp.A); assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(-1); @@ -424,6 +435,53 @@ public class RollbackTest { } /** + * Test that available rollbacks should expire correctly when the property + * {@link RollbackManager#PROPERTY_ROLLBACK_LIFETIME_MILLIS} is changed + */ + @Test + public void testRollbackExpiresWhenLifetimeChanges() throws Exception { + long defaultExpirationTime = TimeUnit.HOURS.toMillis(48); + RollbackManager rm = RollbackUtils.getRollbackManager(); + + try { + InstallUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.TEST_MANAGE_ROLLBACKS, + Manifest.permission.WRITE_DEVICE_CONFIG); + + Uninstall.packages(TestApp.A); + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(-1); + Install.single(TestApp.A1).commit(); + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1); + Install.single(TestApp.A2).setEnableRollback().commit(); + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2); + RollbackInfo rollback = waitForAvailableRollback(TestApp.A); + assertThat(rollback).packagesContainsExactly(Rollback.from(TestApp.A2).to(TestApp.A1)); + + // Change the lifetime to 0 which should expire rollbacks immediately + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK_BOOT, + RollbackManager.PROPERTY_ROLLBACK_LIFETIME_MILLIS, + Long.toString(0), false /* makeDefault*/); + + // Keep polling until device config changes has happened (which might take more than + // 5 sec depending how busy system_server is) and rollbacks have expired + for (int i = 0; i < 30; ++i) { + if (hasRollbackInclude(rm.getAvailableRollbacks(), TestApp.A)) { + Thread.sleep(1000); + } + } + rollback = getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), TestApp.A); + assertThat(rollback).isNull(); + } finally { + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK_BOOT, + RollbackManager.PROPERTY_ROLLBACK_LIFETIME_MILLIS, + Long.toString(defaultExpirationTime), false /* makeDefault*/); + InstallUtils.dropShellPermissionIdentity(); + } + } + + /** * Test that changing time on device does not affect the duration of time that we keep * rollback available */ @@ -445,9 +503,6 @@ public class RollbackTest { 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 Uninstall.packages(TestApp.A); Install.single(TestApp.A1).commit(); @@ -479,23 +534,24 @@ public class RollbackTest { // 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); - RollbackInfo rollback = + RollbackInfo rollbackA = getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), TestApp.A); - assertThat(rollback).isNull(); + Log.i(TAG, "Checking if the rollback for TestApp.A is null"); // Rollback for app B should not be expired - rollback = getUniqueRollbackInfoForPackage( + RollbackInfo rollbackB1 = getUniqueRollbackInfoForPackage( rm.getAvailableRollbacks(), TestApp.B); - assertThat(rollback).isNotNull(); - assertThat(rollback).packagesContainsExactly( - Rollback.from(TestApp.B2).to(TestApp.B1)); // Wait until rollback for app B has expired Thread.sleep(expirationTime / 2); - rollback = getUniqueRollbackInfoForPackage( + RollbackInfo rollbackB2 = getUniqueRollbackInfoForPackage( rm.getAvailableRollbacks(), TestApp.B); - // Rollback should be expired by now - assertThat(rollback).isNull(); + + assertThat(rollbackA).isNull(); + assertThat(rollbackB1).isNotNull(); + assertThat(rollbackB1).packagesContainsExactly( + Rollback.from(TestApp.B2).to(TestApp.B1)); + assertThat(rollbackB2).isNull(); } finally { RollbackUtils.forwardTimeBy(-expirationTime); } @@ -1003,7 +1059,6 @@ public class RollbackTest { } @Test - @Ignore("b/136605788") public void testEnableRollbackTimeoutFailsRollback() throws Exception { try { InstallUtils.adoptShellPermissionIdentity( @@ -1044,4 +1099,107 @@ public class RollbackTest { InstallUtils.dropShellPermissionIdentity(); } } + + @Test + public void testEnableRollbackTimeoutFailsRollback_MultiPackage() throws Exception { + try { + InstallUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.TEST_MANAGE_ROLLBACKS, + Manifest.permission.MANAGE_ROLLBACKS, + Manifest.permission.WRITE_DEVICE_CONFIG); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK, + PROPERTY_ENABLE_ROLLBACK_TIMEOUT_MILLIS, + Long.toString(5000), false /* makeDefault*/); + RollbackManager rm = RollbackUtils.getRollbackManager(); + + Uninstall.packages(TestApp.A, TestApp.B); + Install.multi(TestApp.A1, TestApp.B1).commit(); + waitForUnavailableRollback(TestApp.A); + + // Block the 2nd session for 10s so it will not be able to enable the rollback in time. + rm.blockRollbackManager(TimeUnit.SECONDS.toMillis(0)); + rm.blockRollbackManager(TimeUnit.SECONDS.toMillis(10)); + Install.multi(TestApp.A2, TestApp.B2).setEnableRollback().commit(); + + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2); + assertThat(InstallUtils.getInstalledVersion(TestApp.B)).isEqualTo(2); + + // Give plenty of time for RollbackManager to unblock and attempt + // to make the rollback available before asserting that the + // rollback was not made available. + Thread.sleep(TimeUnit.SECONDS.toMillis(2)); + + List<RollbackInfo> available = rm.getAvailableRollbacks(); + assertThat(getUniqueRollbackInfoForPackage(available, TestApp.A)).isNull(); + assertThat(getUniqueRollbackInfoForPackage(available, TestApp.B)).isNull(); + } finally { + //setting the timeout back to default + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK, + PROPERTY_ENABLE_ROLLBACK_TIMEOUT_MILLIS, + null, false /* makeDefault*/); + InstallUtils.dropShellPermissionIdentity(); + } + } + + /** + * Test we can't enable rollback for non-whitelisted app without + * TEST_MANAGE_ROLLBACKS permission + */ + @Test + public void testNonRollbackWhitelistedApp() throws Exception { + try { + InstallUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.MANAGE_ROLLBACKS); + + Uninstall.packages(TestApp.A); + Install.single(TestApp.A1).commit(); + assertThat(RollbackUtils.getAvailableRollback(TestApp.A)).isNull(); + + Install.single(TestApp.A2).setEnableRollback().commit(); + Thread.sleep(TimeUnit.SECONDS.toMillis(2)); + assertThat(RollbackUtils.getAvailableRollback(TestApp.A)).isNull(); + } finally { + InstallUtils.dropShellPermissionIdentity(); + } + } + + @Test + public void testRollbackDataPolicy() throws Exception { + try { + InstallUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.TEST_MANAGE_ROLLBACKS); + + Uninstall.packages(TestApp.A, TestApp.B); + Install.multi(TestApp.A1, TestApp.B1).commit(); + // Write user data version = 1 + InstallUtils.processUserData(TestApp.A); + InstallUtils.processUserData(TestApp.B); + + Install a2 = Install.single(TestApp.A2) + .setEnableRollback(PackageManager.RollbackDataPolicy.WIPE); + Install b2 = Install.single(TestApp.B2) + .setEnableRollback(PackageManager.RollbackDataPolicy.RESTORE); + Install.multi(a2, b2).setEnableRollback().commit(); + // Write user data version = 2 + InstallUtils.processUserData(TestApp.A); + InstallUtils.processUserData(TestApp.B); + + RollbackInfo info = RollbackUtils.getAvailableRollback(TestApp.A); + RollbackUtils.rollback(info.getRollbackId()); + // Read user data version from userdata.txt + // A's user data version is -1 for user data is wiped. + // B's user data version is 1 as rollback committed. + assertThat(InstallUtils.getUserDataVersion(TestApp.A)).isEqualTo(-1); + assertThat(InstallUtils.getUserDataVersion(TestApp.B)).isEqualTo(1); + } finally { + InstallUtils.dropShellPermissionIdentity(); + } + } } diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java index 9e6ac8ef679b..6c9ffe2a7fac 100644 --- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java @@ -21,20 +21,16 @@ import static com.android.cts.rollback.lib.RollbackUtils.getUniqueRollbackInfoFo import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; - import android.Manifest; -import android.annotation.Nullable; -import android.content.ComponentName; import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; +import android.content.pm.PackageManager; import android.content.rollback.RollbackInfo; import android.content.rollback.RollbackManager; -import android.text.TextUtils; +import android.os.storage.StorageManager; +import android.provider.DeviceConfig; -import androidx.test.InstrumentationRegistry; +import androidx.test.platform.app.InstrumentationRegistry; import com.android.cts.install.lib.Install; import com.android.cts.install.lib.InstallUtils; @@ -51,6 +47,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import java.io.File; +import java.util.concurrent.TimeUnit; + /** * Tests for rollback of staged installs. * <p> @@ -61,11 +60,8 @@ import org.junit.runners.JUnit4; */ @RunWith(JUnit4.class) public class StagedRollbackTest { - - private static final String NETWORK_STACK_CONNECTOR_CLASS = - "android.net.INetworkStackConnector"; - - private static final String MODULE_META_DATA_PACKAGE = getModuleMetadataPackageName(); + private static final String PROPERTY_WATCHDOG_TRIGGER_FAILURE_COUNT = + "watchdog_trigger_failure_count"; /** * Adopts common shell permissions needed for rollback tests. @@ -76,7 +72,8 @@ public class StagedRollbackTest { Manifest.permission.INSTALL_PACKAGES, Manifest.permission.DELETE_PACKAGES, Manifest.permission.TEST_MANAGE_ROLLBACKS, - Manifest.permission.FORCE_STOP_PACKAGES); + Manifest.permission.FORCE_STOP_PACKAGES, + Manifest.permission.WRITE_DEVICE_CONFIG); } /** @@ -92,7 +89,7 @@ public class StagedRollbackTest { * Enable rollback phase. */ @Test - public void testBadApkOnlyEnableRollback() throws Exception { + public void testBadApkOnly_Phase1() throws Exception { Uninstall.packages(TestApp.A); assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(-1); @@ -101,9 +98,6 @@ public class StagedRollbackTest { InstallUtils.processUserData(TestApp.A); Install.single(TestApp.ACrashing2).setEnableRollback().setStaged().commit(); - - // At this point, the host test driver will reboot the device and run - // testBadApkOnlyConfirmEnableRollback(). } /** @@ -111,7 +105,7 @@ public class StagedRollbackTest { * Confirm that rollback was successfully enabled. */ @Test - public void testBadApkOnlyConfirmEnableRollback() throws Exception { + public void testBadApkOnly_Phase2() throws Exception { assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2); InstallUtils.processUserData(TestApp.A); @@ -123,26 +117,22 @@ public class StagedRollbackTest { Rollback.from(TestApp.A2).to(TestApp.A1)); assertThat(rollback.isStaged()).isTrue(); - // At this point, the host test driver will run - // testBadApkOnlyTriggerRollback(). + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK, + PROPERTY_WATCHDOG_TRIGGER_FAILURE_COUNT, + Integer.toString(5), false); + RollbackUtils.sendCrashBroadcast(TestApp.A, 4); + // Sleep for a while to make sure we don't trigger rollback + Thread.sleep(TimeUnit.SECONDS.toMillis(30)); } /** * 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. + * Trigger rollback phase. */ @Test - public void testBadApkOnlyTriggerRollback() throws Exception { - // Crash TestApp.A PackageWatchdog#TRIGGER_FAILURE_COUNT times to trigger rollback - RollbackUtils.sendCrashBroadcast(TestApp.A, 5); - - // 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"); + public void testBadApkOnly_Phase3() throws Exception { + // One more crash to trigger rollback + RollbackUtils.sendCrashBroadcast(TestApp.A, 1); } /** @@ -150,7 +140,7 @@ public class StagedRollbackTest { * Confirm rollback phase. */ @Test - public void testBadApkOnlyConfirmRollback() throws Exception { + public void testBadApkOnly_Phase4() throws Exception { assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1); InstallUtils.processUserData(TestApp.A); @@ -165,84 +155,107 @@ public class StagedRollbackTest { assertThat(rollback.getCommittedSessionId()).isNotEqualTo(-1); } + /** + * Stage install an apk with rollback that will be later triggered by unattributable crash. + */ @Test - public void resetNetworkStack() throws Exception { - RollbackManager rm = RollbackUtils.getRollbackManager(); - String networkStack = getNetworkStackPackageName(); - - rm.expireRollbackForPackage(networkStack); - Uninstall.packages(networkStack); - - assertThat(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), - networkStack)).isNull(); - } - - @Test - public void installModuleMetadataPackage() throws Exception { - resetModuleMetadataPackage(); - Context context = InstrumentationRegistry.getContext(); - PackageInfo metadataPackageInfo = context.getPackageManager().getPackageInfo( - MODULE_META_DATA_PACKAGE, 0); - String metadataApkPath = metadataPackageInfo.applicationInfo.sourceDir; - assertThat(metadataApkPath).isNotNull(); - assertThat(metadataApkPath).isNotEqualTo(""); + public void testNativeWatchdogTriggersRollback_Phase1() throws Exception { + Uninstall.packages(TestApp.A); + Install.single(TestApp.A1).commit(); + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1); - runShellCommand("pm install " - + "-r --enable-rollback --staged --wait " - + metadataApkPath); + Install.single(TestApp.A2).setEnableRollback().setStaged().commit(); } + /** + * Verify the rollback is available. + */ @Test - public void assertNetworkStackRollbackAvailable() throws Exception { + public void testNativeWatchdogTriggersRollback_Phase2() throws Exception { + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2); RollbackManager rm = RollbackUtils.getRollbackManager(); assertThat(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), - getNetworkStackPackageName())).isNotNull(); + TestApp.A)).isNotNull(); } + /** + * Verify the rollback is committed after crashing. + */ @Test - public void assertNetworkStackRollbackCommitted() throws Exception { + public void testNativeWatchdogTriggersRollback_Phase3() throws Exception { + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1); RollbackManager rm = RollbackUtils.getRollbackManager(); assertThat(getUniqueRollbackInfoForPackage(rm.getRecentlyCommittedRollbacks(), - getNetworkStackPackageName())).isNotNull(); + TestApp.A)).isNotNull(); } + /** + * Stage install an apk with rollback that will be later triggered by unattributable crash. + */ @Test - public void assertNoNetworkStackRollbackCommitted() throws Exception { - RollbackManager rm = RollbackUtils.getRollbackManager(); - assertThat(getUniqueRollbackInfoForPackage(rm.getRecentlyCommittedRollbacks(), - getNetworkStackPackageName())).isNull(); + public void testNativeWatchdogTriggersRollbackForAll_Phase1() throws Exception { + Uninstall.packages(TestApp.A); + Install.single(TestApp.A1).commit(); + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1); + + Install.single(TestApp.A2).setEnableRollback().setStaged().commit(); } + /** + * Verify the rollback is available and then install another package with rollback. + */ @Test - public void assertModuleMetadataRollbackAvailable() throws Exception { + public void testNativeWatchdogTriggersRollbackForAll_Phase2() throws Exception { + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2); RollbackManager rm = RollbackUtils.getRollbackManager(); assertThat(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), - MODULE_META_DATA_PACKAGE)).isNotNull(); + TestApp.A)).isNotNull(); + + // Install another package with rollback + Uninstall.packages(TestApp.B); + Install.single(TestApp.B1).commit(); + assertThat(InstallUtils.getInstalledVersion(TestApp.B)).isEqualTo(1); + + Install.single(TestApp.B2).setEnableRollback().setStaged().commit(); } + /** + * Verify the rollbacks are available. + */ @Test - public void assertModuleMetadataRollbackCommitted() throws Exception { + public void testNativeWatchdogTriggersRollbackForAll_Phase3() throws Exception { + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2); + assertThat(InstallUtils.getInstalledVersion(TestApp.B)).isEqualTo(2); RollbackManager rm = RollbackUtils.getRollbackManager(); - assertThat(getUniqueRollbackInfoForPackage(rm.getRecentlyCommittedRollbacks(), - MODULE_META_DATA_PACKAGE)).isNotNull(); + assertThat(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), + TestApp.A)).isNotNull(); + assertThat(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), + TestApp.B)).isNotNull(); } - private String getNetworkStackPackageName() { - Intent intent = new Intent(NETWORK_STACK_CONNECTOR_CLASS); - ComponentName comp = intent.resolveSystemService( - InstrumentationRegistry.getContext().getPackageManager(), 0); - return comp.getPackageName(); + /** + * Verify the rollbacks are committed after crashing. + */ + @Test + public void testNativeWatchdogTriggersRollbackForAll_Phase4() throws Exception { + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1); + assertThat(InstallUtils.getInstalledVersion(TestApp.B)).isEqualTo(1); + RollbackManager rm = RollbackUtils.getRollbackManager(); + assertThat(getUniqueRollbackInfoForPackage(rm.getRecentlyCommittedRollbacks(), + TestApp.A)).isNotNull(); + assertThat(getUniqueRollbackInfoForPackage(rm.getRecentlyCommittedRollbacks(), + TestApp.B)).isNotNull(); } @Test - public void testPreviouslyAbandonedRollbacksEnableRollback() throws Exception { + public void testPreviouslyAbandonedRollbacks_Phase1() throws Exception { Uninstall.packages(TestApp.A); Install.single(TestApp.A1).commit(); assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1); int sessionId = Install.single(TestApp.A2).setStaged().setEnableRollback().commit(); - PackageInstaller pi = InstrumentationRegistry.getContext().getPackageManager() - .getPackageInstaller(); + PackageInstaller pi = InstrumentationRegistry.getInstrumentation().getContext() + .getPackageManager().getPackageInstaller(); pi.abandonSession(sessionId); // Remove the first intent sender result, so that the next staged install session does not @@ -253,7 +266,7 @@ public class StagedRollbackTest { } @Test - public void testPreviouslyAbandonedRollbacksCommitRollback() throws Exception { + public void testPreviouslyAbandonedRollbacks_Phase2() throws Exception { assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2); InstallUtils.processUserData(TestApp.A); @@ -264,34 +277,212 @@ public class StagedRollbackTest { } @Test - public void testPreviouslyAbandonedRollbacksCheckUserdataRollback() throws Exception { + public void testPreviouslyAbandonedRollbacks_Phase3() throws Exception { assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1); InstallUtils.processUserData(TestApp.A); Uninstall.packages(TestApp.A); } - @Nullable private static String getModuleMetadataPackageName() { - String packageName = InstrumentationRegistry.getContext().getResources().getString( - R.string.config_defaultModuleMetadataProvider); - if (TextUtils.isEmpty(packageName)) { - return null; - } - return packageName; + return InstrumentationRegistry.getInstrumentation().getContext() + .getResources().getString(R.string.config_defaultModuleMetadataProvider); + } + + @Test + public void testRollbackWhitelistedApp_Phase1() throws Exception { + // Remove available rollbacks + String pkgName = getModuleMetadataPackageName(); + RollbackUtils.getRollbackManager().expireRollbackForPackage(pkgName); + assertThat(RollbackUtils.getAvailableRollback(pkgName)).isNull(); + + // Overwrite existing permissions. We don't want TEST_MANAGE_ROLLBACKS which allows us + // to enable rollback for any app + InstallUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.MANAGE_ROLLBACKS); + + // Re-install a whitelisted app with rollbacks enabled + String filePath = InstrumentationRegistry.getInstrumentation().getContext() + .getPackageManager().getPackageInfo(pkgName, 0).applicationInfo.sourceDir; + TestApp app = new TestApp("ModuleMetadata", pkgName, -1, false, new File(filePath)); + Install.single(app).setStaged().setEnableRollback() + .addInstallFlags(PackageManager.INSTALL_REPLACE_EXISTING).commit(); + } + + @Test + public void testRollbackWhitelistedApp_Phase2() throws Exception { + assertThat(RollbackUtils.getAvailableRollback(getModuleMetadataPackageName())).isNotNull(); + } + + @Test + public void testRollbackDataPolicy_Phase1() throws Exception { + Uninstall.packages(TestApp.A, TestApp.B); + Install.multi(TestApp.A1, TestApp.B1).commit(); + // Write user data version = 1 + InstallUtils.processUserData(TestApp.A); + InstallUtils.processUserData(TestApp.B); + + Install a2 = Install.single(TestApp.A2).setStaged() + .setEnableRollback(PackageManager.RollbackDataPolicy.WIPE); + Install b2 = Install.single(TestApp.B2).setStaged() + .setEnableRollback(PackageManager.RollbackDataPolicy.RESTORE); + Install.multi(a2, b2).setEnableRollback().setStaged().commit(); + } + + @Test + public void testRollbackDataPolicy_Phase2() throws Exception { + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2); + assertThat(InstallUtils.getInstalledVersion(TestApp.B)).isEqualTo(2); + // Write user data version = 2 + InstallUtils.processUserData(TestApp.A); + InstallUtils.processUserData(TestApp.B); + + RollbackInfo info = RollbackUtils.getAvailableRollback(TestApp.A); + RollbackUtils.rollback(info.getRollbackId()); + } + + @Test + public void testRollbackDataPolicy_Phase3() throws Exception { + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1); + assertThat(InstallUtils.getInstalledVersion(TestApp.B)).isEqualTo(1); + // Read user data version from userdata.txt + // A's user data version is -1 for user data is wiped. + // B's user data version is 1 as rollback committed. + assertThat(InstallUtils.getUserDataVersion(TestApp.A)).isEqualTo(-1); + assertThat(InstallUtils.getUserDataVersion(TestApp.B)).isEqualTo(1); } - private void resetModuleMetadataPackage() { + @Test + public void testCleanUp() throws Exception { + // testNativeWatchdogTriggersRollback will fail if multiple staged sessions are + // committed on a device which doesn't support checkpoint. Let's clean up all rollbacks + // so there is only one rollback to commit when testing native crashes. RollbackManager rm = RollbackUtils.getRollbackManager(); + rm.getAvailableRollbacks().stream().flatMap(info -> info.getPackages().stream()) + .map(info -> info.getPackageName()).forEach(rm::expireRollbackForPackage); + assertThat(RollbackUtils.getRollbackManager().getAvailableRollbacks()).isEmpty(); + } - assertThat(MODULE_META_DATA_PACKAGE).isNotNull(); - rm.expireRollbackForPackage(MODULE_META_DATA_PACKAGE); + private static final String APK_IN_APEX_TESTAPEX_NAME = "com.android.apex.apkrollback.test"; + private static final TestApp TEST_APEX_WITH_APK_V1 = new TestApp("TestApexWithApkV1", + APK_IN_APEX_TESTAPEX_NAME, 1, /*isApex*/true, APK_IN_APEX_TESTAPEX_NAME + "_v1.apex"); + private static final TestApp TEST_APEX_WITH_APK_V2 = new TestApp("TestApexWithApkV2", + APK_IN_APEX_TESTAPEX_NAME, 2, /*isApex*/true, APK_IN_APEX_TESTAPEX_NAME + "_v2.apex"); + private static final TestApp TEST_APEX_WITH_APK_V2_CRASHING = new TestApp( + "TestApexWithApkV2Crashing", APK_IN_APEX_TESTAPEX_NAME, 2, /*isApex*/true, + APK_IN_APEX_TESTAPEX_NAME + "_v2Crashing.apex"); - assertThat(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), - MODULE_META_DATA_PACKAGE)).isNull(); + @Test + public void testRollbackApexWithApk_Phase1() throws Exception { + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1); + InstallUtils.processUserData(TestApp.A); + + int sessionId = Install.single(TEST_APEX_WITH_APK_V2).setStaged().setEnableRollback() + .commit(); + InstallUtils.waitForSessionReady(sessionId); + } + + @Test + public void testRollbackApexWithApk_Phase2() throws Exception { + assertThat(InstallUtils.getInstalledVersion(APK_IN_APEX_TESTAPEX_NAME)).isEqualTo(2); + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2); + InstallUtils.processUserData(TestApp.A); + + RollbackInfo available = RollbackUtils.getAvailableRollback(APK_IN_APEX_TESTAPEX_NAME); + assertThat(available).isStaged(); + assertThat(available).packagesContainsExactly( + Rollback.from(TEST_APEX_WITH_APK_V2).to(TEST_APEX_WITH_APK_V1), + Rollback.from(TestApp.A, 0).to(TestApp.A1)); + + RollbackUtils.rollback(available.getRollbackId(), TEST_APEX_WITH_APK_V2); + RollbackInfo committed = RollbackUtils.getCommittedRollbackById(available.getRollbackId()); + assertThat(committed).isNotNull(); + assertThat(committed).isStaged(); + assertThat(committed).packagesContainsExactly( + Rollback.from(TEST_APEX_WITH_APK_V2).to(TEST_APEX_WITH_APK_V1), + Rollback.from(TestApp.A, 0).to(TestApp.A1)); + assertThat(committed).causePackagesContainsExactly(TEST_APEX_WITH_APK_V2); + assertThat(committed.getCommittedSessionId()).isNotEqualTo(-1); + + // Note: The app is not rolled back until after the rollback is staged + // and the device has been rebooted. + InstallUtils.waitForSessionReady(committed.getCommittedSessionId()); + assertThat(InstallUtils.getInstalledVersion(APK_IN_APEX_TESTAPEX_NAME)).isEqualTo(2); + } + + @Test + public void testRollbackApexWithApk_Phase3() throws Exception { + assertThat(InstallUtils.getInstalledVersion(APK_IN_APEX_TESTAPEX_NAME)).isEqualTo(1); + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1); + InstallUtils.processUserData(TestApp.A); } - private void runShellCommand(String cmd) { - InstrumentationRegistry.getInstrumentation().getUiAutomation() - .executeShellCommand(cmd); + /** + * Installs an apex with an apk that can crash. + */ + @Test + public void testRollbackApexWithApkCrashing_Phase1() throws Exception { + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1); + int sessionId = Install.single(TEST_APEX_WITH_APK_V2_CRASHING).setStaged() + .setEnableRollback().commit(); + InstallUtils.waitForSessionReady(sessionId); + } + + /** + * Verifies rollback has been enabled successfully. Then makes TestApp.A crash. + */ + @Test + public void testRollbackApexWithApkCrashing_Phase2() throws Exception { + assertThat(InstallUtils.getInstalledVersion(APK_IN_APEX_TESTAPEX_NAME)).isEqualTo(2); + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2); + + RollbackInfo available = RollbackUtils.getAvailableRollback(APK_IN_APEX_TESTAPEX_NAME); + assertThat(available).isStaged(); + assertThat(available).packagesContainsExactly( + Rollback.from(TEST_APEX_WITH_APK_V2).to(TEST_APEX_WITH_APK_V1), + Rollback.from(TestApp.A, 0).to(TestApp.A1)); + + // Crash TestApp.A PackageWatchdog#TRIGGER_FAILURE_COUNT times to trigger rollback + RollbackUtils.sendCrashBroadcast(TestApp.A, 5); + } + + @Test + public void testRollbackApexWithApkCrashing_Phase3() throws Exception { + assertThat(InstallUtils.getInstalledVersion(APK_IN_APEX_TESTAPEX_NAME)).isEqualTo(1); + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1); + } + + @Test + public void testRollbackApexDataDirectories_Phase1() throws Exception { + int sessionId = Install.single(TEST_APEX_WITH_APK_V2).setStaged().setEnableRollback() + .commit(); + InstallUtils.waitForSessionReady(sessionId); + } + + @Test + public void testRollbackApexDataDirectories_Phase2() throws Exception { + RollbackInfo available = RollbackUtils.getAvailableRollback(APK_IN_APEX_TESTAPEX_NAME); + + RollbackUtils.rollback(available.getRollbackId(), TEST_APEX_WITH_APK_V2); + RollbackInfo committed = RollbackUtils.getCommittedRollbackById(available.getRollbackId()); + + // Note: The app is not rolled back until after the rollback is staged + // and the device has been rebooted. + InstallUtils.waitForSessionReady(committed.getCommittedSessionId()); + } + + @Test + public void isCheckpointSupported() { + Context context = InstrumentationRegistry.getInstrumentation().getContext(); + StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE); + assertThat(sm.isCheckpointSupported()).isTrue(); + } + + @Test + public void hasMainlineModule() throws Exception { + String pkgName = getModuleMetadataPackageName(); + boolean existed = InstrumentationRegistry.getInstrumentation().getContext() + .getPackageManager().getModuleInfo(pkgName, 0) != null; + assertThat(existed).isTrue(); } } 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 index 47d01734be40..78775be84828 100644 --- a/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java +++ b/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java @@ -16,24 +16,32 @@ package com.android.tests.rollback.host; +import static com.android.tests.rollback.host.WatchdogEventLogger.watchdogEventOccurred; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; -import com.android.ddmlib.Log.LogLevel; -import com.android.tradefed.device.DeviceNotAvailableException; -import com.android.tradefed.log.LogUtil.CLog; +import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; +import com.android.tradefed.util.CommandResult; +import com.android.tradefed.util.CommandStatus; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; +import java.io.File; +import java.util.List; import java.util.concurrent.TimeUnit; /** * Runs the staged rollback tests. + * + * TODO(gavincorkery): Support the verification of logging parents in Watchdog metrics. */ @RunWith(DeviceJUnit4ClassRunner.class) public class StagedRollbackTest extends BaseHostJUnit4Test { @@ -51,55 +59,106 @@ public class StagedRollbackTest extends BaseHostJUnit4Test { phase)); } + private static final String APK_IN_APEX_TESTAPEX_NAME = "com.android.apex.apkrollback.test"; + private static final String TESTAPP_A = "com.android.cts.install.lib.testapp.A"; + + private static final String TEST_SUBDIR = "/subdir/"; + + private static final String TEST_FILENAME_1 = "test_file.txt"; + private static final String TEST_STRING_1 = "hello this is a test"; + private static final String TEST_FILENAME_2 = "another_file.txt"; + private static final String TEST_STRING_2 = "this is a different file"; + private static final String TEST_FILENAME_3 = "also.xyz"; + private static final String TEST_STRING_3 = "also\n a\n test\n string"; + private static final String TEST_FILENAME_4 = "one_more.test"; + private static final String TEST_STRING_4 = "once more unto the test"; + + private static final String REASON_APP_CRASH = "REASON_APP_CRASH"; + private static final String REASON_NATIVE_CRASH = "REASON_NATIVE_CRASH"; + + private static final String ROLLBACK_INITIATE = "ROLLBACK_INITIATE"; + private static final String ROLLBACK_BOOT_TRIGGERED = "ROLLBACK_BOOT_TRIGGERED"; + private static final String ROLLBACK_SUCCESS = "ROLLBACK_SUCCESS"; + + private WatchdogEventLogger mLogger = new WatchdogEventLogger(); + @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"); + deleteFiles("/system/apex/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex", + "/data/apex/active/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex"); + runPhase("testCleanUp"); + mLogger.start(getDevice()); } @After public void tearDown() throws Exception { - // Reconnect internet after testing network health triggered rollbacks - getDevice().executeShellCommand("svc wifi enable"); - getDevice().executeShellCommand("svc data enable"); + mLogger.stop(); + runPhase("testCleanUp"); + deleteFiles("/system/apex/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex", + "/data/apex/active/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex", + apexDataDirDeSys(APK_IN_APEX_TESTAPEX_NAME) + "*", + apexDataDirCe(APK_IN_APEX_TESTAPEX_NAME, 0) + "*"); + } + + /** + * Deletes files and reboots the device if necessary. + * @param files the paths of files which might contain wildcards + */ + private void deleteFiles(String... files) throws Exception { + boolean found = false; + for (String file : files) { + CommandResult result = getDevice().executeShellV2Command("ls " + file); + if (result.getStatus() == CommandStatus.SUCCESS) { + found = true; + break; + } + } + + if (found) { + if (!getDevice().isAdbRoot()) { + getDevice().enableAdbRoot(); + } + getDevice().remountSystemWritable(); + for (String file : files) { + getDevice().executeShellCommand("rm -rf " + file); + } + getDevice().reboot(); + } } /** * Tests watchdog triggered staged rollbacks involving only apks. */ @Test - @Ignore("b/139175593 flaky test") public void testBadApkOnly() throws Exception { - runPhase("testBadApkOnlyEnableRollback"); + runPhase("testBadApkOnly_Phase1"); 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. - } + runPhase("testBadApkOnly_Phase2"); + + // Trigger rollback and wait for reboot to happen + runPhase("testBadApkOnly_Phase3"); + assertTrue(getDevice().waitForDeviceNotAvailable(TimeUnit.MINUTES.toMillis(2))); getDevice().waitForDeviceAvailable(); - runPhase("testBadApkOnlyConfirmRollback"); + runPhase("testBadApkOnly_Phase4"); + + List<String> watchdogEvents = mLogger.getWatchdogLoggingEvents(); + assertTrue(watchdogEventOccurred(watchdogEvents, ROLLBACK_INITIATE, null, + REASON_APP_CRASH, TESTAPP_A)); + assertTrue(watchdogEventOccurred(watchdogEvents, ROLLBACK_BOOT_TRIGGERED, null, + null, null)); + assertTrue(watchdogEventOccurred(watchdogEvents, ROLLBACK_SUCCESS, null, null, null)); } @Test public void testNativeWatchdogTriggersRollback() throws Exception { - //Stage install ModuleMetadata package - this simulates a Mainline module update - runPhase("installModuleMetadataPackage"); + runPhase("testNativeWatchdogTriggersRollback_Phase1"); // Reboot device to activate staged package getDevice().reboot(); - getDevice().waitForDeviceAvailable(); - runPhase("assertModuleMetadataRollbackAvailable"); + runPhase("testNativeWatchdogTriggersRollback_Phase2"); // crash system_server enough times to trigger a rollback crashProcess("system_server", NATIVE_CRASHES_THRESHOLD); @@ -116,94 +175,268 @@ public class StagedRollbackTest extends BaseHostJUnit4Test { getDevice().waitForDeviceAvailable(); // verify rollback committed - runPhase("assertModuleMetadataRollbackCommitted"); + runPhase("testNativeWatchdogTriggersRollback_Phase3"); + + List<String> watchdogEvents = mLogger.getWatchdogLoggingEvents(); + assertTrue(watchdogEventOccurred(watchdogEvents, ROLLBACK_INITIATE, null, + REASON_NATIVE_CRASH, null)); + assertTrue(watchdogEventOccurred(watchdogEvents, ROLLBACK_BOOT_TRIGGERED, null, + null, null)); + assertTrue(watchdogEventOccurred(watchdogEvents, ROLLBACK_SUCCESS, null, null, null)); + } + + @Test + public void testNativeWatchdogTriggersRollbackForAll() throws Exception { + // This test requires committing multiple staged rollbacks + assumeTrue(isCheckpointSupported()); + + // Install a package with rollback enabled. + runPhase("testNativeWatchdogTriggersRollbackForAll_Phase1"); + getDevice().reboot(); + + // Once previous staged install is applied, install another package + runPhase("testNativeWatchdogTriggersRollbackForAll_Phase2"); + getDevice().reboot(); + + // Verify the new staged install has also been applied successfully. + runPhase("testNativeWatchdogTriggersRollbackForAll_Phase3"); + + // crash system_server enough times to trigger a rollback + crashProcess("system_server", NATIVE_CRASHES_THRESHOLD); + + // Rollback should be committed automatically now. + // Give time for rollback to be committed. This could take a while, + // because we need all of the following to happen: + // 1. system_server comes back up and boot completes. + // 2. Rollback health observer detects updatable crashing signal. + // 3. Staged rollback session becomes ready. + // 4. Device actually reboots. + // So we give a generous timeout here. + assertTrue(getDevice().waitForDeviceNotAvailable(TimeUnit.MINUTES.toMillis(5))); + getDevice().waitForDeviceAvailable(); + + // verify all available rollbacks have been committed + runPhase("testNativeWatchdogTriggersRollbackForAll_Phase4"); + + List<String> watchdogEvents = mLogger.getWatchdogLoggingEvents(); + assertTrue(watchdogEventOccurred(watchdogEvents, ROLLBACK_INITIATE, null, + REASON_NATIVE_CRASH, null)); + assertTrue(watchdogEventOccurred(watchdogEvents, ROLLBACK_BOOT_TRIGGERED, null, + null, null)); + assertTrue(watchdogEventOccurred(watchdogEvents, ROLLBACK_SUCCESS, null, null, null)); } /** - * Tests failed network health check triggers watchdog staged rollbacks. + * Tests rolling back user data where there are multiple rollbacks for that package. */ @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 120000"); - // Simulate re-installation of new NetworkStack with rollbacks enabled - getDevice().executeShellCommand("pm install -r --staged --enable-rollback " - + getNetworkStackPath()); - - // Sleep to allow writes to disk before reboot - Thread.sleep(5000); - // Reboot device to activate staged package + public void testPreviouslyAbandonedRollbacks() throws Exception { + runPhase("testPreviouslyAbandonedRollbacks_Phase1"); getDevice().reboot(); - getDevice().waitForDeviceAvailable(); + runPhase("testPreviouslyAbandonedRollbacks_Phase2"); + getDevice().reboot(); + runPhase("testPreviouslyAbandonedRollbacks_Phase3"); + } - // Verify rollback was enabled - runPhase("assertNetworkStackRollbackAvailable"); + /** + * Tests we can enable rollback for a whitelisted app. + */ + @Test + public void testRollbackWhitelistedApp() throws Exception { + assumeTrue(hasMainlineModule()); + runPhase("testRollbackWhitelistedApp_Phase1"); + getDevice().reboot(); + runPhase("testRollbackWhitelistedApp_Phase2"); + } - // Sleep for < health check deadline - Thread.sleep(5000); - // Verify rollback was not executed before health check deadline - runPhase("assertNoNetworkStackRollbackCommitted"); + @Test + public void testRollbackDataPolicy() throws Exception { + runPhase("testRollbackDataPolicy_Phase1"); + getDevice().reboot(); + runPhase("testRollbackDataPolicy_Phase2"); + getDevice().reboot(); + runPhase("testRollbackDataPolicy_Phase3"); + } - // Wait for reboot to happen + /** + * Tests that userdata of apk-in-apex is restored when apex is rolled back. + */ + @Test + public void testRollbackApexWithApk() throws Exception { + getDevice().uninstallPackage("com.android.cts.install.lib.testapp.A"); + pushTestApex(); + runPhase("testRollbackApexWithApk_Phase1"); + getDevice().reboot(); + runPhase("testRollbackApexWithApk_Phase2"); + getDevice().reboot(); + runPhase("testRollbackApexWithApk_Phase3"); + } + + /** + * Tests that RollbackPackageHealthObserver is observing apk-in-apex. + */ + @Test + public void testRollbackApexWithApkCrashing() throws Exception { + getDevice().uninstallPackage("com.android.cts.install.lib.testapp.A"); + pushTestApex(); + + // Install an apex with apk that crashes + runPhase("testRollbackApexWithApkCrashing_Phase1"); + getDevice().reboot(); + // Verify apex was installed and then crash the apk + runPhase("testRollbackApexWithApkCrashing_Phase2"); + // Wait for crash to trigger rollback assertTrue(getDevice().waitForDeviceNotAvailable(TimeUnit.MINUTES.toMillis(5))); - // Wait for reboot to complete and device to become available getDevice().waitForDeviceAvailable(); - // Verify rollback was executed after health check deadline - runPhase("assertNetworkStackRollbackCommitted"); + // Verify rollback occurred due to crash of apk-in-apex + runPhase("testRollbackApexWithApkCrashing_Phase3"); + + List<String> watchdogEvents = mLogger.getWatchdogLoggingEvents(); + assertTrue(watchdogEventOccurred(watchdogEvents, ROLLBACK_INITIATE, null, + REASON_APP_CRASH, TESTAPP_A)); + assertTrue(watchdogEventOccurred(watchdogEvents, ROLLBACK_BOOT_TRIGGERED, null, + null, null)); + assertTrue(watchdogEventOccurred(watchdogEvents, ROLLBACK_SUCCESS, null, null, null)); } /** - * Tests passed network health check does not trigger watchdog staged rollbacks. + * Tests that data in DE_sys apex data directory is restored when apex is rolled back. */ @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 " - + getNetworkStackPath()); - - // Sleep to allow writes to disk before reboot - Thread.sleep(5000); - // Reboot device to activate staged package + public void testRollbackApexDataDirectories_DeSys() throws Exception { + pushTestApex(); + + // Push files to apex data directory + String oldFilePath1 = apexDataDirDeSys(APK_IN_APEX_TESTAPEX_NAME) + "/" + TEST_FILENAME_1; + String oldFilePath2 = + apexDataDirDeSys(APK_IN_APEX_TESTAPEX_NAME) + TEST_SUBDIR + TEST_FILENAME_2; + assertTrue(getDevice().pushString(TEST_STRING_1, oldFilePath1)); + assertTrue(getDevice().pushString(TEST_STRING_2, oldFilePath2)); + + // Install new version of the APEX with rollback enabled + runPhase("testRollbackApexDataDirectories_Phase1"); getDevice().reboot(); - getDevice().waitForDeviceAvailable(); - // Verify rollback was enabled - runPhase("assertNetworkStackRollbackAvailable"); + // Replace files in data directory + getDevice().deleteFile(oldFilePath1); + getDevice().deleteFile(oldFilePath2); + String newFilePath3 = apexDataDirDeSys(APK_IN_APEX_TESTAPEX_NAME) + "/" + TEST_FILENAME_3; + String newFilePath4 = + apexDataDirDeSys(APK_IN_APEX_TESTAPEX_NAME) + TEST_SUBDIR + TEST_FILENAME_4; + assertTrue(getDevice().pushString(TEST_STRING_3, newFilePath3)); + assertTrue(getDevice().pushString(TEST_STRING_4, newFilePath4)); + + // Roll back the APEX + runPhase("testRollbackApexDataDirectories_Phase2"); + getDevice().reboot(); - // Connect to internet so network health check passes - getDevice().executeShellCommand("svc wifi enable"); - getDevice().executeShellCommand("svc data enable"); + // Verify that old files have been restored and new files are gone + assertEquals(TEST_STRING_1, getDevice().pullFileContents(oldFilePath1)); + assertEquals(TEST_STRING_2, getDevice().pullFileContents(oldFilePath2)); + assertNull(getDevice().pullFile(newFilePath3)); + assertNull(getDevice().pullFile(newFilePath4)); + } - // Wait for device available because emulator device may restart after turning - // on mobile data - getDevice().waitForDeviceAvailable(); + /** + * Tests that data in DE (user) apex data directory is restored when apex is rolled back. + */ + @Test + public void testRollbackApexDataDirectories_DeUser() throws Exception { + pushTestApex(); + + // Push files to apex data directory + String oldFilePath1 = apexDataDirDeUser( + APK_IN_APEX_TESTAPEX_NAME, 0) + "/" + TEST_FILENAME_1; + String oldFilePath2 = + apexDataDirDeUser(APK_IN_APEX_TESTAPEX_NAME, 0) + TEST_SUBDIR + TEST_FILENAME_2; + assertTrue(getDevice().pushString(TEST_STRING_1, oldFilePath1)); + assertTrue(getDevice().pushString(TEST_STRING_2, oldFilePath2)); + + // Install new version of the APEX with rollback enabled + runPhase("testRollbackApexDataDirectories_Phase1"); + getDevice().reboot(); + + // Replace files in data directory + getDevice().deleteFile(oldFilePath1); + getDevice().deleteFile(oldFilePath2); + String newFilePath3 = + apexDataDirDeUser(APK_IN_APEX_TESTAPEX_NAME, 0) + "/" + TEST_FILENAME_3; + String newFilePath4 = + apexDataDirDeUser(APK_IN_APEX_TESTAPEX_NAME, 0) + TEST_SUBDIR + TEST_FILENAME_4; + assertTrue(getDevice().pushString(TEST_STRING_3, newFilePath3)); + assertTrue(getDevice().pushString(TEST_STRING_4, newFilePath4)); + + // Roll back the APEX + runPhase("testRollbackApexDataDirectories_Phase2"); + getDevice().reboot(); - // Sleep for > health check deadline - Thread.sleep(310000); - // Verify rollback was not executed after health check deadline - runPhase("assertNoNetworkStackRollbackCommitted"); + // Verify that old files have been restored and new files are gone + assertEquals(TEST_STRING_1, getDevice().pullFileContents(oldFilePath1)); + assertEquals(TEST_STRING_2, getDevice().pullFileContents(oldFilePath2)); + assertNull(getDevice().pullFile(newFilePath3)); + assertNull(getDevice().pullFile(newFilePath4)); } /** - * Tests rolling back user data where there are multiple rollbacks for that package. + * Tests that data in CE apex data directory is restored when apex is rolled back. */ @Test - public void testPreviouslyAbandonedRollbacks() throws Exception { - runPhase("testPreviouslyAbandonedRollbacksEnableRollback"); + public void testRollbackApexDataDirectories_Ce() throws Exception { + pushTestApex(); + + // Push files to apex data directory + String oldFilePath1 = apexDataDirCe(APK_IN_APEX_TESTAPEX_NAME, 0) + "/" + TEST_FILENAME_1; + String oldFilePath2 = + apexDataDirCe(APK_IN_APEX_TESTAPEX_NAME, 0) + TEST_SUBDIR + TEST_FILENAME_2; + assertTrue(getDevice().pushString(TEST_STRING_1, oldFilePath1)); + assertTrue(getDevice().pushString(TEST_STRING_2, oldFilePath2)); + + // Install new version of the APEX with rollback enabled + runPhase("testRollbackApexDataDirectories_Phase1"); getDevice().reboot(); - runPhase("testPreviouslyAbandonedRollbacksCommitRollback"); + + // Replace files in data directory + getDevice().deleteFile(oldFilePath1); + getDevice().deleteFile(oldFilePath2); + String newFilePath3 = apexDataDirCe(APK_IN_APEX_TESTAPEX_NAME, 0) + "/" + TEST_FILENAME_3; + String newFilePath4 = + apexDataDirCe(APK_IN_APEX_TESTAPEX_NAME, 0) + TEST_SUBDIR + TEST_FILENAME_4; + assertTrue(getDevice().pushString(TEST_STRING_3, newFilePath3)); + assertTrue(getDevice().pushString(TEST_STRING_4, newFilePath4)); + + // Roll back the APEX + runPhase("testRollbackApexDataDirectories_Phase2"); getDevice().reboot(); - runPhase("testPreviouslyAbandonedRollbacksCheckUserdataRollback"); + + // Verify that old files have been restored and new files are gone + assertEquals(TEST_STRING_1, getDevice().pullFileContents(oldFilePath1)); + assertEquals(TEST_STRING_2, getDevice().pullFileContents(oldFilePath2)); + assertNull(getDevice().pullFile(newFilePath3)); + assertNull(getDevice().pullFile(newFilePath4)); + } + + private void pushTestApex() throws Exception { + CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(getBuild()); + final String fileName = APK_IN_APEX_TESTAPEX_NAME + "_v1.apex"; + final File apex = buildHelper.getTestFile(fileName); + if (!getDevice().isAdbRoot()) { + getDevice().enableAdbRoot(); + } + getDevice().remountSystemWritable(); + assertTrue(getDevice().pushFile(apex, "/system/apex/" + fileName)); + getDevice().reboot(); + } + + private static String apexDataDirDeSys(String apexName) { + return String.format("/data/misc/apexdata/%s", apexName); + } + + private static String apexDataDirDeUser(String apexName, int userId) { + return String.format("/data/misc_de/%d/apexdata/%s", userId, apexName); + } + + private static String apexDataDirCe(String apexName, int userId) { + return String.format("/data/misc_ce/%d/apexdata/%s", userId, apexName); } private void crashProcess(String processName, int numberOfCrashes) throws Exception { @@ -220,8 +453,24 @@ public class StagedRollbackTest extends BaseHostJUnit4Test { } } - private String getNetworkStackPath() throws DeviceNotAvailableException { - // Find the NetworkStack path (can be NetworkStack.apk or NetworkStackNext.apk) - return getDevice().executeShellCommand("ls /system/priv-app/NetworkStack*/*.apk"); + private boolean isCheckpointSupported() throws Exception { + try { + runPhase("isCheckpointSupported"); + return true; + } catch (AssertionError ignore) { + return false; + } + } + + /** + * True if this build has mainline modules installed. + */ + private boolean hasMainlineModule() throws Exception { + try { + runPhase("hasMainlineModule"); + return true; + } catch (AssertionError ignore) { + return false; + } } } diff --git a/tests/RollbackTest/TEST_MAPPING b/tests/RollbackTest/TEST_MAPPING index fefde5b4be12..0f4c4603f9b4 100644 --- a/tests/RollbackTest/TEST_MAPPING +++ b/tests/RollbackTest/TEST_MAPPING @@ -7,6 +7,9 @@ "name": "StagedRollbackTest" }, { + "name": "NetworkStagedRollbackTest" + }, + { "name": "MultiUserRollbackTest" } ] diff --git a/tests/RollbackTest/lib/src/com/android/tests/rollback/host/WatchdogEventLogger.java b/tests/RollbackTest/lib/src/com/android/tests/rollback/host/WatchdogEventLogger.java new file mode 100644 index 000000000000..88731504eafe --- /dev/null +++ b/tests/RollbackTest/lib/src/com/android/tests/rollback/host/WatchdogEventLogger.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2020 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 com.android.tradefed.device.ITestDevice; +import com.android.tradefed.device.LogcatReceiver; +import com.android.tradefed.result.InputStreamSource; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +public class WatchdogEventLogger { + private LogcatReceiver mReceiver; + + public void start(ITestDevice device) { + mReceiver = new LogcatReceiver(device, "logcat -s WatchdogRollbackLogger", + device.getOptions().getMaxLogcatDataSize(), 0); + mReceiver.start(); + } + + public void stop() { + if (mReceiver != null) { + mReceiver.stop(); + mReceiver.clear(); + } + } + + /** + * Returns a list of all Watchdog logging events which have occurred. + */ + public List<String> getWatchdogLoggingEvents() throws Exception { + try (InputStreamSource logcatStream = mReceiver.getLogcatData()) { + return getWatchdogLoggingEvents(logcatStream); + } + } + + private static List<String> getWatchdogLoggingEvents(InputStreamSource inputStreamSource) + throws Exception { + List<String> watchdogEvents = new ArrayList<>(); + InputStream inputStream = inputStreamSource.createInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + String line; + while ((line = reader.readLine()) != null) { + if (line.contains("Watchdog event occurred")) { + watchdogEvents.add(line); + } + } + return watchdogEvents; + } + + /** + * Returns whether a Watchdog event has occurred that matches the given criteria. + * + * Check the value of all non-null parameters against the list of Watchdog events that have + * occurred, and return {@code true} if an event exists which matches all criteria. + */ + public static boolean watchdogEventOccurred(List<String> loggingEvents, + String type, String logPackage, + String rollbackReason, String failedPackageName) throws Exception { + List<String> eventCriteria = new ArrayList<>(); + if (type != null) { + eventCriteria.add("type: " + type); + } + if (logPackage != null) { + eventCriteria.add("logPackage: " + logPackage); + } + if (rollbackReason != null) { + eventCriteria.add("rollbackReason: " + rollbackReason); + } + if (failedPackageName != null) { + eventCriteria.add("failedPackageName: " + failedPackageName); + } + for (String loggingEvent: loggingEvents) { + boolean matchesCriteria = true; + for (String criterion: eventCriteria) { + if (!loggingEvent.contains(criterion)) { + matchesCriteria = false; + } + } + if (matchesCriteria) { + return true; + } + } + return false; + } +} diff --git a/tests/RollbackTest/testdata/AndroidManifest.xml b/tests/RollbackTest/testdata/AndroidManifest.xml new file mode 100644 index 000000000000..f21ec899eb69 --- /dev/null +++ b/tests/RollbackTest/testdata/AndroidManifest.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.apex.apkrollback.test"> + <!-- APEX does not have classes.dex --> + <application android:hasCode="false" /> + <uses-sdk android:minSdkVersion="28" android:targetSdkVersion="29"/> +</manifest> + diff --git a/tests/RollbackTest/testdata/manifest_v1.json b/tests/RollbackTest/testdata/manifest_v1.json new file mode 100644 index 000000000000..1762fc6764cf --- /dev/null +++ b/tests/RollbackTest/testdata/manifest_v1.json @@ -0,0 +1,4 @@ +{ + "name": "com.android.apex.apkrollback.test", + "version": 1 +} diff --git a/tests/RollbackTest/testdata/manifest_v2.json b/tests/RollbackTest/testdata/manifest_v2.json new file mode 100644 index 000000000000..c5127b9c3023 --- /dev/null +++ b/tests/RollbackTest/testdata/manifest_v2.json @@ -0,0 +1,4 @@ +{ + "name": "com.android.apex.apkrollback.test", + "version": 2 +} diff --git a/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerTestService.java b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerTestService.java index b185a26bb972..9324ba0b8b72 100644 --- a/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerTestService.java +++ b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerTestService.java @@ -250,10 +250,12 @@ public class SoundTriggerTestService extends Service { boolean status = mSoundTriggerUtil.addOrUpdateSoundModel(soundModel); if (status) { - postToast("Successfully loaded " + modelInfo.name + ", UUID=" + soundModel.uuid); + postToast("Successfully loaded " + modelInfo.name + ", UUID=" + + soundModel.getUuid()); setModelState(modelInfo, "Loaded"); } else { - postErrorToast("Failed to load " + modelInfo.name + ", UUID=" + soundModel.uuid + "!"); + postErrorToast("Failed to load " + modelInfo.name + ", UUID=" + + soundModel.getUuid() + "!"); setModelState(modelInfo, "Failed to load"); } } @@ -275,11 +277,12 @@ public class SoundTriggerTestService extends Service { modelInfo.detector = null; boolean status = mSoundTriggerUtil.deleteSoundModel(modelUuid); if (status) { - postToast("Successfully unloaded " + modelInfo.name + ", UUID=" + soundModel.uuid); + postToast("Successfully unloaded " + modelInfo.name + ", UUID=" + + soundModel.getUuid()); setModelState(modelInfo, "Unloaded"); } else { postErrorToast("Failed to unload " + - modelInfo.name + ", UUID=" + soundModel.uuid + "!"); + modelInfo.name + ", UUID=" + soundModel.getUuid() + "!"); setModelState(modelInfo, "Failed to unload"); } } @@ -299,7 +302,8 @@ public class SoundTriggerTestService extends Service { GenericSoundModel updated = createNewSoundModel(modelInfo); boolean status = mSoundTriggerUtil.addOrUpdateSoundModel(updated); if (status) { - postToast("Successfully reloaded " + modelInfo.name + ", UUID=" + modelInfo.modelUuid); + postToast("Successfully reloaded " + modelInfo.name + ", UUID=" + + modelInfo.modelUuid); setModelState(modelInfo, "Reloaded"); } else { postErrorToast("Failed to reload " @@ -321,7 +325,8 @@ public class SoundTriggerTestService extends Service { modelUuid, new DetectorCallback(modelInfo)); } - postMessage("Starting recognition for " + modelInfo.name + ", UUID=" + modelInfo.modelUuid); + postMessage("Starting recognition for " + modelInfo.name + ", UUID=" + + modelInfo.modelUuid); if (modelInfo.detector.startRecognition(modelInfo.captureAudio ? SoundTriggerDetector.RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO : SoundTriggerDetector.RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS)) { @@ -495,7 +500,8 @@ public class SoundTriggerTestService extends Service { if (properties.containsKey("dataFile")) { File modelDataFile = new File( - getFilesDir().getPath() + "/" + properties.getProperty("dataFile")); + getFilesDir().getPath() + "/" + + properties.getProperty("dataFile")); modelInfo.modelData = new byte[(int) modelDataFile.length()]; FileInputStream input = new FileInputStream(modelDataFile); input.read(modelInfo.modelData, 0, modelInfo.modelData.length); @@ -602,12 +608,14 @@ public class SoundTriggerTestService extends Service { FileOutputStream fos = null; try { fos = new FileOutputStream( new File( - getFilesDir() + File.separator + mModelInfo.name.replace(' ', '_') + - "_capture_" + format.getChannelCount() + "ch_" + - format.getSampleRate() + "hz_" + encoding + ".pcm")); + getFilesDir() + File.separator + + mModelInfo.name.replace(' ', '_') + + "_capture_" + format.getChannelCount() + "ch_" + + format.getSampleRate() + "hz_" + encoding + ".pcm")); } catch (IOException e) { Log.e(TAG, "Failed to open output for saving PCM data", e); - postErrorToast("Failed to open output for saving PCM data: " + e.getMessage()); + postErrorToast("Failed to open output for saving PCM data: " + + e.getMessage()); } // Inform the user we're recording. @@ -690,7 +698,8 @@ public class SoundTriggerTestService extends Service { AudioFormat format = event.getCaptureAudioFormat(); result = result + "AudioFormat: " + ((format == null) ? "null" : format.toString()); byte[] triggerAudio = event.getTriggerAudio(); - result = result + ", TriggerAudio: " + (triggerAudio == null ? "null" : triggerAudio.length); + result = result + ", TriggerAudio: " + + (triggerAudio == null ? "null" : triggerAudio.length); byte[] data = event.getData(); result = result + ", Data: " + (data == null ? "null" : data.length); if (data != null) { diff --git a/tests/SoundTriggerTests/src/android/hardware/soundtrigger/SoundTriggerTest.java b/tests/SoundTriggerTests/src/android/hardware/soundtrigger/SoundTriggerTest.java index 65a3d8a337db..e36f398c53ea 100644 --- a/tests/SoundTriggerTests/src/android/hardware/soundtrigger/SoundTriggerTest.java +++ b/tests/SoundTriggerTests/src/android/hardware/soundtrigger/SoundTriggerTest.java @@ -16,7 +16,6 @@ package android.hardware.soundtrigger; -import android.hardware.soundtrigger.SoundTrigger; import android.hardware.soundtrigger.SoundTrigger.ConfidenceLevel; import android.hardware.soundtrigger.SoundTrigger.Keyphrase; import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionEvent; @@ -30,6 +29,7 @@ import android.test.suitebuilder.annotation.LargeTest; import android.test.suitebuilder.annotation.SmallTest; import java.util.Arrays; +import java.util.Locale; import java.util.Random; import java.util.UUID; @@ -38,7 +38,8 @@ public class SoundTriggerTest extends InstrumentationTestCase { @SmallTest public void testKeyphraseParcelUnparcel_noUsers() throws Exception { - Keyphrase keyphrase = new Keyphrase(1, 0, "en-US", "hello", null); + Keyphrase keyphrase = new Keyphrase(1, 0, + Locale.forLanguageTag("en-US"), "hello", null); // Write to a parcel Parcel parcel = Parcel.obtain(); @@ -49,15 +50,16 @@ public class SoundTriggerTest extends InstrumentationTestCase { Keyphrase unparceled = Keyphrase.CREATOR.createFromParcel(parcel); // Verify that they are the same - assertEquals(keyphrase.id, unparceled.id); - assertNull(unparceled.users); - assertEquals(keyphrase.locale, unparceled.locale); - assertEquals(keyphrase.text, unparceled.text); + assertEquals(keyphrase.getId(), unparceled.getId()); + assertNull(unparceled.getUsers()); + assertEquals(keyphrase.getLocale(), unparceled.getLocale()); + assertEquals(keyphrase.getText(), unparceled.getText()); } @SmallTest public void testKeyphraseParcelUnparcel_zeroUsers() throws Exception { - Keyphrase keyphrase = new Keyphrase(1, 0, "en-US", "hello", new int[0]); + Keyphrase keyphrase = new Keyphrase(1, 0, + Locale.forLanguageTag("en-US"), "hello", new int[0]); // Write to a parcel Parcel parcel = Parcel.obtain(); @@ -68,15 +70,16 @@ public class SoundTriggerTest extends InstrumentationTestCase { Keyphrase unparceled = Keyphrase.CREATOR.createFromParcel(parcel); // Verify that they are the same - assertEquals(keyphrase.id, unparceled.id); - assertTrue(Arrays.equals(keyphrase.users, unparceled.users)); - assertEquals(keyphrase.locale, unparceled.locale); - assertEquals(keyphrase.text, unparceled.text); + assertEquals(keyphrase.getId(), unparceled.getId()); + assertTrue(Arrays.equals(keyphrase.getUsers(), unparceled.getUsers())); + assertEquals(keyphrase.getLocale(), unparceled.getLocale()); + assertEquals(keyphrase.getText(), unparceled.getText()); } @SmallTest public void testKeyphraseParcelUnparcel_pos() throws Exception { - Keyphrase keyphrase = new Keyphrase(1, 0, "en-US", "hello", new int[] {1, 2, 3, 4, 5}); + Keyphrase keyphrase = new Keyphrase(1, 0, + Locale.forLanguageTag("en-US"), "hello", new int[] {1, 2, 3, 4, 5}); // Write to a parcel Parcel parcel = Parcel.obtain(); @@ -87,17 +90,19 @@ public class SoundTriggerTest extends InstrumentationTestCase { Keyphrase unparceled = Keyphrase.CREATOR.createFromParcel(parcel); // Verify that they are the same - assertEquals(keyphrase.id, unparceled.id); - assertTrue(Arrays.equals(keyphrase.users, unparceled.users)); - assertEquals(keyphrase.locale, unparceled.locale); - assertEquals(keyphrase.text, unparceled.text); + assertEquals(keyphrase.getId(), unparceled.getId()); + assertTrue(Arrays.equals(keyphrase.getUsers(), unparceled.getUsers())); + assertEquals(keyphrase.getLocale(), unparceled.getLocale()); + assertEquals(keyphrase.getText(), unparceled.getText()); } @SmallTest public void testKeyphraseSoundModelParcelUnparcel_noData() throws Exception { Keyphrase[] keyphrases = new Keyphrase[2]; - keyphrases[0] = new Keyphrase(1, 0, "en-US", "hello", new int[] {0}); - keyphrases[1] = new Keyphrase(2, 0, "fr-FR", "there", new int[] {1, 2}); + keyphrases[0] = new Keyphrase(1, 0, Locale.forLanguageTag("en-US"), + "hello", new int[] {0}); + keyphrases[1] = new Keyphrase(2, 0, Locale.forLanguageTag("fr-FR"), + "there", new int[] {1, 2}); KeyphraseSoundModel ksm = new KeyphraseSoundModel(UUID.randomUUID(), UUID.randomUUID(), null, keyphrases); @@ -110,17 +115,19 @@ public class SoundTriggerTest extends InstrumentationTestCase { KeyphraseSoundModel unparceled = KeyphraseSoundModel.CREATOR.createFromParcel(parcel); // Verify that they are the same - assertEquals(ksm.uuid, unparceled.uuid); - assertNull(unparceled.data); - assertEquals(ksm.type, unparceled.type); - assertTrue(Arrays.equals(keyphrases, unparceled.keyphrases)); + assertEquals(ksm.getUuid(), unparceled.getUuid()); + assertNull(unparceled.getData()); + assertEquals(ksm.getType(), unparceled.getType()); + assertTrue(Arrays.equals(keyphrases, unparceled.getKeyphrases())); } @SmallTest public void testKeyphraseSoundModelParcelUnparcel_zeroData() throws Exception { Keyphrase[] keyphrases = new Keyphrase[2]; - keyphrases[0] = new Keyphrase(1, 0, "en-US", "hello", new int[] {0}); - keyphrases[1] = new Keyphrase(2, 0, "fr-FR", "there", new int[] {1, 2}); + keyphrases[0] = new Keyphrase(1, 0, Locale.forLanguageTag("en-US"), + "hello", new int[] {0}); + keyphrases[1] = new Keyphrase(2, 0, Locale.forLanguageTag("fr-FR"), + "there", new int[] {1, 2}); KeyphraseSoundModel ksm = new KeyphraseSoundModel(UUID.randomUUID(), UUID.randomUUID(), new byte[0], keyphrases); @@ -133,10 +140,10 @@ public class SoundTriggerTest extends InstrumentationTestCase { KeyphraseSoundModel unparceled = KeyphraseSoundModel.CREATOR.createFromParcel(parcel); // Verify that they are the same - assertEquals(ksm.uuid, unparceled.uuid); - assertEquals(ksm.type, unparceled.type); - assertTrue(Arrays.equals(ksm.keyphrases, unparceled.keyphrases)); - assertTrue(Arrays.equals(ksm.data, unparceled.data)); + assertEquals(ksm.getUuid(), unparceled.getUuid()); + assertEquals(ksm.getType(), unparceled.getType()); + assertTrue(Arrays.equals(ksm.getKeyphrases(), unparceled.getKeyphrases())); + assertTrue(Arrays.equals(ksm.getData(), unparceled.getData())); } @SmallTest @@ -155,10 +162,10 @@ public class SoundTriggerTest extends InstrumentationTestCase { KeyphraseSoundModel unparceled = KeyphraseSoundModel.CREATOR.createFromParcel(parcel); // Verify that they are the same - assertEquals(ksm.uuid, unparceled.uuid); - assertEquals(ksm.type, unparceled.type); - assertNull(unparceled.keyphrases); - assertTrue(Arrays.equals(ksm.data, unparceled.data)); + assertEquals(ksm.getUuid(), unparceled.getUuid()); + assertEquals(ksm.getType(), unparceled.getType()); + assertNull(unparceled.getKeyphrases()); + assertTrue(Arrays.equals(ksm.getData(), unparceled.getData())); } @SmallTest @@ -177,17 +184,19 @@ public class SoundTriggerTest extends InstrumentationTestCase { KeyphraseSoundModel unparceled = KeyphraseSoundModel.CREATOR.createFromParcel(parcel); // Verify that they are the same - assertEquals(ksm.uuid, unparceled.uuid); - assertEquals(ksm.type, unparceled.type); - assertTrue(Arrays.equals(ksm.keyphrases, unparceled.keyphrases)); - assertTrue(Arrays.equals(ksm.data, unparceled.data)); + assertEquals(ksm.getUuid(), unparceled.getUuid()); + assertEquals(ksm.getType(), unparceled.getType()); + assertTrue(Arrays.equals(ksm.getKeyphrases(), unparceled.getKeyphrases())); + assertTrue(Arrays.equals(ksm.getData(), unparceled.getData())); } @LargeTest public void testKeyphraseSoundModelParcelUnparcel_largeData() throws Exception { Keyphrase[] keyphrases = new Keyphrase[2]; - keyphrases[0] = new Keyphrase(1, 0, "en-US", "hello", new int[] {0}); - keyphrases[1] = new Keyphrase(2, 0, "fr-FR", "there", new int[] {1, 2}); + keyphrases[0] = new Keyphrase(1, 0, Locale.forLanguageTag("en-US"), + "hello", new int[] {0}); + keyphrases[1] = new Keyphrase(2, 0, Locale.forLanguageTag("fr-FR"), + "there", new int[] {1, 2}); byte[] data = new byte[200 * 1024]; mRandom.nextBytes(data); KeyphraseSoundModel ksm = new KeyphraseSoundModel(UUID.randomUUID(), UUID.randomUUID(), @@ -202,10 +211,10 @@ public class SoundTriggerTest extends InstrumentationTestCase { KeyphraseSoundModel unparceled = KeyphraseSoundModel.CREATOR.createFromParcel(parcel); // Verify that they are the same - assertEquals(ksm.uuid, unparceled.uuid); - assertEquals(ksm.type, unparceled.type); - assertTrue(Arrays.equals(ksm.data, unparceled.data)); - assertTrue(Arrays.equals(ksm.keyphrases, unparceled.keyphrases)); + assertEquals(ksm.getUuid(), unparceled.getUuid()); + assertEquals(ksm.getType(), unparceled.getType()); + assertTrue(Arrays.equals(ksm.getData(), unparceled.getData())); + assertTrue(Arrays.equals(ksm.getKeyphrases(), unparceled.getKeyphrases())); } @SmallTest diff --git a/tests/SoundTriggerTests/src/android/hardware/soundtrigger/stubhal/GenericSoundModelTest.java b/tests/SoundTriggerTests/src/android/hardware/soundtrigger/stubhal/GenericSoundModelTest.java index c0583ceb2fab..2c3592c640bc 100644 --- a/tests/SoundTriggerTests/src/android/hardware/soundtrigger/stubhal/GenericSoundModelTest.java +++ b/tests/SoundTriggerTests/src/android/hardware/soundtrigger/stubhal/GenericSoundModelTest.java @@ -17,7 +17,6 @@ package android.hardware.soundtrigger; import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.timeout; @@ -37,6 +36,8 @@ import android.test.suitebuilder.annotation.SmallTest; import com.android.internal.app.ISoundTriggerService; +import org.mockito.MockitoAnnotations; + import java.io.DataOutputStream; import java.net.InetAddress; import java.net.Socket; @@ -45,8 +46,6 @@ import java.util.HashSet; import java.util.Random; import java.util.UUID; -import org.mockito.MockitoAnnotations; - public class GenericSoundModelTest extends AndroidTestCase { static final int MSG_DETECTION_ERROR = -1; static final int MSG_DETECTION_RESUME = 0; @@ -96,11 +95,11 @@ public class GenericSoundModelTest extends AndroidTestCase { // Update sound model soundTriggerService.updateSoundModel(model); - loadedModelUuids.add(model.uuid); + loadedModelUuids.add(model.getUuid()); // Confirm it was updated GenericSoundModel returnedModel = - soundTriggerService.getSoundModel(new ParcelUuid(model.uuid)); + soundTriggerService.getSoundModel(new ParcelUuid(model.getUuid())); assertEquals(model, returnedModel); } @@ -110,15 +109,15 @@ public class GenericSoundModelTest extends AndroidTestCase { // Update sound model soundTriggerService.updateSoundModel(model); - loadedModelUuids.add(model.uuid); + loadedModelUuids.add(model.getUuid()); // Delete sound model - soundTriggerService.deleteSoundModel(new ParcelUuid(model.uuid)); - loadedModelUuids.remove(model.uuid); + soundTriggerService.deleteSoundModel(new ParcelUuid(model.getUuid())); + loadedModelUuids.remove(model.getUuid()); // Confirm it was deleted GenericSoundModel returnedModel = - soundTriggerService.getSoundModel(new ParcelUuid(model.uuid)); + soundTriggerService.getSoundModel(new ParcelUuid(model.getUuid())); assertEquals(null, returnedModel); } @@ -134,14 +133,14 @@ public class GenericSoundModelTest extends AndroidTestCase { // Update and start sound model recognition soundTriggerService.updateSoundModel(model); - loadedModelUuids.add(model.uuid); - int r = soundTriggerService.startRecognition(new ParcelUuid(model.uuid), spyCallback, + loadedModelUuids.add(model.getUuid()); + int r = soundTriggerService.startRecognition(new ParcelUuid(model.getUuid()), spyCallback, config); assertEquals("Could Not Start Recognition with code: " + r, android.hardware.soundtrigger.SoundTrigger.STATUS_OK, r); // Stop recognition - r = soundTriggerService.stopRecognition(new ParcelUuid(model.uuid), spyCallback); + r = soundTriggerService.stopRecognition(new ParcelUuid(model.getUuid()), spyCallback); assertEquals("Could Not Stop Recognition with code: " + r, android.hardware.soundtrigger.SoundTrigger.STATUS_OK, r); } @@ -158,13 +157,13 @@ public class GenericSoundModelTest extends AndroidTestCase { // Update and start sound model soundTriggerService.updateSoundModel(model); - loadedModelUuids.add(model.uuid); - soundTriggerService.startRecognition(new ParcelUuid(model.uuid), spyCallback, config); + loadedModelUuids.add(model.getUuid()); + soundTriggerService.startRecognition(new ParcelUuid(model.getUuid()), spyCallback, config); // Send trigger to stub HAL Socket socket = new Socket(InetAddress.getLocalHost(), 14035); DataOutputStream out = new DataOutputStream(socket.getOutputStream()); - out.writeBytes("trig " + model.uuid.toString() + "\r\n"); + out.writeBytes("trig " + model.getUuid().toString() + "\r\n"); out.flush(); socket.close(); @@ -227,11 +226,12 @@ public class GenericSoundModelTest extends AndroidTestCase { if (operation == 0 && modelInfo.status == STATUS_UNLOADED) { // Update and start sound model soundTriggerService.updateSoundModel(modelInfo.model); - loadedModelUuids.add(modelInfo.model.uuid); + loadedModelUuids.add(modelInfo.model.getUuid()); modelInfo.status = STATUS_LOADED; } else if (operation == 1 && modelInfo.status == STATUS_LOADED) { // Start the sound model - int r = soundTriggerService.startRecognition(new ParcelUuid(modelInfo.model.uuid), + int r = soundTriggerService.startRecognition(new ParcelUuid( + modelInfo.model.getUuid()), spyCallback, config); assertEquals("Could Not Start Recognition with code: " + r, android.hardware.soundtrigger.SoundTrigger.STATUS_OK, r); @@ -240,7 +240,7 @@ public class GenericSoundModelTest extends AndroidTestCase { // Send trigger to stub HAL Socket socket = new Socket(InetAddress.getLocalHost(), 14035); DataOutputStream out = new DataOutputStream(socket.getOutputStream()); - out.writeBytes("trig " + modelInfo.model.uuid + "\r\n"); + out.writeBytes("trig " + modelInfo.model.getUuid() + "\r\n"); out.flush(); socket.close(); @@ -249,19 +249,20 @@ public class GenericSoundModelTest extends AndroidTestCase { reset(spyCallback); } else if (operation == 3 && modelInfo.status == STATUS_STARTED) { // Stop recognition - int r = soundTriggerService.stopRecognition(new ParcelUuid(modelInfo.model.uuid), + int r = soundTriggerService.stopRecognition(new ParcelUuid( + modelInfo.model.getUuid()), spyCallback); assertEquals("Could Not Stop Recognition with code: " + r, android.hardware.soundtrigger.SoundTrigger.STATUS_OK, r); modelInfo.status = STATUS_LOADED; } else if (operation == 4 && modelInfo.status != STATUS_UNLOADED) { // Delete sound model - soundTriggerService.deleteSoundModel(new ParcelUuid(modelInfo.model.uuid)); - loadedModelUuids.remove(modelInfo.model.uuid); + soundTriggerService.deleteSoundModel(new ParcelUuid(modelInfo.model.getUuid())); + loadedModelUuids.remove(modelInfo.model.getUuid()); // Confirm it was deleted - GenericSoundModel returnedModel = - soundTriggerService.getSoundModel(new ParcelUuid(modelInfo.model.uuid)); + GenericSoundModel returnedModel = soundTriggerService.getSoundModel( + new ParcelUuid(modelInfo.model.getUuid())); assertEquals(null, returnedModel); modelInfo.status = STATUS_UNLOADED; } diff --git a/tests/StatusBar/src/com/android/statusbartest/StatusBarTest.java b/tests/StatusBar/src/com/android/statusbartest/StatusBarTest.java index cd04c2e197f9..3d72ee67a227 100644 --- a/tests/StatusBar/src/com/android/statusbartest/StatusBarTest.java +++ b/tests/StatusBar/src/com/android/statusbartest/StatusBarTest.java @@ -18,13 +18,13 @@ package com.android.statusbartest; import android.app.Notification; import android.app.NotificationManager; -import android.view.View; -import android.content.Intent; import android.app.PendingIntent; import android.app.StatusBarManager; +import android.content.Intent; import android.os.Handler; -import android.util.Log; import android.os.SystemClock; +import android.util.Log; +import android.view.View; import android.view.Window; import android.view.WindowManager; diff --git a/tests/SurfaceComposition/src/android/surfacecomposition/SurfaceCompositionMeasuringActivity.java b/tests/SurfaceComposition/src/android/surfacecomposition/SurfaceCompositionMeasuringActivity.java index 4771b6cfc750..e0a96689c191 100644 --- a/tests/SurfaceComposition/src/android/surfacecomposition/SurfaceCompositionMeasuringActivity.java +++ b/tests/SurfaceComposition/src/android/surfacecomposition/SurfaceCompositionMeasuringActivity.java @@ -23,7 +23,6 @@ import android.app.ActionBar; import android.app.Activity; import android.app.ActivityManager; import android.app.ActivityManager.MemoryInfo; -import android.content.Context; import android.content.pm.PackageManager; import android.graphics.Color; import android.graphics.PixelFormat; @@ -531,8 +530,7 @@ public class SurfaceCompositionMeasuringActivity extends Activity implements OnC } private void detectRefreshRate() { - WindowManager wm = (WindowManager)getSystemService(Context.WINDOW_SERVICE); - mRefreshRate = wm.getDefaultDisplay().getRefreshRate(); + mRefreshRate = getDisplay().getRefreshRate(); if (mRefreshRate < MIN_REFRESH_RATE_SUPPORTED) throw new RuntimeException("Unsupported display refresh rate: " + mRefreshRate); mTargetFPS = mRefreshRate - 2.0f; diff --git a/tests/SurfaceControlViewHostTest/Android.bp b/tests/SurfaceControlViewHostTest/Android.bp new file mode 100644 index 000000000000..e4e060010eea --- /dev/null +++ b/tests/SurfaceControlViewHostTest/Android.bp @@ -0,0 +1,22 @@ +// +// 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 { + name: "SurfaceControlViewHostTest", + srcs: ["**/*.java"], + platform_apis: true, + certificate: "platform", +} diff --git a/tests/SurfaceControlViewHostTest/AndroidManifest.xml b/tests/SurfaceControlViewHostTest/AndroidManifest.xml new file mode 100644 index 000000000000..ee95763453f7 --- /dev/null +++ b/tests/SurfaceControlViewHostTest/AndroidManifest.xml @@ -0,0 +1,28 @@ +<?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.test.viewembed"> + + <application> + <activity android:name="SurfaceControlViewHostTest" android:label="View Embedding Test"> + <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/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostTest.java b/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostTest.java new file mode 100644 index 000000000000..3bc530975580 --- /dev/null +++ b/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostTest.java @@ -0,0 +1,86 @@ +/* + * 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.viewembed; + +import android.app.Activity; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.os.Bundle; +import android.view.Gravity; +import android.view.SurfaceControl; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.WindowManager; +import android.view.SurfaceControlViewHost; +import android.widget.Button; +import android.widget.FrameLayout; + + +public class SurfaceControlViewHostTest extends Activity implements SurfaceHolder.Callback{ + SurfaceView mView; + SurfaceControlViewHost mVr; + + protected void onCreate(Bundle savedInstanceState) { + FrameLayout content = new FrameLayout(this); + super.onCreate(savedInstanceState); + mView = new SurfaceView(this); + content.addView(mView, new FrameLayout.LayoutParams( + 500, 500, Gravity.CENTER_HORIZONTAL | Gravity.TOP)); + setContentView(content); + + mView.setZOrderOnTop(true); + mView.getHolder().addCallback(this); + + addEmbeddedView(); + } + + void addEmbeddedView() { + mVr = new SurfaceControlViewHost(this, this.getDisplay(), + mView.getHostToken()); + + mView.setChildSurfacePackage(mVr.getSurfacePackage()); + + Button v = new Button(this); + v.setBackgroundColor(Color.BLUE); + v.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + v.setBackgroundColor(Color.RED); + } + }); + WindowManager.LayoutParams lp = + new WindowManager.LayoutParams(500, 500, WindowManager.LayoutParams.TYPE_APPLICATION, + 0, PixelFormat.OPAQUE); + mVr.setView(v, lp); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + Canvas canvas = holder.lockCanvas(); + canvas.drawColor(Color.GREEN); + holder.unlockCanvasAndPost(canvas); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + } +} diff --git a/tests/WindowManagerStressTest/Android.bp b/tests/TaskOrganizerTest/Android.bp index 98749a7e4512..8a13dbc52c66 100644 --- a/tests/WindowManagerStressTest/Android.bp +++ b/tests/TaskOrganizerTest/Android.bp @@ -1,5 +1,5 @@ // -// Copyright (C) 2016 The Android Open Source Project +// 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. @@ -15,7 +15,8 @@ // android_test { - name: "WindowManagerStressTest", + name: "TaskOrganizerTest", srcs: ["**/*.java"], platform_apis: true, + certificate: "platform", } diff --git a/tests/TaskOrganizerTest/AndroidManifest.xml b/tests/TaskOrganizerTest/AndroidManifest.xml new file mode 100644 index 000000000000..a77d7ee80242 --- /dev/null +++ b/tests/TaskOrganizerTest/AndroidManifest.xml @@ -0,0 +1,29 @@ +<?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.test.taskembed"> + <uses-permission android:name="android.permission.CHANGE_CONFIGURATION" /> + <uses-permission android:name="android.permission.MANAGE_ACTIVITY_STACKS" /> + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" /> + <application> + <service android:name=".TaskOrganizerPipTest" + android:exported="true"> + </service> + <activity android:name="TaskOrganizerMultiWindowTest" android:label="TaskOrganizer MW Test"> + <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/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerMultiWindowTest.java b/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerMultiWindowTest.java new file mode 100644 index 000000000000..073ae30aaf1a --- /dev/null +++ b/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerMultiWindowTest.java @@ -0,0 +1,204 @@ +/* + * 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.taskembed; + +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; + +import android.app.Activity; +import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.graphics.Rect; +import android.os.Bundle; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.SurfaceControl; +import android.view.SurfaceHolder; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.window.TaskOrganizer; +import android.window.WindowContainerTransaction; +import android.window.WindowContainerTransactionCallback; + +public class TaskOrganizerMultiWindowTest extends Activity { + static class SplitLayout extends LinearLayout implements View.OnTouchListener { + View mView1; + View mView2; + View mDividerView; + + public boolean onTouch(View v, MotionEvent e) { + if (e.getAction() != MotionEvent.ACTION_UP) { + return true; + } + + float x = e.getRawX(0); + float ratio = (float) x / (float) getWidth() ; + ratio = 1-ratio; + + LinearLayout.LayoutParams lp1 = + new LinearLayout.LayoutParams(0, + ViewGroup.LayoutParams.WRAP_CONTENT, ratio-0.02f); + LinearLayout.LayoutParams lp2 = + new LinearLayout.LayoutParams(0, + ViewGroup.LayoutParams.WRAP_CONTENT, 1-ratio-0.02f); + updateViewLayout(mView1, lp2); + updateViewLayout(mView2, lp1); + return true; + } + + SplitLayout(Context c, View v1, View v2) { + super(c); + LinearLayout.LayoutParams lp1 = + new LinearLayout.LayoutParams(0, + ViewGroup.LayoutParams.WRAP_CONTENT, 0.48f); + LinearLayout.LayoutParams lp3 = + new LinearLayout.LayoutParams(0, + ViewGroup.LayoutParams.WRAP_CONTENT, 0.48f); + LinearLayout.LayoutParams lp2 = + new LinearLayout.LayoutParams(0, + ViewGroup.LayoutParams.FILL_PARENT, 0.04f); + lp2.gravity = Gravity.CENTER; + + setWeightSum(1); + + mView1 = v1; + mView2 = v2; + addView(mView1, lp1); + + mDividerView = new View(getContext()); + mDividerView.setBackgroundColor(Color.BLACK); + addView(mDividerView, lp2); + mDividerView.setOnTouchListener(this); + + addView(mView2, lp3); + } + } + + class ResizingTaskView extends TaskView { + final Intent mIntent; + boolean launched = false; + ResizingTaskView(Context c, Intent i) { + super(c); + mIntent = i; + } + + @Override + public void surfaceChanged(SurfaceHolder h, int format, int width, int height) { + if (!launched) { + launchOrganizedActivity(mIntent, width, height); + launched = true; + } else { + resizeTask(width, height); + } + } + + void resizeTask(int width, int height) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setBounds(mWc, new Rect(0, 0, width, height)); + try { + mOrganizer.applySyncTransaction(wct, mOrganizer.mTransactionCallback); + } catch (Exception e) { + // Oh well + } + } + } + + private TaskView mTaskView1; + private TaskView mTaskView2; + private boolean mGotFirstTask = false; + + class Organizer extends TaskOrganizer { + private int receivedTransactions = 0; + SurfaceControl.Transaction mergedTransaction = new SurfaceControl.Transaction(); + WindowContainerTransactionCallback mTransactionCallback = + new WindowContainerTransactionCallback() { + @Override + public void onTransactionReady(int id, SurfaceControl.Transaction t) { + mergedTransaction.merge(t); + receivedTransactions++; + if (receivedTransactions == 2) { + mergedTransaction.apply(); + receivedTransactions = 0; + } + } + }; + + @Override + public void onTaskAppeared(ActivityManager.RunningTaskInfo ti, SurfaceControl leash) { + if (!mGotFirstTask) { + mTaskView1.reparentTask(ti.token, leash); + mGotFirstTask = true; + } else { + mTaskView2.reparentTask(ti.token, leash); + } + } + } + + private Organizer mOrganizer = new Organizer(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mOrganizer.registerOrganizer(WINDOWING_MODE_MULTI_WINDOW); + + mTaskView1 = new ResizingTaskView(this, makeSettingsIntent()); + mTaskView2 = new ResizingTaskView(this, makeContactsIntent()); + View splitView = new SplitLayout(this, mTaskView1, mTaskView2); + + setContentView(splitView); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mOrganizer.unregisterOrganizer(); + } + + private void addFlags(Intent intent) { + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION); + } + + private Intent makeSettingsIntent() { + Intent intent = new Intent(); + intent.setAction(android.provider.Settings.ACTION_SETTINGS); + addFlags(intent); + return intent; + } + + private Intent makeContactsIntent() { + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_APP_CONTACTS); + addFlags(intent); + return intent; + } + + private Bundle makeLaunchOptions(int width, int height) { + ActivityOptions o = ActivityOptions.makeBasic(); + o.setLaunchWindowingMode(WINDOWING_MODE_MULTI_WINDOW); + o.setLaunchBounds(new Rect(0, 0, width, height)); + return o.toBundle(); + } + + private void launchOrganizedActivity(Intent i, int width, int height) { + startActivity(i, makeLaunchOptions(width, height)); + } +} diff --git a/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerPipTest.java b/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerPipTest.java new file mode 100644 index 000000000000..8fc5c5d78b60 --- /dev/null +++ b/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerPipTest.java @@ -0,0 +1,82 @@ +/* + * 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.taskembed; + +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; + +import android.app.ActivityManager; +import android.app.Service; +import android.content.Intent; +import android.graphics.Rect; +import android.os.IBinder; +import android.view.SurfaceControl; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.window.TaskOrganizer; +import android.window.WindowContainerTransaction; + +public class TaskOrganizerPipTest extends Service { + private static final int PIP_WIDTH = 640; + private static final int PIP_HEIGHT = 360; + + private TaskView mTaskView; + + class Organizer extends TaskOrganizer { + public void onTaskAppeared(ActivityManager.RunningTaskInfo ti, SurfaceControl leash) { + mTaskView.reparentTask(ti.token, leash); + + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.scheduleFinishEnterPip(ti.token, new Rect(0, 0, PIP_WIDTH, PIP_HEIGHT)); + applyTransaction(wct); + } + } + + private Organizer mOrganizer = new Organizer(); + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + + mOrganizer.registerOrganizer(WINDOWING_MODE_PINNED); + + final WindowManager.LayoutParams wlp = new WindowManager.LayoutParams(); + wlp.setTitle("TaskOrganizerPipTest"); + wlp.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + wlp.width = wlp.height = ViewGroup.LayoutParams.WRAP_CONTENT; + + FrameLayout layout = new FrameLayout(this); + ViewGroup.LayoutParams lp = + new ViewGroup.LayoutParams(PIP_WIDTH, PIP_HEIGHT); + mTaskView = new TaskView(this); + layout.addView(mTaskView, lp); + + WindowManager wm = getSystemService(WindowManager.class); + wm.addView(layout, wlp); + } + + @Override + public void onDestroy() { + super.onDestroy(); + mOrganizer.unregisterOrganizer(); + } +} diff --git a/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskView.java b/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskView.java new file mode 100644 index 000000000000..208018c2543a --- /dev/null +++ b/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskView.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2020 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.taskembed; + +import android.content.Context; +import android.view.SurfaceControl; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.window.WindowContainerToken; + +/** + * Simple SurfaceView wrapper which registers a TaskOrganizer + * after it's Surface is ready. + */ +class TaskView extends SurfaceView implements SurfaceHolder.Callback { + WindowContainerToken mWc; + private SurfaceControl mLeash; + + private boolean mSurfaceCreated = false; + private boolean mNeedsReparent; + + TaskView(Context c) { + super(c); + getHolder().addCallback(this); + setZOrderOnTop(true); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + mSurfaceCreated = true; + if (mNeedsReparent) { + mNeedsReparent = false; + reparentLeash(); + } + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + } + + void reparentTask(WindowContainerToken wc, SurfaceControl leash) { + mWc = wc; + mLeash = leash; + if (!mSurfaceCreated) { + mNeedsReparent = true; + } else { + reparentLeash(); + } + } + + private void reparentLeash() { + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + if (mLeash == null) { + return; + } + + t.reparent(mLeash, getSurfaceControl()) + .setPosition(mLeash, 0, 0) + .show(mLeash) + .apply(); + } +} 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 ba77a74974d1..1664746f4636 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 @@ -73,6 +73,10 @@ class TouchLatencyView extends View implements View.OnTouchListener { mFps = 0; mLastFpsUpdate = 0; mFrameCount = 0; + + mDf = new DecimalFormat("fps: #.##"); + mDf.setRoundingMode(RoundingMode.HALF_UP); + Trace.endSection(); } @@ -181,9 +185,7 @@ class TouchLatencyView extends View implements View.OnTouchListener { // Draw the ball canvas.drawColor(BACKGROUND_COLOR); canvas.drawOval(left, top, right, bottom, getBallColor()); - DecimalFormat df = new DecimalFormat("fps: #.##"); - df.setRoundingMode(RoundingMode.HALF_UP); - canvas.drawText(df.format(mFps), width, 100, mTextPaint); + canvas.drawText(mDf.format(mFps), width, 100, mTextPaint); invalidate(); Trace.endSection(); @@ -220,6 +222,7 @@ class TouchLatencyView extends View implements View.OnTouchListener { private long mLastDrawNano, mLastFpsUpdate, mFrameCount; private float mFps; + private DecimalFormat mDf; } public class TouchLatencyActivity extends Activity { diff --git a/tests/UiBench/AndroidManifest.xml b/tests/UiBench/AndroidManifest.xml index c6b4a54f3b0b..dd255ef5233b 100644 --- a/tests/UiBench/AndroidManifest.xml +++ b/tests/UiBench/AndroidManifest.xml @@ -306,5 +306,14 @@ <category android:name="com.android.test.uibench.TEST" /> </intent-filter> </activity> + + <activity + android:name="WindowInsetsControllerActivity" + android:label="WindowInsetsControllerActivity" > + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="com.android.test.uibench.TEST" /> + </intent-filter> + </activity> </application> </manifest> diff --git a/tests/UiBench/src/com/android/test/uibench/ClippedListActivity.java b/tests/UiBench/src/com/android/test/uibench/ClippedListActivity.java index 2bf6040351b8..e21dec32e7ca 100644 --- a/tests/UiBench/src/com/android/test/uibench/ClippedListActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/ClippedListActivity.java @@ -15,27 +15,24 @@ */ package com.android.test.uibench; -import android.os.Bundle; 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.android.test.uibench.listview.CompatListActivity; import com.google.android.material.navigation.NavigationView; -public class ClippedListActivity extends AppCompatActivity +public class ClippedListActivity extends CompatListActivity implements NavigationView.OnNavigationItemSelectedListener { @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + protected void initializeActivity() { setContentView(R.layout.activity_navigation_drawer); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); @@ -48,15 +45,17 @@ public class ClippedListActivity extends AppCompatActivity NavigationView navigationView = findViewById(R.id.nav_view); navigationView.setNavigationItemSelectedListener(this); + } + + @Override + protected ListAdapter createListAdapter() { + return new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, + TextUtils.buildSimpleStringList(40)); + } - FragmentManager fm = getSupportFragmentManager(); - if (fm.findFragmentById(android.R.id.content) == null) { - ListFragment listFragment = new ListFragment(); - ListAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, - TextUtils.buildSimpleStringList(40)); - listFragment.setListAdapter(adapter); - fm.beginTransaction().add(R.id.app_bar_layout, listFragment).commit(); - } + @Override + protected int getListFragmentContainerViewId() { + return R.id.app_bar_layout; } @Override diff --git a/tests/UiBench/src/com/android/test/uibench/MainActivity.java b/tests/UiBench/src/com/android/test/uibench/MainActivity.java index 0a7aa4281b00..77a6c321acf1 100644 --- a/tests/UiBench/src/com/android/test/uibench/MainActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/MainActivity.java @@ -19,13 +19,15 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Bundle; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.ListFragment; -import androidx.appcompat.app.AppCompatActivity; import android.view.View; +import android.widget.ListAdapter; import android.widget.ListView; import android.widget.SimpleAdapter; +import androidx.fragment.app.ListFragment; + +import com.android.test.uibench.listview.CompatListActivity; + import java.text.Collator; import java.util.ArrayList; import java.util.Collections; @@ -34,10 +36,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -public class MainActivity extends AppCompatActivity { +public class MainActivity extends CompatListActivity { private static final String EXTRA_PATH = "activity_path"; private static final String CATEGORY_HWUI_TEST = "com.android.test.uibench.TEST"; + private String mActivityPath = ""; + public static class TestListFragment extends ListFragment { @Override @SuppressWarnings("unchecked") @@ -56,9 +60,7 @@ public class MainActivity extends AppCompatActivity { } @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - + protected void initializeActivity() { Intent intent = getIntent(); String path = intent.getStringExtra(EXTRA_PATH); @@ -68,15 +70,19 @@ public class MainActivity extends AppCompatActivity { // not root level, display where we are in the hierarchy setTitle(path); } + mActivityPath = path; + } - FragmentManager fm = getSupportFragmentManager(); - if (fm.findFragmentById(android.R.id.content) == null) { - ListFragment listFragment = new TestListFragment(); - listFragment.setListAdapter(new SimpleAdapter(this, getData(path), - android.R.layout.simple_list_item_1, new String[] { "title" }, - new int[] { android.R.id.text1 })); - fm.beginTransaction().add(android.R.id.content, listFragment).commit(); - } + @Override + protected ListAdapter createListAdapter() { + return new SimpleAdapter(this, getData(mActivityPath), + android.R.layout.simple_list_item_1, new String[] { "title" }, + new int[] { android.R.id.text1 }); + } + + @Override + protected ListFragment createListFragment() { + return new TestListFragment(); } protected List<Map<String, Object>> getData(String prefix) { diff --git a/tests/UiBench/src/com/android/test/uibench/ShadowGridActivity.java b/tests/UiBench/src/com/android/test/uibench/ShadowGridActivity.java index af7c65acafd4..d6e1d06ca561 100644 --- a/tests/UiBench/src/com/android/test/uibench/ShadowGridActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/ShadowGridActivity.java @@ -16,13 +16,15 @@ package com.android.test.uibench; import android.os.Bundle; -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.ListAdapter; + +import androidx.fragment.app.ListFragment; -public class ShadowGridActivity extends AppCompatActivity { +import com.android.test.uibench.listview.CompatListActivity; + +public class ShadowGridActivity extends CompatListActivity { public static class NoDividerListFragment extends ListFragment { @Override public void onViewCreated(View view, Bundle savedInstanceState) { @@ -31,18 +33,14 @@ public class ShadowGridActivity extends AppCompatActivity { } }; - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - FragmentManager fm = getSupportFragmentManager(); - if (fm.findFragmentById(android.R.id.content) == null) { - ListFragment listFragment = new NoDividerListFragment(); + protected ListAdapter createListAdapter() { + return new ArrayAdapter<>(this, R.layout.card_row, R.id.card_text, + TextUtils.buildSimpleStringList()); + } - listFragment.setListAdapter(new ArrayAdapter<>(this, - R.layout.card_row, R.id.card_text, TextUtils.buildSimpleStringList())); - fm.beginTransaction().add(android.R.id.content, listFragment).commit(); - } + @Override + protected ListFragment createListFragment() { + return new NoDividerListFragment(); } } diff --git a/tests/UiBench/src/com/android/test/uibench/WindowInsetsControllerActivity.java b/tests/UiBench/src/com/android/test/uibench/WindowInsetsControllerActivity.java new file mode 100644 index 000000000000..e4b89cdd5c8d --- /dev/null +++ b/tests/UiBench/src/com/android/test/uibench/WindowInsetsControllerActivity.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2020 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.uibench; + +import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP; + +import android.os.Bundle; +import android.os.PersistableBundle; +import android.view.WindowInsets; +import android.view.WindowInsetsAnimation; +import android.widget.EditText; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import java.util.List; + +public class WindowInsetsControllerActivity extends AppCompatActivity { + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EditText text = new EditText(this); + text.setText("WindowInsetsController"); + setContentView(text); + getWindow().setDecorFitsSystemWindows(false); + + text.setWindowInsetsAnimationCallback( + new WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) { + @NonNull + @Override + public WindowInsets onProgress(@NonNull WindowInsets insets, + @NonNull List<WindowInsetsAnimation> runningAnimations) { + return WindowInsets.CONSUMED; + } + }); + } +} 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 66595585c525..9a4b5270d5a7 100644 --- a/tests/UiBench/src/com/android/test/uibench/listview/CompatListActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/listview/CompatListActivity.java @@ -16,22 +16,29 @@ package com.android.test.uibench.listview; import android.os.Bundle; +import android.widget.ListAdapter; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; 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 { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + initializeActivity(); + int containerViewId = getListFragmentContainerViewId(); FragmentManager fm = getSupportFragmentManager(); - if (fm.findFragmentById(android.R.id.content) == null) { + Fragment fragment = fm.findFragmentById(containerViewId); + if (fragment == null) { ListFragment listFragment = createListFragment(); listFragment.setListAdapter(createListAdapter()); - fm.beginTransaction().add(android.R.id.content, listFragment).commit(); + fm.beginTransaction().add(containerViewId, listFragment).commit(); + } else if (fragment instanceof ListFragment) { + ((ListFragment) fragment).setListAdapter(createListAdapter()); } } @@ -40,4 +47,11 @@ public abstract class CompatListActivity extends AppCompatActivity { protected ListFragment createListFragment() { return new ListFragment(); } + + protected int getListFragmentContainerViewId() { + return android.R.id.content; + } + + protected void initializeActivity() { + } } 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 bd313ad7636d..2f0d6ab901e2 100644 --- a/tests/UiBench/src/com/android/test/uibench/recyclerview/RvCompatListActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/recyclerview/RvCompatListActivity.java @@ -17,15 +17,16 @@ package com.android.test.uibench.recyclerview; import android.content.Context; import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; 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; import com.android.test.uibench.R; @@ -51,14 +52,21 @@ public abstract class RvCompatListActivity extends AppCompatActivity { super.onCreate(savedInstanceState); FragmentManager fm = getSupportFragmentManager(); - if (fm.findFragmentById(android.R.id.content) == null) { + Fragment existingFragment = fm.findFragmentById(android.R.id.content); + if (existingFragment == null) { RecyclerViewFragment fragment = new RecyclerViewFragment(); - fragment.layoutManager = createLayoutManager(this); - fragment.adapter = createAdapter(); + initializeRecyclerViewFragment(fragment); fm.beginTransaction().add(android.R.id.content, fragment).commit(); + } else if (existingFragment instanceof RecyclerViewFragment) { + initializeRecyclerViewFragment((RecyclerViewFragment) existingFragment); } } + private void initializeRecyclerViewFragment(RecyclerViewFragment fragment) { + fragment.layoutManager = createLayoutManager(this); + fragment.adapter = createAdapter(); + } + protected RecyclerView.LayoutManager createLayoutManager(Context context) { return new LinearLayoutManager(context); } 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 index 7d9d0d52b002..7e8a13470c35 100644 --- a/tests/UsageStatsPerfTests/src/com/android/frameworks/perftests/usage/tests/UsageStatsDatabasePerfTest.java +++ b/tests/UsageStatsPerfTests/src/com/android/frameworks/perftests/usage/tests/UsageStatsDatabasePerfTest.java @@ -30,6 +30,7 @@ import androidx.test.filters.LargeTest; import androidx.test.runner.AndroidJUnit4; import com.android.server.usage.IntervalStats; +import com.android.server.usage.PackagesTokenData; import com.android.server.usage.UsageStatsDatabase; import com.android.server.usage.UsageStatsDatabase.StatCombiner; @@ -79,6 +80,7 @@ public class UsageStatsDatabasePerfTest { sContext = InstrumentationRegistry.getTargetContext(); mTestDir = new File(sContext.getFilesDir(), "UsageStatsDatabasePerfTest"); sUsageStatsDatabase = new UsageStatsDatabase(mTestDir); + sUsageStatsDatabase.readMappingsLocked(); sUsageStatsDatabase.init(1); } @@ -140,6 +142,37 @@ public class UsageStatsDatabasePerfTest { } } + private void runObfuscateStatsTest(int packageCount, int eventsPerPackage) { + 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(); + PackagesTokenData packagesTokenData = new PackagesTokenData(); + intervalStats.obfuscateData(packagesTokenData); + final long endTime = SystemClock.elapsedRealtimeNanos(); + elapsedTimeNs = endTime - startTime; + clearUsageStatsFiles(); + } + } + + private void runDeobfuscateStatsTest(int packageCount, int eventsPerPackage) { + final ManualBenchmarkState benchmarkState = mPerfManualStatusReporter.getBenchmarkState(); + IntervalStats intervalStats = new IntervalStats(); + populateIntervalStats(intervalStats, packageCount, eventsPerPackage); + long elapsedTimeNs = 0; + while (benchmarkState.keepRunning(elapsedTimeNs)) { + PackagesTokenData packagesTokenData = new PackagesTokenData(); + intervalStats.obfuscateData(packagesTokenData); + final long startTime = SystemClock.elapsedRealtimeNanos(); + intervalStats.deobfuscateData(packagesTokenData); + final long endTime = SystemClock.elapsedRealtimeNanos(); + elapsedTimeNs = endTime - startTime; + clearUsageStatsFiles(); + } + } + @Test public void testQueryUsageStats_FewPkgsLightUse() throws IOException { runQueryUsageStatsTest(FEW_PKGS, LIGHT_USE); @@ -151,6 +184,16 @@ public class UsageStatsDatabasePerfTest { } @Test + public void testObfuscateStats_FewPkgsLightUse() { + runObfuscateStatsTest(FEW_PKGS, LIGHT_USE); + } + + @Test + public void testDeobfuscateStats_FewPkgsLightUse() { + runDeobfuscateStatsTest(FEW_PKGS, LIGHT_USE); + } + + @Test public void testQueryUsageStats_FewPkgsHeavyUse() throws IOException { runQueryUsageStatsTest(FEW_PKGS, HEAVY_USE); } @@ -161,6 +204,16 @@ public class UsageStatsDatabasePerfTest { } @Test + public void testObfuscateStats_FewPkgsHeavyUse() { + runObfuscateStatsTest(FEW_PKGS, HEAVY_USE); + } + + @Test + public void testDeobfuscateStats_FewPkgsHeavyUse() { + runDeobfuscateStatsTest(FEW_PKGS, HEAVY_USE); + } + + @Test public void testQueryUsageStats_ManyPkgsLightUse() throws IOException { runQueryUsageStatsTest(MANY_PKGS, LIGHT_USE); } @@ -171,6 +224,16 @@ public class UsageStatsDatabasePerfTest { } @Test + public void testObfuscateStats_ManyPkgsLightUse() { + runObfuscateStatsTest(MANY_PKGS, LIGHT_USE); + } + + @Test + public void testDeobfuscateStats_ManyPkgsLightUse() { + runDeobfuscateStatsTest(MANY_PKGS, LIGHT_USE); + } + + @Test public void testQueryUsageStats_ManyPkgsHeavyUse() throws IOException { runQueryUsageStatsTest(MANY_PKGS, HEAVY_USE); } @@ -179,4 +242,14 @@ public class UsageStatsDatabasePerfTest { public void testPutUsageStats_ManyPkgsHeavyUse() throws IOException { runPutUsageStatsTest(MANY_PKGS, HEAVY_USE); } + + @Test + public void testObfuscateStats_ManyPkgsHeavyUse() { + runObfuscateStatsTest(MANY_PKGS, HEAVY_USE); + } + + @Test + public void testDeobfuscateStats_ManyPkgsHeavyUse() { + runDeobfuscateStatsTest(MANY_PKGS, HEAVY_USE); + } } diff --git a/tests/UsbManagerTests/Android.bp b/tests/UsbManagerTests/Android.bp new file mode 100644 index 000000000000..a03c6e223b74 --- /dev/null +++ b/tests/UsbManagerTests/Android.bp @@ -0,0 +1,32 @@ +// +// 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 { + name: "UsbManagerTests", + srcs: ["src/**/*.java"], + static_libs: [ + "frameworks-base-testutils", + "androidx.test.rules", + "mockito-target-inline-minus-junit4", + "platform-test-annotations", + "truth-prebuilt", + "UsbManagerTestLib", + ], + jni_libs: ["libdexmakerjvmtiagent"], + certificate: "platform", + platform_apis: true, + test_suites: ["device-tests"], +} diff --git a/tests/UsbManagerTests/AndroidManifest.xml b/tests/UsbManagerTests/AndroidManifest.xml new file mode 100644 index 000000000000..4e0b790f6dde --- /dev/null +++ b/tests/UsbManagerTests/AndroidManifest.xml @@ -0,0 +1,29 @@ +<?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.server.usbtest" > + + <uses-permission android:name="android.permission.MANAGE_USB" /> + + <application android:debuggable="true"> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.server.usbtest" + android:label="UsbManagerTests"/> +</manifest> diff --git a/tests/UsbManagerTests/AndroidTest.xml b/tests/UsbManagerTests/AndroidTest.xml new file mode 100644 index 000000000000..c6e22cdc4b37 --- /dev/null +++ b/tests/UsbManagerTests/AndroidTest.xml @@ -0,0 +1,31 @@ +<?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 Frameworks USB API instrumentation Tests."> + <target_preparer class="com.android.tradefed.targetprep.TestFilePushSetup"/> + <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <option name="test-file-name" value="UsbManagerTests.apk" /> + </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"/> + <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"/> + <option name="test-suite-tag" value="apct"/> + <option name="test-tag" value="UsbManagerTests" /> + + <test class="com.android.tradefed.testtype.AndroidJUnitTest"> + <option name="package" value="com.android.server.usbtest"/> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner"/> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration> diff --git a/tests/UsbManagerTests/lib/Android.bp b/tests/UsbManagerTests/lib/Android.bp new file mode 100644 index 000000000000..3c5d91b326d0 --- /dev/null +++ b/tests/UsbManagerTests/lib/Android.bp @@ -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. +// + +android_library { + name: "UsbManagerTestLib", + srcs: ["src/**/*.java"], + static_libs: [ + "frameworks-base-testutils", + "androidx.test.rules", + "mockito-target-inline-minus-junit4", + "platform-test-annotations", + "services.core", + "services.net", + "services.usb", + "truth-prebuilt", + "androidx.core_core", + ], + libs: [ + "android.test.mock", + ], +} diff --git a/tests/WindowManagerStressTest/res/values/colors.xml b/tests/UsbManagerTests/lib/AndroidManifest.xml index 4270ca68a860..c8b301ca0298 100644 --- a/tests/WindowManagerStressTest/res/values/colors.xml +++ b/tests/UsbManagerTests/lib/AndroidManifest.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2016 The Android Open Source Project +<!-- 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. @@ -13,8 +13,10 @@ See the License for the specific language governing permissions and limitations under the License. --> -<resources> - <color name="colorPrimary">#3F51B5</color> - <color name="colorPrimaryDark">#303F9F</color> - <color name="colorAccent">#FF4081</color> -</resources> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.server.usblib"> + + <application/> + +</manifest> diff --git a/tests/UsbManagerTests/lib/src/com/android/server/usblib/UsbManagerTestLib.java b/tests/UsbManagerTests/lib/src/com/android/server/usblib/UsbManagerTestLib.java new file mode 100644 index 000000000000..782439f80fc8 --- /dev/null +++ b/tests/UsbManagerTests/lib/src/com/android/server/usblib/UsbManagerTestLib.java @@ -0,0 +1,129 @@ +/* + * 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.usblib; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.hardware.usb.UsbManager; +import android.os.RemoteException; +import android.util.Log; + +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Unit tests lib for {@link android.hardware.usb.UsbManager}. + */ +public class UsbManagerTestLib { + private static final String TAG = UsbManagerTestLib.class.getSimpleName(); + + private Context mContext; + + private UsbManager mUsbManagerSys; + private UsbManager mUsbManagerMock; + @Mock private android.hardware.usb.IUsbManager mMockUsbService; + + public UsbManagerTestLib(Context context) { + MockitoAnnotations.initMocks(this); + mContext = context; + + assertNotNull(mUsbManagerSys = mContext.getSystemService(UsbManager.class)); + assertNotNull(mUsbManagerMock = new UsbManager(mContext, mMockUsbService)); + } + + private long getCurrentFunctions() { + return mUsbManagerMock.getCurrentFunctions(); + } + + private void setCurrentFunctions(long functions) { + mUsbManagerMock.setCurrentFunctions(functions); + } + + private long getCurrentFunctionsSys() { + return mUsbManagerSys.getCurrentFunctions(); + } + + private void setCurrentFunctionsSys(long functions) { + mUsbManagerSys.setCurrentFunctions(functions); + } + + private void testSetGetCurrentFunctions_Matched(long functions) { + setCurrentFunctions(functions); + assertEquals("CurrentFunctions mismatched: ", functions, getCurrentFunctions()); + } + + private void testGetCurrentFunctionsMock_Matched(long functions) { + try { + when(mMockUsbService.getCurrentFunctions()).thenReturn(functions); + + assertEquals("CurrentFunctions mismatched: ", functions, getCurrentFunctions()); + } catch (RemoteException remEx) { + Log.w(TAG, "RemoteException"); + } + } + + private void testSetCurrentFunctionsMock_Matched(long functions) { + try { + setCurrentFunctions(functions); + + verify(mMockUsbService).setCurrentFunctions(eq(functions)); + } catch (RemoteException remEx) { + Log.w(TAG, "RemoteException"); + } + } + + public void testGetCurrentFunctionsSysEx() throws Exception { + getCurrentFunctionsSys(); + } + + public void testSetCurrentFunctionsSysEx(long functions) throws Exception { + setCurrentFunctionsSys(functions); + } + + public void testGetCurrentFunctionsEx() throws Exception { + getCurrentFunctions(); + + verify(mMockUsbService).getCurrentFunctions(); + } + + public void testSetCurrentFunctionsEx(long functions) throws Exception { + setCurrentFunctions(functions); + + verify(mMockUsbService).setCurrentFunctions(eq(functions)); + } + + public void testGetCurrentFunctions_shouldMatched() { + testGetCurrentFunctionsMock_Matched(UsbManager.FUNCTION_NONE); + testGetCurrentFunctionsMock_Matched(UsbManager.FUNCTION_MTP); + testGetCurrentFunctionsMock_Matched(UsbManager.FUNCTION_PTP); + testGetCurrentFunctionsMock_Matched(UsbManager.FUNCTION_MIDI); + testGetCurrentFunctionsMock_Matched(UsbManager.FUNCTION_RNDIS); + } + + public void testSetCurrentFunctions_shouldMatched() { + testSetCurrentFunctionsMock_Matched(UsbManager.FUNCTION_NONE); + testSetCurrentFunctionsMock_Matched(UsbManager.FUNCTION_MTP); + testSetCurrentFunctionsMock_Matched(UsbManager.FUNCTION_PTP); + testSetCurrentFunctionsMock_Matched(UsbManager.FUNCTION_MIDI); + testSetCurrentFunctionsMock_Matched(UsbManager.FUNCTION_RNDIS); + } +} diff --git a/tests/UsbManagerTests/src/com/android/server/usbtest/UsbManagerApiTest.java b/tests/UsbManagerTests/src/com/android/server/usbtest/UsbManagerApiTest.java new file mode 100644 index 000000000000..8b21763b4a24 --- /dev/null +++ b/tests/UsbManagerTests/src/com/android/server/usbtest/UsbManagerApiTest.java @@ -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.usbtest; + +import android.content.Context; +import android.hardware.usb.UsbManager; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; + +import com.android.server.usblib.UsbManagerTestLib; + +/** + * Unit tests for {@link android.hardware.usb.UsbManager}. + * Note: MUST claimed MANAGE_USB permission in Manifest + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class UsbManagerApiTest { + private Context mContext; + + private final UsbManagerTestLib mUsbManagerTestLib = + new UsbManagerTestLib(mContext = InstrumentationRegistry.getContext()); + + /** + * Verify NO SecurityException + * Go through System Server + */ + @Test + public void testUsbApi_GetCurrentFunctionsSys_shouldNoSecurityException() throws Exception { + mUsbManagerTestLib.testGetCurrentFunctionsSysEx(); + } + + /** + * Verify NO SecurityException + * Go through System Server + */ + @Test + public void testUsbApi_SetCurrentFunctionsSys_shouldNoSecurityException() throws Exception { + mUsbManagerTestLib.testSetCurrentFunctionsSysEx(UsbManager.FUNCTION_NONE); + } + + /** + * Verify NO SecurityException + * Go through Direct API, will not be denied by @RequiresPermission annotation + */ + @Test + public void testUsbApi_GetCurrentFunctions_shouldNoSecurityException() throws Exception { + mUsbManagerTestLib.testGetCurrentFunctionsEx(); + } + + /** + * Verify NO SecurityException + * Go through Direct API, will not be denied by @RequiresPermission annotation + */ + @Test + public void testUsbApi_SetCurrentFunctions_shouldNoSecurityException() throws Exception { + mUsbManagerTestLib.testSetCurrentFunctionsEx(UsbManager.FUNCTION_NONE); + } + + /** + * Verify API path from UsbManager to UsbService + */ + @Test + public void testUsbApi_GetCurrentFunctions_shouldMatched() { + mUsbManagerTestLib.testGetCurrentFunctions_shouldMatched(); + } + + /** + * Verify API path from UsbManager to UsbService + */ + @Test + public void testUsbApi_SetCurrentFunctions_shouldMatched() { + mUsbManagerTestLib.testSetCurrentFunctions_shouldMatched(); + } +} diff --git a/tests/UsbTests/Android.bp b/tests/UsbTests/Android.bp index 1b2cf638f514..7c2be9b63ac3 100644 --- a/tests/UsbTests/Android.bp +++ b/tests/UsbTests/Android.bp @@ -26,6 +26,7 @@ android_test { "services.net", "services.usb", "truth-prebuilt", + "UsbManagerTestLib", ], jni_libs: ["libdexmakerjvmtiagent"], certificate: "platform", diff --git a/tests/UsbTests/src/com/android/server/usb/UsbHandlerTest.java b/tests/UsbTests/src/com/android/server/usb/UsbHandlerTest.java index ca1eb705e457..ef973acf763b 100644 --- a/tests/UsbTests/src/com/android/server/usb/UsbHandlerTest.java +++ b/tests/UsbTests/src/com/android/server/usb/UsbHandlerTest.java @@ -63,6 +63,8 @@ public class UsbHandlerTest { @Mock private UsbSettingsManager mUsbSettingsManager; @Mock + private UsbPermissionManager mUsbPermissionManager; + @Mock private SharedPreferences mSharedPreferences; @Mock private SharedPreferences.Editor mEditor; @@ -87,8 +89,9 @@ public class UsbHandlerTest { Intent mBroadcastedIntent; MockUsbHandler(Looper looper, Context context, UsbDeviceManager deviceManager, - UsbAlsaManager alsaManager, UsbSettingsManager settingsManager) { - super(looper, context, deviceManager, alsaManager, settingsManager); + UsbAlsaManager alsaManager, UsbSettingsManager settingsManager, + UsbPermissionManager permissionManager) { + super(looper, context, deviceManager, alsaManager, permissionManager); mUseUsbNotification = false; mIsUsbTransferAllowed = true; mCurrentUsbFunctionsReceived = true; @@ -142,7 +145,7 @@ public class UsbHandlerTest { mUsbHandler = new MockUsbHandler(FgThread.get().getLooper(), InstrumentationRegistry.getContext(), mUsbDeviceManager, mUsbAlsaManager, - mUsbSettingsManager); + mUsbSettingsManager, mUsbPermissionManager); } @SmallTest @@ -205,7 +208,7 @@ public class UsbHandlerTest { UsbManager.USB_FUNCTION_ADB); mUsbHandler = new MockUsbHandler(FgThread.get().getLooper(), InstrumentationRegistry.getContext(), mUsbDeviceManager, mUsbAlsaManager, - mUsbSettingsManager); + mUsbSettingsManager, mUsbPermissionManager); sendBootCompleteMessages(mUsbHandler); mUsbHandler.handleMessage(mUsbHandler.obtainMessage(MSG_ENABLE_ADB, 0)); @@ -228,7 +231,7 @@ public class UsbHandlerTest { mMockProperties.put(UsbDeviceManager.UsbHandler.USB_PERSISTENT_CONFIG_PROPERTY, "adb"); mUsbHandler = new MockUsbHandler(FgThread.get().getLooper(), InstrumentationRegistry.getContext(), mUsbDeviceManager, mUsbAlsaManager, - mUsbSettingsManager); + mUsbSettingsManager, mUsbPermissionManager); sendBootCompleteMessages(mUsbHandler); assertEquals(mUsbHandler.getEnabledFunctions(), UsbManager.FUNCTION_NONE); @@ -316,7 +319,7 @@ public class UsbHandlerTest { .thenReturn(UsbManager.USB_FUNCTION_MTP); mUsbHandler = new MockUsbHandler(FgThread.get().getLooper(), InstrumentationRegistry.getContext(), mUsbDeviceManager, mUsbAlsaManager, - mUsbSettingsManager); + mUsbSettingsManager, mUsbPermissionManager); sendBootCompleteMessages(mUsbHandler); mUsbHandler.handleMessage(mUsbHandler.obtainMessage(MSG_UPDATE_SCREEN_LOCK, 1)); mUsbHandler.handleMessage(mUsbHandler.obtainMessage(MSG_UPDATE_SCREEN_LOCK, 0)); diff --git a/tests/UsbTests/src/com/android/server/usb/UsbManagerNoPermTest.java b/tests/UsbTests/src/com/android/server/usb/UsbManagerNoPermTest.java new file mode 100644 index 000000000000..a0fd9d40506b --- /dev/null +++ b/tests/UsbTests/src/com/android/server/usb/UsbManagerNoPermTest.java @@ -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.usb; + +import android.content.Context; +import android.hardware.usb.UsbManager; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; + +import com.android.server.usblib.UsbManagerTestLib; + +/** + * Unit tests for {@link android.hardware.usb.UsbManager}. + * Note: NOT claimed MANAGE_USB permission in Manifest + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class UsbManagerNoPermTest { + private Context mContext; + + private final UsbManagerTestLib mUsbManagerTestLib = + new UsbManagerTestLib(mContext = InstrumentationRegistry.getContext()); + + /** + * Verify SecurityException resulting from required permissions missing + * Go through System Server + */ + @Test(expected = SecurityException.class) + public void testUsbApi_GetCurrentFunctionsSys_OnSecurityException() throws Exception { + mUsbManagerTestLib.testGetCurrentFunctionsSysEx(); + } + + /** + * Verify SecurityException resulting from required permissions missing + * Go through System Server + */ + @Test(expected = SecurityException.class) + public void testUsbApi_SetCurrentFunctionsSys_OnSecurityException() throws Exception { + mUsbManagerTestLib.testSetCurrentFunctionsSysEx(UsbManager.FUNCTION_NONE); + } + + /** + * Verify SecurityException resulting from required permissions missing + * Go through Direct API, will not be denied by @RequiresPermission annotation + */ + @Test(expected = SecurityException.class) + @Ignore + public void testUsbApi_GetCurrentFunctions_OnSecurityException() throws Exception { + mUsbManagerTestLib.testGetCurrentFunctionsEx(); + } + + /** + * Verify SecurityException resulting from required permissions missing + * Go through Direct API, will not be denied by @RequiresPermission annotation + */ + @Test(expected = SecurityException.class) + @Ignore + public void testUsbApi_SetCurrentFunctions_OnSecurityException() throws Exception { + mUsbManagerTestLib.testSetCurrentFunctionsEx(UsbManager.FUNCTION_NONE); + } +} diff --git a/tests/VoiceEnrollment/src/com/android/test/voiceenrollment/EnrollmentUtil.java b/tests/VoiceEnrollment/src/com/android/test/voiceenrollment/EnrollmentUtil.java index 7927ac4cb895..287364fec8eb 100644 --- a/tests/VoiceEnrollment/src/com/android/test/voiceenrollment/EnrollmentUtil.java +++ b/tests/VoiceEnrollment/src/com/android/test/voiceenrollment/EnrollmentUtil.java @@ -159,36 +159,36 @@ public class EnrollmentUtil { Log.e(TAG, "KeyphraseSoundModel must be non-null"); return false; } - if (soundModel.uuid == null) { + if (soundModel.getUuid() == null) { Log.e(TAG, "KeyphraseSoundModel must have a UUID"); return false; } - if (soundModel.data == null) { + if (soundModel.getData() == null) { Log.e(TAG, "KeyphraseSoundModel must have data"); return false; } - if (soundModel.keyphrases == null || soundModel.keyphrases.length != 1) { + if (soundModel.getKeyphrases() == null || soundModel.getKeyphrases().length != 1) { Log.e(TAG, "Keyphrase must be exactly 1"); return false; } - Keyphrase keyphrase = soundModel.keyphrases[0]; - if (keyphrase.id <= 0) { + Keyphrase keyphrase = soundModel.getKeyphrases()[0]; + if (keyphrase.getId() <= 0) { Log.e(TAG, "Keyphrase must have a valid ID"); return false; } - if (keyphrase.recognitionModes < 0) { + if (keyphrase.getRecognitionModes() < 0) { Log.e(TAG, "Recognition modes must be valid"); return false; } - if (keyphrase.locale == null) { + if (keyphrase.getLocale() == null) { Log.e(TAG, "Locale must not be null"); return false; } - if (keyphrase.text == null) { + if (keyphrase.getText() == null) { Log.e(TAG, "Text must not be null"); return false; } - if (keyphrase.users == null || keyphrase.users.length == 0) { + if (keyphrase.getUsers() == null || keyphrase.getUsers().length == 0) { Log.e(TAG, "Keyphrase must have valid user(s)"); return false; } diff --git a/tests/VoiceEnrollment/src/com/android/test/voiceenrollment/TestEnrollmentActivity.java b/tests/VoiceEnrollment/src/com/android/test/voiceenrollment/TestEnrollmentActivity.java index 54c944f9588e..e4880fd10d67 100644 --- a/tests/VoiceEnrollment/src/com/android/test/voiceenrollment/TestEnrollmentActivity.java +++ b/tests/VoiceEnrollment/src/com/android/test/voiceenrollment/TestEnrollmentActivity.java @@ -16,9 +16,6 @@ package com.android.test.voiceenrollment; -import java.util.Random; -import java.util.UUID; - import android.app.Activity; import android.hardware.soundtrigger.SoundTrigger; import android.hardware.soundtrigger.SoundTrigger.Keyphrase; @@ -29,6 +26,13 @@ import android.util.Log; import android.view.View; import android.widget.Toast; +import java.util.Locale; +import java.util.Random; +import java.util.UUID; + +/** + * TODO: must be transitioned to a service. + */ public class TestEnrollmentActivity extends Activity { private static final String TAG = "TestEnrollmentActivity"; private static final boolean DBG = false; @@ -56,7 +60,8 @@ public class TestEnrollmentActivity extends Activity { * Performs a fresh enrollment. */ public void onEnrollButtonClicked(View v) { - Keyphrase kp = new Keyphrase(KEYPHRASE_ID, RECOGNITION_MODES, BCP47_LOCALE, TEXT, + Keyphrase kp = new Keyphrase(KEYPHRASE_ID, RECOGNITION_MODES, + Locale.forLanguageTag(BCP47_LOCALE), TEXT, new int[] { UserManager.get(this).getUserHandle() /* current user */}); UUID modelUuid = UUID.randomUUID(); // Generate a fake model to push. @@ -86,7 +91,7 @@ public class TestEnrollmentActivity extends Activity { } boolean status = mEnrollmentUtil.deleteSoundModel(KEYPHRASE_ID, BCP47_LOCALE); if (status) { - Toast.makeText(this, "Successfully un-enrolled, model UUID=" + soundModel.uuid, + Toast.makeText(this, "Successfully un-enrolled, model UUID=" + soundModel.getUuid(), Toast.LENGTH_SHORT) .show(); } else { @@ -107,11 +112,11 @@ public class TestEnrollmentActivity extends Activity { // Generate a fake model to push. byte[] data = new byte[2048]; mRandom.nextBytes(data); - KeyphraseSoundModel updated = new KeyphraseSoundModel(soundModel.uuid, - soundModel.vendorUuid, data, soundModel.keyphrases); + KeyphraseSoundModel updated = new KeyphraseSoundModel(soundModel.getUuid(), + soundModel.getVendorUuid(), data, soundModel.getKeyphrases()); boolean status = mEnrollmentUtil.addOrUpdateSoundModel(updated); if (status) { - Toast.makeText(this, "Successfully re-enrolled, model UUID=" + updated.uuid, + Toast.makeText(this, "Successfully re-enrolled, model UUID=" + updated.getUuid(), Toast.LENGTH_SHORT) .show(); } else { diff --git a/tests/VoiceInteraction/src/com/android/test/voiceinteraction/MainInteractionService.java b/tests/VoiceInteraction/src/com/android/test/voiceinteraction/MainInteractionService.java index f1dd1de0e517..b7f5b15f72ac 100644 --- a/tests/VoiceInteraction/src/com/android/test/voiceinteraction/MainInteractionService.java +++ b/tests/VoiceInteraction/src/com/android/test/voiceinteraction/MainInteractionService.java @@ -16,14 +16,11 @@ package com.android.test.voiceinteraction; -import android.content.ComponentName; import android.content.Intent; -import android.os.Bundle; import android.service.voice.AlwaysOnHotwordDetector; import android.service.voice.AlwaysOnHotwordDetector.Callback; import android.service.voice.AlwaysOnHotwordDetector.EventPayload; import android.service.voice.VoiceInteractionService; -import android.service.voice.VoiceInteractionSession; import android.util.Log; import java.util.Arrays; @@ -68,7 +65,8 @@ public class MainInteractionService extends VoiceInteractionService { Log.i(TAG, "Creating " + this); Log.i(TAG, "Keyphrase enrollment error? " + getKeyphraseEnrollmentInfo().getParseError()); Log.i(TAG, "Keyphrase enrollment meta-data: " - + Arrays.toString(getKeyphraseEnrollmentInfo().listKeyphraseMetadata())); + + Arrays.toString(getKeyphraseEnrollmentInfo().listKeyphraseMetadata().toArray( + new android.hardware.soundtrigger.KeyphraseMetadata[0]))); mHotwordDetector = createAlwaysOnHotwordDetector( "Hello There", Locale.forLanguageTag("en-US"), mHotwordCallback); diff --git a/tests/WindowInsetsTests/Android.bp b/tests/WindowInsetsTests/Android.bp new file mode 100644 index 000000000000..7272152dc257 --- /dev/null +++ b/tests/WindowInsetsTests/Android.bp @@ -0,0 +1,27 @@ +// Copyright (C) 2020 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: "WindowInsetsTests", + srcs: ["src/**/*.java"], + resource_dirs: ["res"], + certificate: "platform", + platform_apis: true, + static_libs: [ + "androidx.core_core", + "androidx.appcompat_appcompat", + "com.google.android.material_material", + ], +} + diff --git a/tests/WindowManagerStressTest/AndroidManifest.xml b/tests/WindowInsetsTests/AndroidManifest.xml index 17e0f15c29a9..0f6282e20b41 100644 --- a/tests/WindowManagerStressTest/AndroidManifest.xml +++ b/tests/WindowInsetsTests/AndroidManifest.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ Copyright (C) 2017 The Android Open Source Project + ~ Copyright (018C) 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. @@ -16,15 +16,13 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="test.windowmanagerstresstest"> + package="com.google.android.test.windowinsetstests"> + + <application android:label="@string/activity_title"> + <activity android:name=".WindowInsetsActivity" + android:theme="@style/appTheme" + android:windowSoftInputMode="adjustResize"> - <application - android:allowBackup="false" - android:icon="@mipmap/ic_launcher" - android:label="@string/app_name" - android:supportsRtl="true" - android:theme="@style/AppTheme"> - <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> diff --git a/tests/WindowInsetsTests/res/drawable/bubble.xml b/tests/WindowInsetsTests/res/drawable/bubble.xml new file mode 100644 index 000000000000..26deb1e59e41 --- /dev/null +++ b/tests/WindowInsetsTests/res/drawable/bubble.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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. + --> + +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="@color/bubble" /> + <corners android:radius="@dimen/bubble_corner" /> + <padding android:left="@dimen/bubble_padding_side" android:top="@dimen/bubble_padding" + android:right="@dimen/bubble_padding_side" android:bottom="@dimen/bubble_padding" /> +</shape>
\ No newline at end of file diff --git a/tests/WindowInsetsTests/res/drawable/bubble_self.xml b/tests/WindowInsetsTests/res/drawable/bubble_self.xml new file mode 100644 index 000000000000..5f098a2fea60 --- /dev/null +++ b/tests/WindowInsetsTests/res/drawable/bubble_self.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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. + --> + +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="@color/bubble_self" /> + <corners android:radius="@dimen/bubble_corner" /> + <padding android:left="@dimen/bubble_padding_side" android:top="@dimen/bubble_padding" + android:right="@dimen/bubble_padding_side" android:bottom="@dimen/bubble_padding" /> +</shape>
\ No newline at end of file diff --git a/tests/WindowInsetsTests/res/drawable/ic_send.xml b/tests/WindowInsetsTests/res/drawable/ic_send.xml new file mode 100644 index 000000000000..15bc411e85fb --- /dev/null +++ b/tests/WindowInsetsTests/res/drawable/ic_send.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:autoMirrored="true" + android:width="24.0dp" + android:height="24.0dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path + android:fillColor="#FF000000" + android:pathData="M4.02,42.0L46.0,24.0 4.02,6.0 4.0,20.0l30.0,4.0 -30.0,4.0z"/> +</vector> diff --git a/tests/WindowInsetsTests/res/layout/message.xml b/tests/WindowInsetsTests/res/layout/message.xml new file mode 100644 index 000000000000..d6b29c33f662 --- /dev/null +++ b/tests/WindowInsetsTests/res/layout/message.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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. + --> + +<TextView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/bubble" + android:textSize="32sp" + android:text="Hello World"> + +</TextView>
\ No newline at end of file diff --git a/tests/WindowInsetsTests/res/layout/message_self.xml b/tests/WindowInsetsTests/res/layout/message_self.xml new file mode 100644 index 000000000000..de34e4896331 --- /dev/null +++ b/tests/WindowInsetsTests/res/layout/message_self.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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. + --> + +<merge> + <include + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt" + layout="@layout/message"> + <aapt:attr name="android:theme"> + <style> + <item name="android:layout_gravity">end</item> + <item name="bubbleBackground">@color/bubble_self</item> + </style> + </aapt:attr> + </include> +</merge> diff --git a/tests/WindowInsetsTests/res/layout/window_inset_activity.xml b/tests/WindowInsetsTests/res/layout/window_inset_activity.xml new file mode 100644 index 000000000000..1b51c4f83fe0 --- /dev/null +++ b/tests/WindowInsetsTests/res/layout/window_inset_activity.xml @@ -0,0 +1,98 @@ +<?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" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" + android:id="@+id/root"> + + <androidx.appcompat.widget.Toolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + /> + + <FrameLayout + android:id="@+id/scrollView" + android:layout_height="0dp" + android:layout_width="match_parent" + android:paddingStart="8dp" + android:paddingEnd="8dp" + android:layout_weight="1"> + + <LinearLayout + android:orientation="vertical" + android:layout_gravity="bottom" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + style="@style/bubble" + android:text="Hey, look at this buttery smooth animation!" /> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + style="@style/bubble_self" + android:text="Wow, that's pretty neat, how does this work?" /> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + style="@style/bubble" + android:text="Using the new WindowInsets animation system of course!" /> + + </LinearLayout> + + </FrameLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingStart="8dp" + android:paddingEnd="8dp" + android:id="@+id/editText"> + + <com.google.android.material.textfield.TextInputLayout + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1"> + + <com.google.android.material.textfield.TextInputEditText + android:hint="Text message" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.floatingactionbutton.FloatingActionButton + android:id="@+id/floating_action_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + app:elevation="0dp" + app:fabSize="mini" + app:srcCompat="@drawable/ic_send"/> + + </LinearLayout> +</LinearLayout> + diff --git a/tests/WindowManagerStressTest/res/values/strings.xml b/tests/WindowInsetsTests/res/values/strings.xml index cef05dcb6584..2b8e5f3da362 100644 --- a/tests/WindowManagerStressTest/res/values/strings.xml +++ b/tests/WindowInsetsTests/res/values/strings.xml @@ -1,5 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> <!-- - ~ Copyright (C) 2017 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. @@ -13,7 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> + <resources> - <string name="app_name">WmSlam</string> - <string name="run">Run</string> + <string name="activity_title">New Insets Chat</string> </resources> diff --git a/tests/WindowInsetsTests/res/values/styles.xml b/tests/WindowInsetsTests/res/values/styles.xml new file mode 100644 index 000000000000..220671fb8e71 --- /dev/null +++ b/tests/WindowInsetsTests/res/values/styles.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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="appTheme" parent="@style/Theme.MaterialComponents.Light"> + <item name="windowActionBar">false</item> + <item name="windowNoTitle">true</item> + + <item name="colorPrimary">@color/primaryColor</item> + <item name="colorPrimaryDark">@color/primaryDarkColor</item> + <item name="colorSecondary">?attr/colorPrimary</item> + <item name="colorOnSecondary">@color/primaryTextColor</item> + + <!-- Window decor --> + <item name="android:statusBarColor">#ffffff</item> + <item name="android:windowLightStatusBar">true</item> + <item name="android:windowLightNavigationBar">true</item> + <item name="android:navigationBarColor">#ffffff</item> + + </style> + + <style name="bubble_base" parent=""> + <item name="android:textSize">20sp</item> + <item name="android:layout_marginBottom">16dp</item> + </style> + + <style name="bubble" parent="@style/bubble_base"> + <item name="android:layout_marginEnd">56dp</item> + <item name="android:background">@drawable/bubble</item> + <item name="android:layout_gravity">start</item> + </style> + + <style name="bubble_self" parent="@style/bubble_base"> + <item name="android:layout_marginStart">56dp</item> + <item name="android:background">@drawable/bubble_self</item> + <item name="android:layout_gravity">end</item> + </style> + + <color name="primaryColor">#1c3fef</color> + <color name="primaryLightColor">#6f6bff</color> + <color name="primaryDarkColor">#0016bb</color> + <color name="primaryTextColor">#ffffff</color> + + <color name="bubble">#eeeeee</color> + <color name="bubble_self">#D8DCF0</color> + + <dimen name="bubble_corner">16dp</dimen> + <dimen name="bubble_padding">8dp</dimen> + <dimen name="bubble_padding_side">16dp</dimen> + + +</resources>
\ No newline at end of file diff --git a/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/WindowInsetsActivity.java b/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/WindowInsetsActivity.java new file mode 100644 index 000000000000..498cb7c1c710 --- /dev/null +++ b/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/WindowInsetsActivity.java @@ -0,0 +1,278 @@ +/* + * 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.windowinsetstests; + +import static android.view.WindowInsets.Type.ime; +import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP; +import static java.lang.Math.max; +import static java.lang.Math.min; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.Insets; +import android.os.Bundle; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.WindowInsets; +import android.view.WindowInsetsAnimation; +import android.view.WindowInsetsAnimation.Callback; +import android.view.WindowInsetsAnimationControlListener; +import android.view.WindowInsetsAnimationController; +import android.view.WindowInsetsController; +import android.view.WindowInsetsController.OnControllableInsetsChangedListener; +import android.view.animation.LinearInterpolator; +import android.widget.LinearLayout; + +import java.util.ArrayList; +import java.util.List; + +import androidx.appcompat.app.AppCompatActivity; + +public class WindowInsetsActivity extends AppCompatActivity { + + private View mRoot; + + final ArrayList<Transition> mTransitions = new ArrayList<>(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.window_inset_activity); + + setSupportActionBar(findViewById(R.id.toolbar)); + + mRoot = findViewById(R.id.root); + mRoot.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + + mTransitions.add(new Transition(findViewById(R.id.scrollView))); + mTransitions.add(new Transition(findViewById(R.id.editText))); + + mRoot.setOnTouchListener(new View.OnTouchListener() { + private final ViewConfiguration mViewConfiguration = + ViewConfiguration.get(WindowInsetsActivity.this); + WindowInsetsAnimationController mAnimationController; + WindowInsetsAnimationControlListener mCurrentRequest; + boolean mRequestedController = false; + float mDown = 0; + float mCurrent = 0; + Insets mDownInsets = Insets.NONE; + boolean mShownAtDown; + + @Override + public boolean onTouch(View v, MotionEvent event) { + mCurrent = event.getY(); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mDown = event.getY(); + mDownInsets = v.getRootWindowInsets().getInsets(ime()); + mShownAtDown = v.getRootWindowInsets().isVisible(ime()); + mRequestedController = false; + mCurrentRequest = null; + break; + case MotionEvent.ACTION_MOVE: + if (mAnimationController != null) { + updateInset(); + } else if (Math.abs(mDown - event.getY()) + > mViewConfiguration.getScaledTouchSlop() + && !mRequestedController) { + mRequestedController = true; + v.getWindowInsetsController().controlWindowInsetsAnimation(ime(), + 1000, new LinearInterpolator(), null /* cancellationSignal */, + mCurrentRequest = new WindowInsetsAnimationControlListener() { + @Override + public void onReady( + @NonNull WindowInsetsAnimationController controller, + int types) { + if (mCurrentRequest == this) { + mAnimationController = controller; + updateInset(); + } else { + controller.finish(mShownAtDown); + } + } + + @Override + public void onFinished( + WindowInsetsAnimationController controller) { + mAnimationController = null; + } + + @Override + public void onCancelled( + WindowInsetsAnimationController controller) { + mAnimationController = null; + } + }); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if (mAnimationController != null) { + boolean isCancel = event.getAction() == MotionEvent.ACTION_CANCEL; + mAnimationController.finish(isCancel ? mShownAtDown : !mShownAtDown); + mAnimationController = null; + } + mRequestedController = false; + mCurrentRequest = null; + break; + } + return true; + } + + private void updateInset() { + int inset = (int) (mDownInsets.bottom + (mDown - mCurrent)); + final int hidden = mAnimationController.getHiddenStateInsets().bottom; + final int shown = mAnimationController.getShownStateInsets().bottom; + final int start = mShownAtDown ? shown : hidden; + final int end = mShownAtDown ? hidden : shown; + inset = max(inset, hidden); + inset = min(inset, shown); + mAnimationController.setInsetsAndAlpha( + Insets.of(0, 0, 0, inset), + 1f, (inset - start) / (float)(end - start)); + } + }); + + mRoot.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { + @Override + public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { + mRoot.setPadding(insets.getSystemWindowInsetLeft(), + insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), + insets.getSystemWindowInsetBottom()); + return WindowInsets.CONSUMED; + } + }); + + mRoot.setWindowInsetsAnimationCallback(new Callback(DISPATCH_MODE_STOP) { + + @Override + public void onPrepare(WindowInsetsAnimation animation) { + mTransitions.forEach(it -> it.onPrepare(animation)); + } + + @Override + public WindowInsets onProgress(WindowInsets insets, + @NonNull List<WindowInsetsAnimation> runningAnimations) { + mTransitions.forEach(it -> it.onProgress(insets)); + return insets; + } + + @Override + public WindowInsetsAnimation.Bounds onStart(WindowInsetsAnimation animation, + WindowInsetsAnimation.Bounds bounds) { + mTransitions.forEach(Transition::onStart); + return bounds; + } + + @Override + public void onEnd(WindowInsetsAnimation animation) { + mTransitions.forEach(it -> it.onFinish(animation)); + } + }); + + findViewById(R.id.floating_action_button).setOnClickListener( + v -> v.getWindowInsetsController().controlWindowInsetsAnimation(ime(), -1, + new LinearInterpolator(), null /* cancellationSignal */, + new WindowInsetsAnimationControlListener() { + @Override + public void onReady( + WindowInsetsAnimationController controller, + int types) { + ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f); + anim.setDuration(1500); + anim.addUpdateListener(animation + -> controller.setInsetsAndAlpha( + controller.getShownStateInsets(), + (float) animation.getAnimatedValue(), + anim.getAnimatedFraction())); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + controller.finish(true); + } + }); + anim.start(); + } + + @Override + public void onCancelled(WindowInsetsAnimationController controller) { + } + + @Override + public void onFinished(WindowInsetsAnimationController controller) { + } + })); + } + + @Override + public void onResume() { + super.onResume(); + // TODO: move this to onCreate once setDecorFitsSystemWindows can be safely called there. + getWindow().getDecorView().post(() -> getWindow().setDecorFitsSystemWindows(false)); + } + + static class Transition { + private int mEndBottom; + private int mStartBottom; + private final View mView; + private WindowInsetsAnimation mInsetsAnimation; + + Transition(View root) { + mView = root; + } + + void onPrepare(WindowInsetsAnimation animation) { + if ((animation.getTypeMask() & ime()) != 0) { + mInsetsAnimation = animation; + } + mStartBottom = mView.getBottom(); + } + + void onProgress(WindowInsets insets) { + mView.setY(mStartBottom + (mEndBottom - mStartBottom) + * mInsetsAnimation.getInterpolatedFraction() + - mView.getHeight()); + } + + void onStart() { + mEndBottom = mView.getBottom(); + } + + void onFinish(WindowInsetsAnimation animation) { + if (mInsetsAnimation == animation) { + mInsetsAnimation = null; + } + } + } + + static class ImeLinearLayout extends LinearLayout { + + public ImeLinearLayout(Context context, + @Nullable AttributeSet attrs) { + super(context, attrs); + } + } +} diff --git a/tests/WindowManagerStressTest/res/layout/activity_main.xml b/tests/WindowManagerStressTest/res/layout/activity_main.xml deleted file mode 100644 index 6cf82691155c..000000000000 --- a/tests/WindowManagerStressTest/res/layout/activity_main.xml +++ /dev/null @@ -1,38 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2016 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" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical" - android:paddingBottom="@dimen/activity_vertical_margin" - android:paddingLeft="@dimen/activity_horizontal_margin" - android:paddingRight="@dimen/activity_horizontal_margin" - android:paddingTop="@dimen/activity_vertical_margin" - tools:context="test.amslam.MainActivity"> - - <Button - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:id="@+id/run" - android:text="@string/run" /> - - <TextView - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:id="@+id/output" /> - -</LinearLayout> diff --git a/tests/WindowManagerStressTest/res/mipmap-hdpi/ic_launcher.png b/tests/WindowManagerStressTest/res/mipmap-hdpi/ic_launcher.png Binary files differdeleted file mode 100644 index cde69bcccec6..000000000000 --- a/tests/WindowManagerStressTest/res/mipmap-hdpi/ic_launcher.png +++ /dev/null diff --git a/tests/WindowManagerStressTest/res/mipmap-mdpi/ic_launcher.png b/tests/WindowManagerStressTest/res/mipmap-mdpi/ic_launcher.png Binary files differdeleted file mode 100644 index c133a0cbd379..000000000000 --- a/tests/WindowManagerStressTest/res/mipmap-mdpi/ic_launcher.png +++ /dev/null diff --git a/tests/WindowManagerStressTest/res/mipmap-xhdpi/ic_launcher.png b/tests/WindowManagerStressTest/res/mipmap-xhdpi/ic_launcher.png Binary files differdeleted file mode 100644 index bfa42f0e7b91..000000000000 --- a/tests/WindowManagerStressTest/res/mipmap-xhdpi/ic_launcher.png +++ /dev/null diff --git a/tests/WindowManagerStressTest/res/mipmap-xxhdpi/ic_launcher.png b/tests/WindowManagerStressTest/res/mipmap-xxhdpi/ic_launcher.png Binary files differdeleted file mode 100644 index 324e72cdd748..000000000000 --- a/tests/WindowManagerStressTest/res/mipmap-xxhdpi/ic_launcher.png +++ /dev/null diff --git a/tests/WindowManagerStressTest/res/mipmap-xxxhdpi/ic_launcher.png b/tests/WindowManagerStressTest/res/mipmap-xxxhdpi/ic_launcher.png Binary files differdeleted file mode 100644 index aee44e138434..000000000000 --- a/tests/WindowManagerStressTest/res/mipmap-xxxhdpi/ic_launcher.png +++ /dev/null diff --git a/tests/WindowManagerStressTest/src/test/windowmanagerstresstest/MainActivity.java b/tests/WindowManagerStressTest/src/test/windowmanagerstresstest/MainActivity.java deleted file mode 100644 index 85646987940f..000000000000 --- a/tests/WindowManagerStressTest/src/test/windowmanagerstresstest/MainActivity.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright (C) 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 test.windowmanagerstresstest; - -import android.app.Activity; -import android.graphics.Rect; -import android.os.Bundle; -import android.os.RemoteException; -import android.os.SystemClock; -import android.util.Log; -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; -import android.view.WindowManagerGlobal; -import android.widget.TextView; - -import com.android.internal.view.BaseIWindow; - -import java.util.ArrayList; - -public class MainActivity extends Activity { - - private static final String TAG = "WmSlam"; - - private TextView mOutput; - private volatile boolean finished; - private final ArrayList<BaseIWindow> mWindows = new ArrayList<>(); - private final LayoutParams mLayoutParams = new LayoutParams(); - private final Rect mTmpRect = new Rect(); - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - mOutput = (TextView) findViewById(R.id.output); - - findViewById(R.id.run).setOnClickListener(view -> { - view.setEnabled(false); - mOutput.setText(""); - startBatch(); - }); - mLayoutParams.token = getActivityToken(); - } - - void startBatch() { - new Thread(() -> { - finished = false; - addWindows(); - startCpuRunnables(); - for (int i = 0; i < 5; i++) { - final long time = SystemClock.uptimeMillis(); - slamWm(); - log("Total: " + (SystemClock.uptimeMillis() - time) + " ms"); - } - removeWindows(); - finished = true; - }).start(); - } - - void startCpuRunnables() { - for (int i = 0; i < 10; i++) { - new Thread(mUseCpuRunnable).start(); - } - } - - private final Runnable mUseCpuRunnable = new Runnable() { - @Override - public void run() { - while (!finished) { - } - } - }; - - private void log(String text) { - mOutput.post(() -> mOutput.append(text + "\n")); - Log.d(TAG, text); - } - - private void slamWm() { - ArrayList<Thread> threads = new ArrayList<>(); - for (int i = 0; i < 20; i++) { - for (BaseIWindow window : mWindows) { - Thread t = new Thread(() -> { - try { - WindowManagerGlobal.getWindowSession().relayout(window, - window.mSeq, mLayoutParams, -1, -1, View.VISIBLE, 0, -1, mTmpRect, - mTmpRect, mTmpRect, mTmpRect, mTmpRect, mTmpRect, mTmpRect, - new DisplayCutout.ParcelableWrapper(), new MergedConfiguration(), - new SurfaceControl(), new InsetsState()); - } catch (RemoteException e) { - e.printStackTrace(); - } - }); - threads.add(t); - t.start(); - } - } - for (Thread t : threads) { - try { - t.join(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - - void addWindows() { - for (int i = 0; i < 50; i++) { - final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); - layoutParams.token = getActivityToken(); - final BaseIWindow window = new BaseIWindow(); - 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, - new InsetsState()); - } catch (RemoteException e) { - e.printStackTrace(); - } - mWindows.add(window); - } - } - - void removeWindows() { - for (BaseIWindow window : mWindows) { - try { - WindowManagerGlobal.getWindowSession().remove(window); - } catch (RemoteException e) { - e.printStackTrace(); - } - } - } -} diff --git a/tests/net/java/android/net/TelephonyNetworkSpecifierTest.java b/tests/net/java/android/net/TelephonyNetworkSpecifierTest.java index 47afed441ace..efb92033df1e 100644 --- a/tests/net/java/android/net/TelephonyNetworkSpecifierTest.java +++ b/tests/net/java/android/net/TelephonyNetworkSpecifierTest.java @@ -19,7 +19,10 @@ package android.net; import static com.android.testutils.ParcelUtilsKt.assertParcelSane; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import android.net.wifi.WifiNetworkSpecifier; import android.telephony.SubscriptionManager; import androidx.test.filters.SmallTest; @@ -32,6 +35,7 @@ import org.junit.Test; @SmallTest public class TelephonyNetworkSpecifierTest { private static final int TEST_SUBID = 5; + private static final String TEST_SSID = "Test123"; /** * Validate that IllegalArgumentException will be thrown if build TelephonyNetworkSpecifier @@ -79,4 +83,31 @@ public class TelephonyNetworkSpecifierTest { .build(); assertParcelSane(specifier, 1 /* fieldCount */); } + + /** + * Validate the behavior of method canBeSatisfiedBy(). + */ + @Test + public void testCanBeSatisfiedBy() { + final TelephonyNetworkSpecifier tns1 = new TelephonyNetworkSpecifier.Builder() + .setSubscriptionId(TEST_SUBID) + .build(); + final TelephonyNetworkSpecifier tns2 = new TelephonyNetworkSpecifier.Builder() + .setSubscriptionId(TEST_SUBID) + .build(); + final WifiNetworkSpecifier wns = new WifiNetworkSpecifier.Builder() + .setSsid(TEST_SSID) + .build(); + final MatchAllNetworkSpecifier mans = new MatchAllNetworkSpecifier(); + + // Test equality + assertEquals(tns1, tns2); + assertTrue(tns1.canBeSatisfiedBy(tns1)); + assertTrue(tns1.canBeSatisfiedBy(tns2)); + + // Test other edge cases. + assertFalse(tns1.canBeSatisfiedBy(null)); + assertFalse(tns1.canBeSatisfiedBy(wns)); + assertTrue(tns1.canBeSatisfiedBy(mans)); + } } diff --git a/tests/net/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/net/java/com/android/server/connectivity/PermissionMonitorTest.java index 76e3e2fced6f..39f849c340f7 100644 --- a/tests/net/java/com/android/server/connectivity/PermissionMonitorTest.java +++ b/tests/net/java/com/android/server/connectivity/PermissionMonitorTest.java @@ -56,7 +56,6 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; -import android.content.pm.PackageList; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.UserInfo; @@ -72,6 +71,7 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.server.LocalServices; +import com.android.server.pm.PackageList; import org.junit.Before; import org.junit.Test; diff --git a/tests/permission/src/com/android/framework/permission/tests/PmPermissionsTests.java b/tests/permission/src/com/android/framework/permission/tests/PmPermissionsTests.java index b39db61794de..299d8d003d9c 100644 --- a/tests/permission/src/com/android/framework/permission/tests/PmPermissionsTests.java +++ b/tests/permission/src/com/android/framework/permission/tests/PmPermissionsTests.java @@ -43,8 +43,8 @@ public class PmPermissionsTests extends AndroidTestCase { try { mPm.getPackageSizeInfo(mPkgName, null); fail("PackageManager.getPackageSizeInfo" + - "did not throw SecurityException as expected"); - } catch (SecurityException e) { + "did not throw UnsupportedOperationException as expected"); + } catch (UnsupportedOperationException e) { // expected } } @@ -130,4 +130,4 @@ public class PmPermissionsTests extends AndroidTestCase { // expected } } -}
\ No newline at end of file +} 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 c50229ae30f4..d1d6a26790fd 100644 --- a/tests/permission/src/com/android/framework/permission/tests/VibratorServicePermissionTest.java +++ b/tests/permission/src/com/android/framework/permission/tests/VibratorServicePermissionTest.java @@ -16,12 +16,12 @@ package com.android.framework.permission.tests; -import android.media.AudioAttributes; import android.os.Binder; import android.os.IVibratorService; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.test.suitebuilder.annotation.SmallTest; @@ -52,8 +52,8 @@ public class VibratorServicePermissionTest extends TestCase { try { final VibrationEffect effect = VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE); - final AudioAttributes attrs = new AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_ALARM) + final VibrationAttributes attrs = new VibrationAttributes.Builder() + .setUsage(VibrationAttributes.USAGE_ALARM) .build(); mVibratorService.vibrate(Process.myUid(), null, effect, attrs, "testVibrate", new Binder()); 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 4c2a984f8198..737665fb97e4 100644 --- a/tests/permission/src/com/android/framework/permission/tests/WindowManagerPermissionTests.java +++ b/tests/permission/src/com/android/framework/permission/tests/WindowManagerPermissionTests.java @@ -17,6 +17,7 @@ package com.android.framework.permission.tests; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import android.os.Binder; import android.os.RemoteException; @@ -27,6 +28,8 @@ import android.view.IWindowManager; import junit.framework.TestCase; +import org.junit.Test; + /** * TODO: Remove this. This is only a placeholder, need to implement this. */ @@ -53,7 +56,7 @@ public class WindowManagerPermissionTests extends TestCase { } try { - mWm.addWindowToken(null, 0, DEFAULT_DISPLAY); + mWm.addWindowToken(null, TYPE_APPLICATION, DEFAULT_DISPLAY); fail("IWindowManager.addWindowToken did not throw SecurityException as" + " expected"); } catch (SecurityException e) { @@ -63,16 +66,6 @@ public class WindowManagerPermissionTests extends TestCase { } try { - mWm.removeWindowToken(null, DEFAULT_DISPLAY); - fail("IWindowManager.removeWindowToken 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"); @@ -182,4 +175,29 @@ public class WindowManagerPermissionTests extends TestCase { fail("Unexpected remote exception"); } } + + @Test + public void testADD_WINDOW_TOKEN_WITH_OPTIONS() { + // Verify if addWindowTokenWithOptions throw SecurityException for privileged window type. + try { + mWm.addWindowTokenWithOptions(null, TYPE_APPLICATION, DEFAULT_DISPLAY, null, ""); + fail("IWindowManager.addWindowTokenWithOptions did not throw SecurityException as" + + " expected"); + } catch (SecurityException e) { + // expected + } catch (RemoteException e) { + fail("Unexpected remote exception"); + } + + // Verify if addWindowTokenWithOptions throw SecurityException for null packageName. + try { + mWm.addWindowTokenWithOptions(null, TYPE_APPLICATION, DEFAULT_DISPLAY, null, null); + fail("IWindowManager.addWindowTokenWithOptions did not throw SecurityException as" + + " expected"); + } catch (SecurityException e) { + // expected + } catch (RemoteException e) { + fail("Unexpected remote exception"); + } + } } diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java index 8d99ac7100eb..ebe9b5706bf8 100644 --- a/tests/testables/src/android/testing/TestableLooper.java +++ b/tests/testables/src/android/testing/TestableLooper.java @@ -222,6 +222,10 @@ public class TestableLooper { return sLoopers.get(test); } + public static void remove(Object test) { + sLoopers.remove(test); + } + static class LooperFrameworkMethod extends FrameworkMethod { private HandlerThread mHandlerThread; @@ -234,6 +238,9 @@ public class TestableLooper { try { mLooper = setAsMain ? Looper.getMainLooper() : createLooper(); mTestableLooper = new TestableLooper(mLooper, false); + if (!setAsMain) { + mTestableLooper.getLooper().getThread().setName(test.getClass().getName()); + } } catch (Exception e) { throw new RuntimeException(e); } diff --git a/tests/utils/testutils/Android.bp b/tests/utils/testutils/Android.bp index f71be7b0b7d3..a6625ab9c17f 100644 --- a/tests/utils/testutils/Android.bp +++ b/tests/utils/testutils/Android.bp @@ -22,6 +22,7 @@ java_library { static_libs: [ "junit", "hamcrest-library", + "androidx.test.runner", ], libs: [ diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java index a49eda3d86d0..a826646f69f3 100644 --- a/tests/utils/testutils/java/android/os/test/TestLooper.java +++ b/tests/utils/testutils/java/android/os/test/TestLooper.java @@ -48,6 +48,8 @@ public class TestLooper { private static final Method MESSAGE_MARK_IN_USE_METHOD; private static final String TAG = "TestLooper"; + private final Clock mClock; + private AutoDispatchThread mAutoDispatchThread; static { @@ -69,8 +71,25 @@ public class TestLooper { } } - + /** + * Creates a TestLooper and installs it as the looper for the current thread. + */ public TestLooper() { + this(SystemClock::uptimeMillis); + } + + /** + * Creates a TestLooper with a custom clock and installs it as the looper for the current + * thread. + * + * Messages are dispatched when their {@link Message#when} is before or at {@link + * Clock#uptimeMillis()}. + * Use a custom clock with care. When using an offsettable clock like {@link + * com.android.server.testutils.OffsettableClock} be sure not to double offset messages by + * offsetting the clock and calling {@link #moveTimeForward(long)}. Instead, offset the clock + * and call {@link #dispatchAll()}. + */ + public TestLooper(Clock clock) { try { mLooper = LOOPER_CONSTRUCTOR.newInstance(false); @@ -80,6 +99,8 @@ public class TestLooper { } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) { throw new RuntimeException("Reflection error constructing or accessing looper", e); } + + mClock = clock; } public Looper getLooper() { @@ -116,9 +137,13 @@ public class TestLooper { } } + private long currentTime() { + return mClock.uptimeMillis(); + } + private Message messageQueueNext() { try { - long now = SystemClock.uptimeMillis(); + long now = currentTime(); Message prevMsg = null; Message msg = getMessageLinkedList(); @@ -157,7 +182,7 @@ public class TestLooper { public synchronized boolean isIdle() { Message messageList = getMessageLinkedList(); - return messageList != null && SystemClock.uptimeMillis() >= messageList.getWhen(); + return messageList != null && currentTime() >= messageList.getWhen(); } /** @@ -187,6 +212,7 @@ public class TestLooper { /** * Dispatch all messages currently in the queue * Will not fail if there are no messages pending + * * @return the number of messages dispatched */ public synchronized int dispatchAll() { @@ -198,6 +224,10 @@ public class TestLooper { return count; } + public interface Clock { + long uptimeMillis(); + } + /** * Thread used to dispatch messages when the main thread is blocked waiting for a response. */ @@ -210,33 +240,36 @@ public class TestLooper { /** * Run method for the auto dispatch thread. * The thread loops a maximum of MAX_LOOPS times with a 10ms sleep between loops. - * The thread continues looping and attempting to dispatch all messages until at - * least one message has been dispatched. + * The thread continues looping and attempting to dispatch all messages until + * {@link #stopAutoDispatch()} has been invoked. */ @Override public void run() { int dispatchCount = 0; for (int i = 0; i < MAX_LOOPS; i++) { try { - dispatchCount = dispatchAll(); + dispatchCount += dispatchAll(); } catch (RuntimeException e) { mAutoDispatchException = e; - } - Log.d(TAG, "dispatched " + dispatchCount + " messages"); - if (dispatchCount > 0) { return; } + Log.d(TAG, "dispatched " + dispatchCount + " messages"); try { Thread.sleep(LOOP_SLEEP_TIME_MS); } catch (InterruptedException e) { - mAutoDispatchException = new IllegalStateException( - "stopAutoDispatch called before any messages were dispatched."); + if (dispatchCount == 0) { + Log.e(TAG, "stopAutoDispatch called before any messages were dispatched."); + mAutoDispatchException = new IllegalStateException( + "stopAutoDispatch called before any messages were dispatched."); + } return; } } - Log.e(TAG, "AutoDispatchThread did not dispatch any messages."); - mAutoDispatchException = new IllegalStateException( - "TestLooper did not dispatch any messages before exiting."); + if (dispatchCount == 0) { + Log.e(TAG, "AutoDispatchThread did not dispatch any messages."); + mAutoDispatchException = new IllegalStateException( + "TestLooper did not dispatch any messages before exiting."); + } } /** @@ -287,4 +320,17 @@ public class TestLooper { "stopAutoDispatch called without startAutoDispatch."); } } + + /** + * If an AutoDispatchThread is currently running, stop and clean up. + * This method ignores exceptions raised for indicating that no messages were dispatched. + */ + public void stopAutoDispatchAndIgnoreExceptions() { + try { + stopAutoDispatch(); + } catch (IllegalStateException e) { + Log.e(TAG, "stopAutoDispatch", e); + } + + } } diff --git a/tests/utils/testutils/java/android/view/test/InsetsModeSession.java b/tests/utils/testutils/java/android/view/test/InsetsModeSession.java index c83dfa41d260..e05fdce0ca0c 100644 --- a/tests/utils/testutils/java/android/view/test/InsetsModeSession.java +++ b/tests/utils/testutils/java/android/view/test/InsetsModeSession.java @@ -31,7 +31,7 @@ public class InsetsModeSession implements AutoCloseable { } @Override - public void close() throws Exception { + public void close() { ViewRootImpl.sNewInsetsMode = mOldMode; } } diff --git a/tests/utils/testutils/java/com/android/server/accessibility/test/MessageCapturingHandler.java b/tests/utils/testutils/java/com/android/server/accessibility/test/MessageCapturingHandler.java new file mode 100644 index 000000000000..bce2ab5c5a7f --- /dev/null +++ b/tests/utils/testutils/java/com/android/server/accessibility/test/MessageCapturingHandler.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2016 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.accessibility.test; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Pair; + +import androidx.test.InstrumentationRegistry; + +import java.util.ArrayList; +import java.util.List; + +/** + * Utility class to capture messages dispatched through a handler and control when they arrive + * at their target. + */ +public class MessageCapturingHandler extends Handler { + public List<Pair<Message, Long>> timedMessages = new ArrayList<>(); + + Handler.Callback mCallback; + + public MessageCapturingHandler(Handler.Callback callback) { + this(InstrumentationRegistry.getContext().getMainLooper(), callback); + } + + public MessageCapturingHandler(Looper looper, Callback callback) { + super(looper); + mCallback = callback; + } + + /** + * Holding messages in queue, but never dispatching. + * @see #removeAllMessages() + */ + @Override + public boolean sendMessageAtTime(Message message, long uptimeMillis) { + timedMessages.add(new Pair<>(Message.obtain(message), uptimeMillis)); + return super.sendMessageAtTime(message, Long.MAX_VALUE); + } + + public void setCallback(Handler.Callback callback) { + mCallback = callback; + } + + public void sendOneMessage() { + Message message = timedMessages.remove(0).first; + removeMessages(message.what, message.obj); + dispatchMessage(message); + removeStaleMessages(); + } + + public void sendAllMessages() { + while (!timedMessages.isEmpty()) { + sendOneMessage(); + } + } + + public void sendLastMessage() { + Message message = timedMessages.remove(timedMessages.size() - 1).first; + removeMessages(message.what, message.obj); + dispatchMessage(message); + removeStaleMessages(); + } + + /** + * Clear messages sent from this handler in queue. + * <p> + * If main looper is used, this method should be called in tear down function + * to ensure messages isolation between test cases. + * </p> + */ + public void removeAllMessages() { + if (hasMessages()) { + for (int i = 0; i < timedMessages.size(); i++) { + Message message = timedMessages.get(i).first; + removeMessages(message.what, message.obj); + } + } + } + + public boolean hasMessages() { + removeStaleMessages(); + return !timedMessages.isEmpty(); + } + + private void removeStaleMessages() { + for (int i = 0; i < timedMessages.size(); i++) { + Message message = timedMessages.get(i).first; + if (!hasMessages(message.what, message.obj)) { + timedMessages.remove(i--); + } + } + } + + public void dispatchMessage(Message m) { + if (mCallback != null) { + mCallback.handleMessage(m); + return; + } + super.dispatchMessage(m); + } +} 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 c9e3404e0f1a..ea803f2aba8b 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 @@ -38,12 +38,18 @@ public final class FrameworksTestsFilter extends SelectTest { "android.app.activity.ActivityThreadClientTest", // Test specifications for FrameworksCoreTests. "android.app.servertransaction.", // all tests under the package. + "android.view.CutoutSpecificationTest", "android.view.DisplayCutoutTest", "android.view.InsetsAnimationControlImplTest", "android.view.InsetsControllerTest", + "android.view.InsetsFlagsTest", "android.view.InsetsSourceTest", "android.view.InsetsSourceConsumerTest", "android.view.InsetsStateTest", + "android.view.WindowMetricsTest", + "android.view.PendingInsetsControllerTest", + "android.app.WindowContextTest", + "android.window.WindowMetricsHelperTest" }; public FrameworksTestsFilter(Bundle testArgs) { |