Create enum for different types of fixed permissions am: 137be684ef
Original change: https://googleplex-android-review.googlesource.com/c/platform/cts/+/19793841
Change-Id: Ic059cd5698a097f2137125f78f2e42ea95c8ab72
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/apps/CtsVerifier/AndroidManifest.xml b/apps/CtsVerifier/AndroidManifest.xml
index b2513e9..254e3c5 100644
--- a/apps/CtsVerifier/AndroidManifest.xml
+++ b/apps/CtsVerifier/AndroidManifest.xml
@@ -2989,24 +2989,6 @@
android.hardware.camera2.CaptureRequest#controlExtendedSceneMode" />
</activity>
- <activity android:name=".camera.its.CameraMuteToggleActivity"
- android:label="@string/camera_hw_toggle_test"
- android:exported="true"
- android:screenOrientation="landscape">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.cts.intent.category.MANUAL_TEST" />
- </intent-filter>
- <meta-data android:name="test_category" android:value="@string/test_category_camera" />
- <meta-data android:name="test_required_configs" android:value="config_has_camera_toggle"/>
- <meta-data android:name="test_required_features" android:value="android.hardware.camera.any"/>
- <meta-data android:name="test_excluded_features"
- android:value="android.hardware.type.automotive"/>
- <meta-data android:name="display_mode"
- android:value="single_display_mode" />
- <meta-data android:name="CddTest" android:value="9.8.13/C-1-3" />
- </activity>
-
<activity android:name=".usb.accessory.UsbAccessoryTestActivity"
android:label="@string/usb_accessory_test"
android:exported="true"
@@ -5708,20 +5690,6 @@
android.hardware.usb.UsbManager#requestPermission"/>
</activity>
- <activity android:name=".audio.AudioMicrophoneMuteToggleActivity"
- android:label="@string/audio_mic_toggle_test"
- android:exported="true"
- android:screenOrientation="locked">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.cts.intent.category.MANUAL_TEST" />
- </intent-filter>
- <meta-data android:name="test_category" android:value="@string/test_category_audio" />
- <meta-data android:name="test_required_configs" android:value="config_has_mic_toggle"/>
- <meta-data android:name="display_mode" android:value="multi_display_mode" />
- <meta-data android:name="CddTest" android:value="9.8.13/C-1-3" />
- </activity>
-
<service android:name=".tv.MockTvInputService"
android:exported="true"
android:permission="android.permission.BIND_TV_INPUT">
diff --git a/apps/CtsVerifier/res/layout/cam_hw_toggle.xml b/apps/CtsVerifier/res/layout/cam_hw_toggle.xml
deleted file mode 100644
index 95aced3..0000000
--- a/apps/CtsVerifier/res/layout/cam_hw_toggle.xml
+++ /dev/null
@@ -1,100 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<!-- Copyright (C) 2022 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- style="@style/RootLayoutPadding">
-
-<LinearLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical">
-
- <LinearLayout
- android:orientation="horizontal"
- android:layout_width="fill_parent"
- android:layout_height="0dp"
- android:layout_weight="1">
-
- <LinearLayout
- android:orientation="vertical"
- android:layout_width="0dp"
- android:layout_height="fill_parent"
- android:layout_weight="3">
-
- <TextureView
- android:id="@+id/preview_view"
- android:layout_height="0dp"
- android:layout_width="fill_parent"
- android:layout_weight="3" />
- <TextView
- android:id="@+id/preview_label"
- android:layout_height="wrap_content"
- android:layout_width="fill_parent"
- android:padding="2dp"
- android:textSize="16sp"
- android:gravity="center" />
-
- </LinearLayout>
- <LinearLayout
- android:orientation="vertical"
- android:layout_width="0dp"
- android:layout_height="fill_parent"
- android:layout_weight="3">
-
- <ImageView
- android:id="@+id/image_view"
- android:layout_height="0dp"
- android:layout_width="fill_parent"
- android:layout_weight="3" />
- <TextView
- android:id="@+id/image_label"
- android:layout_height="wrap_content"
- android:layout_width="fill_parent"
- android:padding="2dp"
- android:textSize="16sp"
- android:gravity="center" />
-
- </LinearLayout>
-
- <LinearLayout
- android:orientation="vertical"
- android:layout_width="0dp"
- android:layout_height="fill_parent"
- android:layout_weight="3"
- android:gravity="bottom">
-
- <TextView
- android:id="@+id/instruction_text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/camera_hw_toggle_test_instruction" />
- <Button
- android:id="@+id/take_picture_button"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/co_photo_button_caption" />
-
- </LinearLayout>
-
- </LinearLayout>
-
- <include layout="@layout/pass_fail_buttons" />
-
-</LinearLayout>
-</ScrollView>
diff --git a/apps/CtsVerifier/res/layout/mic_hw_toggle.xml b/apps/CtsVerifier/res/layout/mic_hw_toggle.xml
deleted file mode 100644
index a17abd4..0000000
--- a/apps/CtsVerifier/res/layout/mic_hw_toggle.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2022 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- style="@style/RootLayoutPadding">
-
-<LinearLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical">
-
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:id="@+id/info_text"/>
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="3"
- android:orientation="horizontal">
- <Button
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="5"
- android:text="@string/hifi_ultrasound_test_record"
- android:id="@+id/recorder_button"/>
- </LinearLayout>
-
- <include layout="@layout/pass_fail_buttons" />
- </LinearLayout>
-</ScrollView>
diff --git a/apps/CtsVerifier/res/values/strings.xml b/apps/CtsVerifier/res/values/strings.xml
index 131228a..b49337c 100644
--- a/apps/CtsVerifier/res/values/strings.xml
+++ b/apps/CtsVerifier/res/values/strings.xml
@@ -6674,41 +6674,4 @@
<string name="report_distance_range_5m_gt_90p">Measurement Range at 5m (90th percentile)</string>
<string name="nan_precision">Nan Precision Test</string>
<string name="wifi_nan">WiFi NAN</string>
-
- <!-- Strings for AudioMicrophoneMuteToggleActivity -->
- <string name="audio_mic_toggle_test">Audio Microphone Hardware Toggle Mute Test</string>
- <string name="audio_mic_toggle_test_info">
- This test verifies that devices which implement microphone hardware privacy toggles enforce sensor privacy when toggles are enabled.
- \nTo pass the test:
- \n - <a href="https://source.android.com/compatibility/android-cdd#9813_sensorprivacymanager">The audio stream should be muted</a>.
- \n - A dialog or notification should be shown that informs the user that the sensor privacy is enabled.
- </string>
- <string name="audio_mic_toggle_test_instruction1">
- Mute the microphone using the hardware privacy toggle.
- \nPress the RECORD button.
- \nObserve a dialog with information regarding the microphone being blocked
- \nIgnore/cancel the dialog and wait for the recording to complete.
- \nThe pass button will be enabled if the test succeeded.</string>
- <string name="audio_mic_toggle_test_analyzing">Analyzing, please wait...\n</string>
-
- <!-- Strings for CameraMuteToggleActivity -->
- <string name="camera_hw_toggle_test">Camera Hardware Toggle Mute Test</string>
- <string name="camera_hw_toggle_test_info">
- This test verifies that devices which implement camera hardware privacy toggles enforce sensor privacy when toggles are enabled.
- \nTo pass the test:
- \n - <a href="https://source.android.com/compatibility/android-cdd#9813_sensorprivacymanager">The video stream should be muted</a>.
- \n - A dialog or notification should be shown that informs the user that the sensor privacy is enabled.
- </string>
- <string name="camera_hw_toggle_test_instruction">
- Mute the camera using the hardware privacy toggle.
- \nObserve a dialog with information regarding the camera being blocked.
- \nCamera preview should show a blank feed.
- \nPress the Take Photo button.
- \nCaptured image should be black.
- \nMark the test as passing if the above conditions are met.</string>
- <string name="camera_hw_toggle_test_no_camera">
- No available camera found.
- \nAdd or enable a camera and re-run this test.
- </string>
-
</resources>
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/ManifestTestListAdapter.java b/apps/CtsVerifier/src/com/android/cts/verifier/ManifestTestListAdapter.java
index 9393283..7bf6b5d 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/ManifestTestListAdapter.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/ManifestTestListAdapter.java
@@ -19,14 +19,12 @@
import static com.android.cts.verifier.TestListActivity.sCurrentDisplayMode;
import static com.android.cts.verifier.TestListActivity.sInitialLaunch;
-import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
-import android.hardware.SensorPrivacyManager;
import android.os.Bundle;
import android.telephony.TelephonyManager;
import android.util.Log;
@@ -144,10 +142,6 @@
private static final String CONFIG_QUICK_SETTINGS_SUPPORTED = "config_quick_settings_supported";
- private static final String CONFIG_HAS_MIC_TOGGLE = "config_has_mic_toggle";
-
- private static final String CONFIG_HAS_CAMERA_TOGGLE = "config_has_camera_toggle";
-
/** The config to represent that a test is only needed to run in the main display mode
* (i.e. unfolded) */
private static final String SINGLE_DISPLAY_MODE = "single_display_mode";
@@ -486,10 +480,6 @@
return false;
}
break;
- case CONFIG_HAS_MIC_TOGGLE:
- return isHardwareToggleSupported(SensorPrivacyManager.Sensors.MICROPHONE);
- case CONFIG_HAS_CAMERA_TOGGLE:
- return isHardwareToggleSupported(SensorPrivacyManager.Sensors.CAMERA);
default:
break;
}
@@ -591,16 +581,4 @@
super.loadTestResults();
}
}
-
- @SuppressLint("NewApi")
- private boolean isHardwareToggleSupported(final int sensorType) {
- boolean isToggleSupported = false;
- SensorPrivacyManager sensorPrivacyManager = mContext.getSystemService(
- SensorPrivacyManager.class);
- if (sensorPrivacyManager != null) {
- isToggleSupported = sensorPrivacyManager.supportsSensorToggle(
- SensorPrivacyManager.TOGGLE_TYPE_HARDWARE, sensorType);
- }
- return isToggleSupported;
- }
}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioAEC.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioAEC.java
index 3f1266e..3c1b8a1 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioAEC.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioAEC.java
@@ -17,11 +17,11 @@
package com.android.cts.verifier.audio;
import android.content.Context;
-import android.media.audiofx.AcousticEchoCanceler;
import android.media.AudioManager;
-import android.media.AudioTrack;
import android.media.AudioRecord;
+import android.media.AudioTrack;
import android.media.MediaRecorder;
+import android.media.audiofx.AcousticEchoCanceler;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
@@ -32,71 +32,130 @@
import com.android.compatibility.common.util.ResultType;
import com.android.compatibility.common.util.ResultUnit;
-import com.android.cts.verifier.audio.soundio.SoundPlayerObject;
-import com.android.cts.verifier.audio.soundio.SoundRecorderObject;
-import com.android.cts.verifier.audio.wavelib.*;
import com.android.cts.verifier.CtsVerifierReportLog;
-import com.android.cts.verifier.PassFailButtons;
import com.android.cts.verifier.R;
-
-import static com.android.cts.verifier.TestListActivity.sCurrentDisplayMode;
-import static com.android.cts.verifier.TestListAdapter.setTestNameSuffix;
+import com.android.cts.verifier.audio.wavelib.DspBufferDouble;
+import com.android.cts.verifier.audio.wavelib.DspBufferMath;
+import com.android.cts.verifier.audio.wavelib.PipeShort;
public class AudioAEC extends AudioFrequencyActivity implements View.OnClickListener {
private static final String TAG = "AudioAEC";
- // Test State
+ private static final int TEST_NONE = -1;
private static final int TEST_AEC = 0;
+ private static final int TEST_COUNT = 1;
+ private static final float MAX_VAL = (float)(1 << 15);
- // UI
+ private int mCurrentTest = TEST_NONE;
private LinearLayout mLinearLayout;
private Button mButtonTest;
private Button mButtonMandatoryYes;
private Button mButtonMandatoryNo;
private ProgressBar mProgress;
private TextView mResultTest;
+ private boolean mTestAECPassed;
+ private SoundPlayerObject mSPlayer;
+ private SoundRecorderObject mSRecorder;
+ private AcousticEchoCanceler mAec;
- // Sound IO
+ private boolean mMandatory = true;
+
private final int mBlockSizeSamples = 4096;
private final int mSamplingRate = 48000;
private final int mSelectedRecordSource = MediaRecorder.AudioSource.VOICE_COMMUNICATION;
- private SoundPlayerObject mSPlayer;
- private SoundRecorderObject mSRecorder;
-
- // Test Results
- private boolean mMandatory = true;
- private boolean mTestAECPassed;
-
private final int TEST_DURATION_MS = 8000;
private final int SHOT_FREQUENCY_MS = 200;
private final int CORRELATION_DURATION_MS = TEST_DURATION_MS - 3000;
private final int SHOT_COUNT_CORRELATION = CORRELATION_DURATION_MS/SHOT_FREQUENCY_MS;
private final int SHOT_COUNT = TEST_DURATION_MS/SHOT_FREQUENCY_MS;
-
private final float MIN_RMS_DB = -60.0f; //dB
private final float MIN_RMS_VAL = (float)Math.pow(10,(MIN_RMS_DB/20));
private final double TEST_THRESHOLD_AEC_ON = 0.5;
private final double TEST_THRESHOLD_AEC_OFF = 0.6;
- private RmsHelper mRMSRecorder1 =
- new RmsHelper(mBlockSizeSamples, SHOT_COUNT, MIN_RMS_DB, MIN_RMS_VAL);
- private RmsHelper mRMSRecorder2 =
- new RmsHelper(mBlockSizeSamples, SHOT_COUNT, MIN_RMS_DB, MIN_RMS_VAL);
+ private RmsHelper mRMSRecorder1 = new RmsHelper(mBlockSizeSamples, SHOT_COUNT);
+ private RmsHelper mRMSRecorder2 = new RmsHelper(mBlockSizeSamples, SHOT_COUNT);
- private RmsHelper mRMSPlayer1 =
- new RmsHelper(mBlockSizeSamples, SHOT_COUNT, MIN_RMS_DB, MIN_RMS_VAL);
- private RmsHelper mRMSPlayer2 =
- new RmsHelper(mBlockSizeSamples, SHOT_COUNT, MIN_RMS_DB, MIN_RMS_VAL);
+ private RmsHelper mRMSPlayer1 = new RmsHelper(mBlockSizeSamples, SHOT_COUNT);
+ private RmsHelper mRMSPlayer2 = new RmsHelper(mBlockSizeSamples, SHOT_COUNT);
private Thread mTestThread;
- // ReportLog schema
- private static final String SECTION_AEC = "aec_activity";
- private static final String KEY_AEC_MANDATORY = "aec_mandatory";
- private static final String KEY_AEC_MAX_WITH = "max_with_aec";
- private static final String KEY_AEC_MAX_WITHOUT = "max_without_aec";
- private static final String KEY_AEC_RESULT = "result_string";
+ //RMS helpers
+ public class RmsHelper {
+ private double mRmsCurrent;
+ public int mBlockSize;
+ private int mShoutCount;
+ public boolean mRunning = false;
+
+ private short[] mAudioShortArray;
+
+ private DspBufferDouble mRmsSnapshots;
+ private int mShotIndex;
+
+ public RmsHelper(int blockSize, int shotCount) {
+ mBlockSize = blockSize;
+ mShoutCount = shotCount;
+ reset();
+ }
+
+ public void reset() {
+ mAudioShortArray = new short[mBlockSize];
+ mRmsSnapshots = new DspBufferDouble(mShoutCount);
+ mShotIndex = 0;
+ mRmsCurrent = 0;
+ mRunning = false;
+ }
+
+ public void captureShot() {
+ if (mShotIndex >= 0 && mShotIndex < mRmsSnapshots.getSize()) {
+ mRmsSnapshots.setValue(mShotIndex++, mRmsCurrent);
+ }
+ }
+
+ public void setRunning(boolean running) {
+ mRunning = running;
+ }
+
+ public double getRmsCurrent() {
+ return mRmsCurrent;
+ }
+
+ public DspBufferDouble getRmsSnapshots() {
+ return mRmsSnapshots;
+ }
+
+ public boolean updateRms(PipeShort pipe, int channelCount, int channel) {
+ if (mRunning) {
+ int samplesAvailable = pipe.availableToRead();
+ while (samplesAvailable >= mBlockSize) {
+ pipe.read(mAudioShortArray, 0, mBlockSize);
+
+ double rmsTempSum = 0;
+ int count = 0;
+ for (int i = channel; i < mBlockSize; i += channelCount) {
+ float value = mAudioShortArray[i] / MAX_VAL;
+
+ rmsTempSum += value * value;
+ count++;
+ }
+ float rms = count > 0 ? (float)Math.sqrt(rmsTempSum / count) : 0f;
+ if (rms < MIN_RMS_VAL) {
+ rms = MIN_RMS_VAL;
+ }
+
+ double alpha = 0.9;
+ double total_rms = rms * alpha + mRmsCurrent * (1.0f - alpha);
+ mRmsCurrent = total_rms;
+
+ samplesAvailable = pipe.availableToRead();
+ }
+ return true;
+ }
+ return false;
+ }
+ }
//compute Acoustic Coupling Factor
private double computeAcousticCouplingFactor(DspBufferDouble buffRmsPlayer,
@@ -143,7 +202,7 @@
super.onCreate(savedInstanceState);
setContentView(R.layout.audio_aec_activity);
- // "AEC Mandatory" Buttons
+ //
mLinearLayout = (LinearLayout)findViewById(R.id.audio_aec_test_layout);
mButtonMandatoryYes = (Button) findViewById(R.id.audio_aec_mandatory_yes);
mButtonMandatoryYes.setOnClickListener(this);
@@ -151,15 +210,16 @@
mButtonMandatoryNo.setOnClickListener(this);
enableUILayout(mLinearLayout, false);
- // Test Button
+ // Test
mButtonTest = (Button) findViewById(R.id.audio_aec_button_test);
mButtonTest.setOnClickListener(this);
mProgress = (ProgressBar) findViewById(R.id.audio_aec_test_progress_bar);
mResultTest = (TextView) findViewById(R.id.audio_aec_test_result);
- showProgressIndicator(false);
+ showView(mProgress, false);
mSPlayer = new SoundPlayerObject(false, mBlockSizeSamples) {
+
@Override
public void periodicNotification(AudioTrack track) {
int channelCount = getChannelCount();
@@ -179,11 +239,12 @@
setPassFailButtonClickListeners();
getPassButton().setEnabled(false);
- setInfoResources(R.string.audio_aec_test, R.string.audio_aec_info, -1);
+ setInfoResources(R.string.audio_aec_test,
+ R.string.audio_aec_info, -1);
}
- private void showProgressIndicator(boolean show) {
- mProgress.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
+ private void showView(View v, boolean show) {
+ v.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
}
@Override
@@ -213,11 +274,7 @@
Log.v(TAG,"test Thread already running.");
return;
}
-
mTestThread = new Thread(new AudioTestRunner(TAG, TEST_AEC, mMessageHandler) {
- // AcousticEchoCanceler
- private AcousticEchoCanceler mAec;
-
public void run() {
super.run();
@@ -433,39 +490,28 @@
String msg) {
CtsVerifierReportLog reportLog = getReportLog();
- reportLog.addValue(KEY_AEC_MANDATORY,
+ reportLog.addValue("AEC_mandatory",
aecMandatory,
ResultType.NEUTRAL,
ResultUnit.NONE);
- reportLog.addValue(KEY_AEC_MAX_WITH,
+ reportLog.addValue("max_with_AEC",
maxAEC,
ResultType.LOWER_BETTER,
ResultUnit.SCORE);
- reportLog.addValue(KEY_AEC_MAX_WITHOUT,
+ reportLog.addValue("max_without_AEC",
maxNoAEC,
ResultType.HIGHER_BETTER,
ResultUnit.SCORE);
- reportLog.addValue(KEY_AEC_RESULT,
+ reportLog.addValue("result_string",
msg,
ResultType.NEUTRAL,
ResultUnit.NONE);
}
- //
- // PassFailButtons Overrides
- //
- @Override
- public String getReportFileName() { return PassFailButtons.AUDIO_TESTS_REPORT_LOG_NAME; }
-
- @Override
- public final String getReportSectionName() {
- return setTestNameSuffix(sCurrentDisplayMode, SECTION_AEC);
- }
-
- @Override
+ @Override // PassFailButtons
public void recordTestResults() {
getReportLog().submit();
}
@@ -477,7 +523,7 @@
public void testStarted(int testId, String str) {
super.testStarted(testId, str);
Log.v(TAG, "Test Started! " + testId + " str:"+str);
- showProgressIndicator(true);
+ showView(mProgress, true);
mTestAECPassed = false;
getPassButton().setEnabled(false);
mResultTest.setText("test in progress..");
@@ -494,7 +540,7 @@
public void testEndedOk(int testId, String str) {
super.testEndedOk(testId, str);
Log.v(TAG, "Test EndedOk. " + testId + " str:"+str);
- showProgressIndicator(false);
+ showView(mProgress, false);
mResultTest.setText("test completed. " + str);
if (!isReportLogOkToPass()) {
mResultTest.setText(getResources().getString(R.string.audio_general_reportlogtest));
@@ -507,7 +553,7 @@
public void testEndedError(int testId, String str) {
super.testEndedError(testId, str);
Log.v(TAG, "Test EndedError. " + testId + " str:"+str);
- showProgressIndicator(false);
+ showView(mProgress, false);
mResultTest.setText("test failed. " + str);
}
};
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyLineActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyLineActivity.java
index 9b24039..582ea0c 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyLineActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyLineActivity.java
@@ -32,7 +32,6 @@
import com.android.compatibility.common.util.ResultType;
import com.android.compatibility.common.util.ResultUnit;
-import com.android.cts.verifier.audio.soundio.SoundPlayerObject;
import com.android.cts.verifier.CtsVerifierReportLog;
import com.android.cts.verifier.R;
import com.android.cts.verifier.audio.wavelib.DspBufferComplex;
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyMicActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyMicActivity.java
index e066943..a83709d 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyMicActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyMicActivity.java
@@ -40,9 +40,6 @@
import com.android.cts.verifier.audio.wavelib.DspFftServer;
import com.android.cts.verifier.audio.wavelib.DspWindow;
import com.android.cts.verifier.audio.wavelib.PipeShort;
-import com.android.cts.verifier.audio.soundio.SoundPlayerObject;
-import com.android.cts.verifier.audio.wavelib.*;
-
import com.android.cts.verifier.audio.wavelib.VectorAverage;
/**
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencySpeakerActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencySpeakerActivity.java
index 0e63b6f..301eb9c 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencySpeakerActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencySpeakerActivity.java
@@ -32,7 +32,6 @@
import com.android.compatibility.common.util.ResultType;
import com.android.compatibility.common.util.ResultUnit;
-import com.android.cts.verifier.audio.soundio.SoundPlayerObject;
import com.android.cts.verifier.CtsVerifierReportLog;
import com.android.cts.verifier.R;
import com.android.cts.verifier.audio.wavelib.DspBufferComplex;
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyUnprocessedActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyUnprocessedActivity.java
index 21de117..3819be2 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyUnprocessedActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyUnprocessedActivity.java
@@ -34,7 +34,6 @@
import com.android.compatibility.common.util.CddTest;
import com.android.compatibility.common.util.ResultType;
import com.android.compatibility.common.util.ResultUnit;
-import com.android.cts.verifier.audio.soundio.SoundPlayerObject;
import com.android.cts.verifier.CtsVerifierReportLog;
import com.android.cts.verifier.R;
import com.android.cts.verifier.audio.wavelib.DspBufferComplex;
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyVoiceRecognitionActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyVoiceRecognitionActivity.java
index 442f626..23a016a 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyVoiceRecognitionActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyVoiceRecognitionActivity.java
@@ -30,8 +30,6 @@
import com.android.compatibility.common.util.ResultType;
import com.android.compatibility.common.util.ResultUnit;
-import com.android.cts.verifier.audio.soundio.SoundPlayerObject;
-import com.android.cts.verifier.audio.soundio.SoundRecorderObject;
import com.android.cts.verifier.CtsVerifierReportLog;
import com.android.cts.verifier.R;
import com.android.cts.verifier.audio.wavelib.DspBufferComplex;
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioMicrophoneMuteToggleActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioMicrophoneMuteToggleActivity.java
deleted file mode 100644
index 1e250ef..0000000
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioMicrophoneMuteToggleActivity.java
+++ /dev/null
@@ -1,210 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.cts.verifier.audio;
-
-import android.media.MediaRecorder;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.text.method.ScrollingMovementMethod;
-import android.util.Log;
-import android.view.View;
-import android.widget.Button;
-import android.widget.TextView;
-
-import com.android.compatibility.common.util.ResultType;
-import com.android.compatibility.common.util.ResultUnit;
-import com.android.cts.verifier.CtsVerifierReportLog;
-import com.android.cts.verifier.PassFailButtons;
-import com.android.cts.verifier.R;
-import com.android.cts.verifier.audio.audiolib.AudioCommon;
-import com.android.cts.verifier.audio.wavelib.WavAnalyzer;
-
-/**
- * Test for manual verification of microphone privacy hardware switches
- */
-public class AudioMicrophoneMuteToggleActivity extends PassFailButtons.Activity {
-
- public enum Status {
- START, RECORDING, DONE, PLAYER
- }
-
- private static final String TAG = "AudioMicrophoneMuteToggleActivity";
-
- private Status mStatus = Status.START;
- private TextView mInfoText;
- private Button mRecorderButton;
-
- private int mAudioSource = -1;
- private int mRecordRate = 0;
-
- // keys for report log
- private static final String KEY_REC_RATE = "rec_rate";
- private static final String KEY_AUDIO_SOURCE = "audio_source";
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.mic_hw_toggle);
- setInfoResources(R.string.audio_mic_toggle_test, R.string.audio_mic_toggle_test_info, -1);
- setPassFailButtonClickListeners();
- getPassButton().setEnabled(false);
-
- mInfoText = findViewById(R.id.info_text);
- mInfoText.setMovementMethod(new ScrollingMovementMethod());
- mInfoText.setText(R.string.audio_mic_toggle_test_instruction1);
-
- mRecorderButton = findViewById(R.id.recorder_button);
- mRecorderButton.setEnabled(true);
-
- final AudioRecordHelper audioRecorder = AudioRecordHelper.getInstance();
- mRecordRate = audioRecorder.getSampleRate();
-
- mRecorderButton.setOnClickListener(new View.OnClickListener() {
- private WavAnalyzerTask mWavAnalyzerTask = null;
-
- private void stopRecording() {
- audioRecorder.stop();
- mInfoText.append(getString(R.string.audio_mic_toggle_test_analyzing));
- mWavAnalyzerTask = new WavAnalyzerTask(audioRecorder.getByte());
- mWavAnalyzerTask.execute();
- mStatus = Status.DONE;
- }
-
- @Override
- public void onClick(View v) {
- switch (mStatus) {
- case START:
- mInfoText.append("Recording at " + mRecordRate + "Hz using ");
- mAudioSource = audioRecorder.getAudioSource();
- switch (mAudioSource) {
- case MediaRecorder.AudioSource.MIC:
- mInfoText.append("MIC");
- break;
- case MediaRecorder.AudioSource.VOICE_RECOGNITION:
- mInfoText.append("VOICE_RECOGNITION");
- break;
- default:
- mInfoText.append("UNEXPECTED " + mAudioSource);
- break;
- }
- mInfoText.append("\n");
- mStatus = Status.RECORDING;
-
- mRecorderButton.setEnabled(false);
- audioRecorder.start();
-
- final View finalV = v;
- new Thread() {
- @Override
- public void run() {
- double recordingDuration_millis = (1000 * (2.5
- + AudioCommon.PREFIX_LENGTH_S
- + AudioCommon.PAUSE_BEFORE_PREFIX_DURATION_S
- + AudioCommon.PAUSE_AFTER_PREFIX_DURATION_S
- + AudioCommon.PIP_NUM * (AudioCommon.PIP_DURATION_S
- + AudioCommon.PAUSE_DURATION_S)
- * AudioCommon.REPETITIONS));
- Log.d(TAG, "Recording for " + recordingDuration_millis + "ms");
- try {
- Thread.sleep((long) recordingDuration_millis);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- stopRecording();
- }
- });
- }
- }.start();
-
- break;
-
- default:
- break;
- }
- }
- });
-
- }
-
- @Override
- public void recordTestResults() {
- CtsVerifierReportLog reportLog = getReportLog();
-
- reportLog.addValue(
- KEY_REC_RATE,
- mRecordRate,
- ResultType.NEUTRAL,
- ResultUnit.NONE);
-
- reportLog.addValue(
- KEY_AUDIO_SOURCE,
- mAudioSource,
- ResultType.NEUTRAL,
- ResultUnit.NONE);
-
- reportLog.submit();
- }
-
- /**
- * AsyncTask class for the analyzing.
- */
- private class WavAnalyzerTask extends AsyncTask<Void, String, String>
- implements WavAnalyzer.Listener {
-
- private static final String TAG = "WavAnalyzerTask";
- private final WavAnalyzer mWavAnalyzer;
-
- public WavAnalyzerTask(byte[] recording) {
- mWavAnalyzer = new WavAnalyzer(recording, AudioCommon.RECORDING_SAMPLE_RATE_HZ,
- WavAnalyzerTask.this);
- }
-
- @Override
- protected String doInBackground(Void... params) {
- boolean result = mWavAnalyzer.doWork();
- if (result) {
- return getString(R.string.pass_button_text);
- }
- return getString(R.string.fail_button_text);
- }
-
- @Override
- protected void onPostExecute(String result) {
- if (mWavAnalyzer.isSilence()) {
- mInfoText.append(getString(R.string.passed));
- getPassButton().setEnabled(true);
- } else {
- mInfoText.append(getString(R.string.failed));
- }
- }
-
- @Override
- protected void onProgressUpdate(String... values) {
- for (String message : values) {
- Log.d(TAG, message);
- }
- }
-
- @Override
- public void sendMessage(String message) {
- publishProgress(message);
- }
- }
-}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/AudioCommon.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/Common.java
similarity index 90%
rename from apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/AudioCommon.java
rename to apps/CtsVerifier/src/com/android/cts/verifier/audio/Common.java
index ba5e39b2..df7460a 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/AudioCommon.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/Common.java
@@ -1,18 +1,15 @@
-package com.android.cts.verifier.audio.audiolib;
+package com.android.cts.verifier.audio;
import android.media.AudioManager;
import android.media.AudioTrack;
-import com.android.cts.verifier.audio.AudioRecordHelper;
-import com.android.cts.verifier.audio.Util;
-
import java.util.ArrayList;
import java.util.Random;
/**
* This class stores common constants and methods.
*/
-public class AudioCommon {
+public class Common {
public static final int RECORDING_SAMPLE_RATE_HZ
= AudioRecordHelper.getInstance().getSampleRate();
@@ -98,7 +95,7 @@
private static double[] frequencies() {
double[] originalFrequencies = originalFrequencies();
- double[] randomFrequencies = new double[AudioCommon.REPETITIONS * originalFrequencies.length];
+ double[] randomFrequencies = new double[Common.REPETITIONS * originalFrequencies.length];
for (int i = 0; i < REPETITIONS * originalFrequencies.length; i++) {
randomFrequencies[i] = originalFrequencies[ORDER[i] % originalFrequencies.length];
}
@@ -111,13 +108,13 @@
*/
private static double[] originalFrequencies() {
ArrayList<Double> frequencies = new ArrayList<Double>();
- double frequency = AudioCommon.MIN_FREQUENCY_HZ;
- while (frequency <= AudioCommon.MAX_FREQUENCY_HZ) {
+ double frequency = Common.MIN_FREQUENCY_HZ;
+ while (frequency <= Common.MAX_FREQUENCY_HZ) {
frequencies.add(new Double(frequency));
if ((frequency >= 18500) && (frequency < 20000)) {
- frequency += AudioCommon.FREQUENCY_STEP_HZ;
+ frequency += Common.FREQUENCY_STEP_HZ;
} else {
- frequency += AudioCommon.FREQUENCY_STEP_HZ * 10;
+ frequency += Common.FREQUENCY_STEP_HZ * 10;
}
}
Double[] frequenciesArray = frequencies.toArray(new Double[frequencies.size()]);
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/HifiUltrasoundSpeakerTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/HifiUltrasoundSpeakerTestActivity.java
index 232e14d..cac4659 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/HifiUltrasoundSpeakerTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/HifiUltrasoundSpeakerTestActivity.java
@@ -37,10 +37,6 @@
import android.widget.TextView;
import java.util.Arrays;
-import com.android.cts.verifier.audio.audiolib.AudioCommon;
-import com.android.cts.verifier.audio.soundio.SoundGenerator;
-import com.android.cts.verifier.audio.wavelib.WavAnalyzer;
-
import com.androidplot.xy.PointLabelFormatter;
import com.androidplot.xy.LineAndPointFormatter;
import com.androidplot.xy.SimpleXYSeries;
@@ -184,12 +180,11 @@
@Override
public void run() {
Double recordingDuration_millis = new Double(1000 * (2.5
- + AudioCommon.PREFIX_LENGTH_S
- + AudioCommon.PAUSE_BEFORE_PREFIX_DURATION_S
- + AudioCommon.PAUSE_AFTER_PREFIX_DURATION_S
- + AudioCommon.PIP_NUM
- * (AudioCommon.PIP_DURATION_S + AudioCommon.PAUSE_DURATION_S)
- * AudioCommon.REPETITIONS));
+ + Common.PREFIX_LENGTH_S
+ + Common.PAUSE_BEFORE_PREFIX_DURATION_S
+ + Common.PAUSE_AFTER_PREFIX_DURATION_S
+ + Common.PIP_NUM * (Common.PIP_DURATION_S + Common.PAUSE_DURATION_S)
+ * Common.REPETITIONS));
Log.d(TAG, "Recording for " + recordingDuration_millis + "ms");
try {
Thread.sleep(recordingDuration_millis.intValue());
@@ -266,17 +261,17 @@
XYPlot plot = (XYPlot) popupView.findViewById(R.id.responseChart);
plot.setDomainStep(XYStepMode.INCREMENT_BY_VAL, 2000);
- Double[] frequencies = new Double[AudioCommon.PIP_NUM];
- for (int i = 0; i < AudioCommon.PIP_NUM; i++) {
- frequencies[i] = new Double(AudioCommon.FREQUENCIES_ORIGINAL[i]);
+ Double[] frequencies = new Double[Common.PIP_NUM];
+ for (int i = 0; i < Common.PIP_NUM; i++) {
+ frequencies[i] = new Double(Common.FREQUENCIES_ORIGINAL[i]);
}
if (wavAnalyzerTask != null) {
double[][] power = wavAnalyzerTask.getPower();
- for(int i = 0; i < AudioCommon.REPETITIONS; i++) {
- Double[] powerWrap = new Double[AudioCommon.PIP_NUM];
- for (int j = 0; j < AudioCommon.PIP_NUM; j++) {
+ for(int i = 0; i < Common.REPETITIONS; i++) {
+ Double[] powerWrap = new Double[Common.PIP_NUM];
+ for (int j = 0; j < Common.PIP_NUM; j++) {
powerWrap[j] = new Double(10 * Math.log10(power[j][i]));
}
XYSeries series = new SimpleXYSeries(
@@ -291,8 +286,8 @@
}
double[] noiseDB = wavAnalyzerTask.getNoiseDB();
- Double[] noiseDBWrap = new Double[AudioCommon.PIP_NUM];
- for (int i = 0; i < AudioCommon.PIP_NUM; i++) {
+ Double[] noiseDBWrap = new Double[Common.PIP_NUM];
+ for (int i = 0; i < Common.PIP_NUM; i++) {
noiseDBWrap[i] = new Double(noiseDB[i]);
}
@@ -307,8 +302,8 @@
plot.addSeries(noiseSeries, noiseSeriesFormat);
double[] dB = wavAnalyzerTask.getDB();
- Double[] dBWrap = new Double[AudioCommon.PIP_NUM];
- for (int i = 0; i < AudioCommon.PIP_NUM; i++) {
+ Double[] dBWrap = new Double[Common.PIP_NUM];
+ for (int i = 0; i < Common.PIP_NUM; i++) {
dBWrap[i] = new Double(dB[i]);
}
@@ -322,7 +317,7 @@
R.xml.ultrasound_line_formatter_median);
plot.addSeries(series, seriesFormat);
- Double[] passX = new Double[] {AudioCommon.MIN_FREQUENCY_HZ, AudioCommon.MAX_FREQUENCY_HZ};
+ Double[] passX = new Double[] {Common.MIN_FREQUENCY_HZ, Common.MAX_FREQUENCY_HZ};
Double[] passY = new Double[] {wavAnalyzerTask.getThreshold(), wavAnalyzerTask.getThreshold()};
XYSeries passSeries = new SimpleXYSeries(
Arrays.asList(passX), Arrays.asList(passY), "passing");
@@ -338,7 +333,7 @@
* Plays the generated pips.
*/
private void play() {
- play(SoundGenerator.getInstance().getByte(), AudioCommon.PLAYING_SAMPLE_RATE_HZ);
+ play(SoundGenerator.getInstance().getByte(), Common.PLAYING_SAMPLE_RATE_HZ);
}
/**
@@ -368,7 +363,7 @@
WavAnalyzer wavAnalyzer;
public WavAnalyzerTask(byte[] recording) {
- wavAnalyzer = new WavAnalyzer(recording, AudioCommon.RECORDING_SAMPLE_RATE_HZ,
+ wavAnalyzer = new WavAnalyzer(recording, Common.RECORDING_SAMPLE_RATE_HZ,
WavAnalyzerTask.this);
}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/HifiUltrasoundTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/HifiUltrasoundTestActivity.java
index a5a11f2..5f1be38 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/HifiUltrasoundTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/HifiUltrasoundTestActivity.java
@@ -37,10 +37,6 @@
import android.widget.TextView;
import java.util.Arrays;
-import com.android.cts.verifier.audio.audiolib.AudioCommon;
-import com.android.cts.verifier.audio.soundio.SoundGenerator;
-import com.android.cts.verifier.audio.wavelib.WavAnalyzer;
-
import com.androidplot.xy.PointLabelFormatter;
import com.androidplot.xy.LineAndPointFormatter;
import com.androidplot.xy.SimpleXYSeries;
@@ -164,11 +160,11 @@
@Override
public void run() {
Double recordingDuration_millis = new Double(1000 * (2.5
- + AudioCommon.PREFIX_LENGTH_S
- + AudioCommon.PAUSE_BEFORE_PREFIX_DURATION_S
- + AudioCommon.PAUSE_AFTER_PREFIX_DURATION_S
- + AudioCommon.PIP_NUM * (AudioCommon.PIP_DURATION_S + AudioCommon.PAUSE_DURATION_S)
- * AudioCommon.REPETITIONS));
+ + Common.PREFIX_LENGTH_S
+ + Common.PAUSE_BEFORE_PREFIX_DURATION_S
+ + Common.PAUSE_AFTER_PREFIX_DURATION_S
+ + Common.PIP_NUM * (Common.PIP_DURATION_S + Common.PAUSE_DURATION_S)
+ * Common.REPETITIONS));
Log.d(TAG, "Recording for " + recordingDuration_millis + "ms");
try {
Thread.sleep(recordingDuration_millis.intValue());
@@ -224,18 +220,18 @@
XYPlot plot = (XYPlot) popupView.findViewById(R.id.responseChart);
plot.setDomainStep(XYStepMode.INCREMENT_BY_VAL, 2000);
- Double[] frequencies = new Double[AudioCommon.PIP_NUM];
- for (int i = 0; i < AudioCommon.PIP_NUM; i++) {
- frequencies[i] = new Double(AudioCommon.FREQUENCIES_ORIGINAL[i]);
+ Double[] frequencies = new Double[Common.PIP_NUM];
+ for (int i = 0; i < Common.PIP_NUM; i++) {
+ frequencies[i] = new Double(Common.FREQUENCIES_ORIGINAL[i]);
}
if (wavAnalyzerTask != null && wavAnalyzerTask.getPower() != null &&
wavAnalyzerTask.getNoiseDB() != null && wavAnalyzerTask.getDB() != null) {
double[][] power = wavAnalyzerTask.getPower();
- for(int i = 0; i < AudioCommon.REPETITIONS; i++) {
- Double[] powerWrap = new Double[AudioCommon.PIP_NUM];
- for (int j = 0; j < AudioCommon.PIP_NUM; j++) {
+ for(int i = 0; i < Common.REPETITIONS; i++) {
+ Double[] powerWrap = new Double[Common.PIP_NUM];
+ for (int j = 0; j < Common.PIP_NUM; j++) {
powerWrap[j] = new Double(10 * Math.log10(power[j][i]));
}
XYSeries series = new SimpleXYSeries(
@@ -250,8 +246,8 @@
}
double[] noiseDB = wavAnalyzerTask.getNoiseDB();
- Double[] noiseDBWrap = new Double[AudioCommon.PIP_NUM];
- for (int i = 0; i < AudioCommon.PIP_NUM; i++) {
+ Double[] noiseDBWrap = new Double[Common.PIP_NUM];
+ for (int i = 0; i < Common.PIP_NUM; i++) {
noiseDBWrap[i] = new Double(noiseDB[i]);
}
@@ -266,8 +262,8 @@
plot.addSeries(noiseSeries, noiseSeriesFormat);
double[] dB = wavAnalyzerTask.getDB();
- Double[] dBWrap = new Double[AudioCommon.PIP_NUM];
- for (int i = 0; i < AudioCommon.PIP_NUM; i++) {
+ Double[] dBWrap = new Double[Common.PIP_NUM];
+ for (int i = 0; i < Common.PIP_NUM; i++) {
dBWrap[i] = new Double(dB[i]);
}
@@ -281,7 +277,7 @@
R.xml.ultrasound_line_formatter_median);
plot.addSeries(series, seriesFormat);
- Double[] passX = new Double[] {AudioCommon.MIN_FREQUENCY_HZ, AudioCommon.MAX_FREQUENCY_HZ};
+ Double[] passX = new Double[] {Common.MIN_FREQUENCY_HZ, Common.MAX_FREQUENCY_HZ};
Double[] passY = new Double[] {wavAnalyzerTask.getThreshold(), wavAnalyzerTask.getThreshold()};
XYSeries passSeries = new SimpleXYSeries(
Arrays.asList(passX), Arrays.asList(passY), "passing");
@@ -297,7 +293,7 @@
* Plays the generated pips.
*/
private void play() {
- play(SoundGenerator.getInstance().getByte(), AudioCommon.PLAYING_SAMPLE_RATE_HZ);
+ play(SoundGenerator.getInstance().getByte(), Common.PLAYING_SAMPLE_RATE_HZ);
}
/**
@@ -327,7 +323,7 @@
WavAnalyzer wavAnalyzer;
public WavAnalyzerTask(byte[] recording) {
- wavAnalyzer = new WavAnalyzer(recording, AudioCommon.RECORDING_SAMPLE_RATE_HZ,
+ wavAnalyzer = new WavAnalyzer(recording, Common.RECORDING_SAMPLE_RATE_HZ,
WavAnalyzerTask.this);
}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/SoundGenerator.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/SoundGenerator.java
new file mode 100644
index 0000000..0ad9371
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/SoundGenerator.java
@@ -0,0 +1,81 @@
+package com.android.cts.verifier.audio;
+
+/**
+ * Sound generator.
+ */
+public class SoundGenerator {
+
+ private static SoundGenerator instance;
+
+ private final byte[] generatedSound;
+ private final double[] sample;
+
+ private SoundGenerator() {
+ // Initialize sample.
+ int pipNum = Common.PIP_NUM;
+ int prefixTotalLength = Util.toLength(Common.PREFIX_LENGTH_S, Common.PLAYING_SAMPLE_RATE_HZ)
+ + Util.toLength(Common.PAUSE_BEFORE_PREFIX_DURATION_S, Common.PLAYING_SAMPLE_RATE_HZ)
+ + Util.toLength(Common.PAUSE_AFTER_PREFIX_DURATION_S, Common.PLAYING_SAMPLE_RATE_HZ);
+ int repetitionLength = pipNum * Util.toLength(
+ Common.PIP_DURATION_S + Common.PAUSE_DURATION_S, Common.PLAYING_SAMPLE_RATE_HZ);
+ int sampleLength = prefixTotalLength + Common.REPETITIONS * repetitionLength;
+ sample = new double[sampleLength];
+
+ // Fill sample with prefix.
+ System.arraycopy(Common.PREFIX_FOR_PLAYER, 0, sample,
+ Util.toLength(Common.PAUSE_BEFORE_PREFIX_DURATION_S, Common.PLAYING_SAMPLE_RATE_HZ),
+ Common.PREFIX_FOR_PLAYER.length);
+
+ // Fill the sample.
+ for (int i = 0; i < pipNum * Common.REPETITIONS; i++) {
+ double[] pip = getPip(Common.WINDOW_FOR_PLAYER, Common.FREQUENCIES[i]);
+ System.arraycopy(pip, 0, sample,
+ prefixTotalLength + i * Util.toLength(
+ Common.PIP_DURATION_S + Common.PAUSE_DURATION_S, Common.PLAYING_SAMPLE_RATE_HZ),
+ pip.length);
+ }
+
+ // Convert sample to byte.
+ generatedSound = new byte[2 * sample.length];
+ int i = 0;
+ for (double dVal : sample) {
+ short val = (short) ((dVal * 32767));
+ generatedSound[i++] = (byte) (val & 0x00ff);
+ generatedSound[i++] = (byte) ((val & 0xff00) >>> 8);
+ }
+ }
+
+ public static SoundGenerator getInstance() {
+ if (instance == null) {
+ instance = new SoundGenerator();
+ }
+ return instance;
+ }
+
+ /**
+ * Gets a pip sample.
+ */
+ private static double[] getPip(double[] window, double frequency) {
+ int pipArrayLength = window.length;
+ double[] pipArray = new double[pipArrayLength];
+ double radPerSample = 2 * Math.PI / (Common.PLAYING_SAMPLE_RATE_HZ / frequency);
+ for (int i = 0; i < pipArrayLength; i++) {
+ pipArray[i] = window[i] * Math.sin(i * radPerSample);
+ }
+ return pipArray;
+ }
+
+ /**
+ * Get generated sound in byte[].
+ */
+ public byte[] getByte() {
+ return generatedSound;
+ }
+
+ /**
+ * Get sample in double[].
+ */
+ public double[] getSample() {
+ return sample;
+ }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/soundio/SoundPlayerObject.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/SoundPlayerObject.java
similarity index 99%
rename from apps/CtsVerifier/src/com/android/cts/verifier/audio/soundio/SoundPlayerObject.java
rename to apps/CtsVerifier/src/com/android/cts/verifier/audio/SoundPlayerObject.java
index 156460b..0d93dbb 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/soundio/SoundPlayerObject.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/SoundPlayerObject.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.cts.verifier.audio.soundio;
+package com.android.cts.verifier.audio;
import android.content.Context;
import android.media.AudioAttributes;
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/soundio/SoundRecorderObject.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/SoundRecorderObject.java
similarity index 98%
rename from apps/CtsVerifier/src/com/android/cts/verifier/audio/soundio/SoundRecorderObject.java
rename to apps/CtsVerifier/src/com/android/cts/verifier/audio/SoundRecorderObject.java
index d83a5dd..8950ec5 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/soundio/SoundRecorderObject.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/SoundRecorderObject.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.cts.verifier.audio.soundio;
+package com.android.cts.verifier.audio;
import android.media.AudioFormat;
import android.media.AudioRecord;
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/wavelib/WavAnalyzer.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/WavAnalyzer.java
similarity index 62%
rename from apps/CtsVerifier/src/com/android/cts/verifier/audio/wavelib/WavAnalyzer.java
rename to apps/CtsVerifier/src/com/android/cts/verifier/audio/WavAnalyzer.java
index 038f080..b75c40b 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/wavelib/WavAnalyzer.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/WavAnalyzer.java
@@ -1,22 +1,4 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.cts.verifier.audio.wavelib;
-
-import com.android.cts.verifier.audio.audiolib.AudioCommon;
-import com.android.cts.verifier.audio.Util;
+package com.android.cts.verifier.audio;
import org.apache.commons.math.complex.Complex;
@@ -27,8 +9,6 @@
* Class contains the analysis to calculate frequency response.
*/
public class WavAnalyzer {
- final double SILENCE_THRESHOLD = Short.MAX_VALUE / 100.0f;
-
private final Listener listener;
private final int sampleRate; // Recording sampling rate.
private double[] data; // Whole recording data.
@@ -80,7 +60,7 @@
/**
* Check if the recording is clipped.
*/
- public boolean isClipped() {
+ boolean isClipped() {
for (int i = 1; i < data.length; i++) {
if ((Math.abs(data[i]) >= Short.MAX_VALUE) && (Math.abs(data[i - 1]) >= Short.MAX_VALUE)) {
listener.sendMessage("WARNING: Data is clipped."
@@ -94,11 +74,11 @@
/**
* Check if the result is consistant across trials.
*/
- public boolean isConsistent() {
- double[] coeffOfVar = new double[AudioCommon.PIP_NUM];
- for (int i = 0; i < AudioCommon.PIP_NUM; i++) {
- double[] powerAtFreq = new double[AudioCommon.REPETITIONS];
- for (int j = 0; j < AudioCommon.REPETITIONS; j++) {
+ boolean isConsistent() {
+ double[] coeffOfVar = new double[Common.PIP_NUM];
+ for (int i = 0; i < Common.PIP_NUM; i++) {
+ double[] powerAtFreq = new double[Common.REPETITIONS];
+ for (int j = 0; j < Common.REPETITIONS; j++) {
powerAtFreq[j] = power[i][j];
}
coeffOfVar[i] = Util.std(powerAtFreq) / Util.mean(powerAtFreq);
@@ -114,7 +94,7 @@
/**
* Determine test pass/fail using the frequency response. Package visible for unit testing.
*/
- public boolean responsePassesHifiTest(double[] dB) {
+ boolean responsePassesHifiTest(double[] dB) {
for (int i = 0; i < dB.length; i++) {
// Precautionary; NaN should not happen.
if (Double.isNaN(dB[i])) {
@@ -124,16 +104,16 @@
}
}
- if (Util.mean(dB) - Util.mean(noiseDB) < AudioCommon.SIGNAL_MIN_STRENGTH_DB_ABOVE_NOISE) {
+ if (Util.mean(dB) - Util.mean(noiseDB) < Common.SIGNAL_MIN_STRENGTH_DB_ABOVE_NOISE) {
listener.sendMessage("WARNING: Signal is too weak or background noise is too strong."
+ " Turn up the volume of the playback device or move to a quieter location.\n");
return false;
}
- int indexOf2000Hz = Util.findClosest(AudioCommon.FREQUENCIES_ORIGINAL, 2000.0);
- threshold = dB[indexOf2000Hz] + AudioCommon.PASSING_THRESHOLD_DB;
- int indexOf18500Hz = Util.findClosest(AudioCommon.FREQUENCIES_ORIGINAL, 18500.0);
- int indexOf20000Hz = Util.findClosest(AudioCommon.FREQUENCIES_ORIGINAL, 20000.0);
+ int indexOf2000Hz = Util.findClosest(Common.FREQUENCIES_ORIGINAL, 2000.0);
+ threshold = dB[indexOf2000Hz] + Common.PASSING_THRESHOLD_DB;
+ int indexOf18500Hz = Util.findClosest(Common.FREQUENCIES_ORIGINAL, 18500.0);
+ int indexOf20000Hz = Util.findClosest(Common.FREQUENCIES_ORIGINAL, 20000.0);
double[] responseInRange = new double[indexOf20000Hz - indexOf18500Hz];
System.arraycopy(dB, indexOf18500Hz, responseInRange, 0, responseInRange.length);
if (Util.mean(responseInRange) < threshold) {
@@ -148,15 +128,15 @@
* Calculate the Fourier Coefficient at the pip frequency to calculate the frequency response.
* Package visible for unit testing.
*/
- public double[] measurePipStrength() {
+ double[] measurePipStrength() {
listener.sendMessage("Aligning data... Please wait...\n");
final int dataStartI = alignData();
final int prefixTotalLength = dataStartI
- + Util.toLength(AudioCommon.PREFIX_LENGTH_S + AudioCommon.PAUSE_AFTER_PREFIX_DURATION_S, sampleRate);
+ + Util.toLength(Common.PREFIX_LENGTH_S + Common.PAUSE_AFTER_PREFIX_DURATION_S, sampleRate);
listener.sendMessage("Done.\n");
listener.sendMessage("Prefix starts at " + (double) dataStartI / sampleRate + " s \n");
- if (dataStartI > Math.round(sampleRate * (AudioCommon.PREFIX_LENGTH_S
- + AudioCommon.PAUSE_BEFORE_PREFIX_DURATION_S + AudioCommon.PAUSE_AFTER_PREFIX_DURATION_S))) {
+ if (dataStartI > Math.round(sampleRate * (Common.PREFIX_LENGTH_S
+ + Common.PAUSE_BEFORE_PREFIX_DURATION_S + Common.PAUSE_AFTER_PREFIX_DURATION_S))) {
listener.sendMessage("WARNING: Unexpected prefix start time. May have missed the prefix.\n"
+ "PLAY button should be pressed on the playback device within one second"
+ " after RECORD is pressed on the recording device.\n"
@@ -165,17 +145,17 @@
}
listener.sendMessage("Analyzing noise strength... Please wait...\n");
- noisePower = new double[AudioCommon.PIP_NUM][AudioCommon.NOISE_SAMPLES];
- noiseDB = new double[AudioCommon.PIP_NUM];
- for (int s = 0; s < AudioCommon.NOISE_SAMPLES; s++) {
- double[] noisePoints = new double[AudioCommon.WINDOW_FOR_RECORDER.length];
+ noisePower = new double[Common.PIP_NUM][Common.NOISE_SAMPLES];
+ noiseDB = new double[Common.PIP_NUM];
+ for (int s = 0; s < Common.NOISE_SAMPLES; s++) {
+ double[] noisePoints = new double[Common.WINDOW_FOR_RECORDER.length];
System.arraycopy(data, dataStartI - (s + 1) * noisePoints.length - 1,
noisePoints, 0, noisePoints.length);
for (int j = 0; j < noisePoints.length; j++) {
- noisePoints[j] = noisePoints[j] * AudioCommon.WINDOW_FOR_RECORDER[j];
+ noisePoints[j] = noisePoints[j] * Common.WINDOW_FOR_RECORDER[j];
}
- for (int i = 0; i < AudioCommon.PIP_NUM; i++) {
- double freq = AudioCommon.FREQUENCIES_ORIGINAL[i];
+ for (int i = 0; i < Common.PIP_NUM; i++) {
+ double freq = Common.FREQUENCIES_ORIGINAL[i];
Complex fourierCoeff = new Complex(0, 0);
final Complex rotator = new Complex(0,
-2.0 * Math.PI * freq / sampleRate).exp();
@@ -188,48 +168,48 @@
noisePower[i][s] = fourierCoeff.multiply(fourierCoeff.conjugate()).abs();
}
}
- for (int i = 0; i < AudioCommon.PIP_NUM; i++) {
+ for (int i = 0; i < Common.PIP_NUM; i++) {
double meanNoisePower = 0;
- for (int j = 0; j < AudioCommon.NOISE_SAMPLES; j++) {
+ for (int j = 0; j < Common.NOISE_SAMPLES; j++) {
meanNoisePower += noisePower[i][j];
}
- meanNoisePower /= AudioCommon.NOISE_SAMPLES;
+ meanNoisePower /= Common.NOISE_SAMPLES;
noiseDB[i] = 10 * Math.log10(meanNoisePower);
}
listener.sendMessage("Analyzing pips... Please wait...\n");
- power = new double[AudioCommon.PIP_NUM][AudioCommon.REPETITIONS];
- for (int i = 0; i < AudioCommon.PIP_NUM * AudioCommon.REPETITIONS; i++) {
- if (i % AudioCommon.PIP_NUM == 0) {
- listener.sendMessage("#" + (i / AudioCommon.PIP_NUM + 1) + "\n");
+ power = new double[Common.PIP_NUM][Common.REPETITIONS];
+ for (int i = 0; i < Common.PIP_NUM * Common.REPETITIONS; i++) {
+ if (i % Common.PIP_NUM == 0) {
+ listener.sendMessage("#" + (i / Common.PIP_NUM + 1) + "\n");
}
int pipExpectedStartI;
pipExpectedStartI = prefixTotalLength
- + Util.toLength(i * (AudioCommon.PIP_DURATION_S + AudioCommon.PAUSE_DURATION_S), sampleRate);
+ + Util.toLength(i * (Common.PIP_DURATION_S + Common.PAUSE_DURATION_S), sampleRate);
// Cut out the data points for the current pip.
- double[] pipPoints = new double[AudioCommon.WINDOW_FOR_RECORDER.length];
+ double[] pipPoints = new double[Common.WINDOW_FOR_RECORDER.length];
System.arraycopy(data, pipExpectedStartI, pipPoints, 0, pipPoints.length);
- for (int j = 0; j < AudioCommon.WINDOW_FOR_RECORDER.length; j++) {
- pipPoints[j] = pipPoints[j] * AudioCommon.WINDOW_FOR_RECORDER[j];
+ for (int j = 0; j < Common.WINDOW_FOR_RECORDER.length; j++) {
+ pipPoints[j] = pipPoints[j] * Common.WINDOW_FOR_RECORDER[j];
}
Complex fourierCoeff = new Complex(0, 0);
final Complex rotator = new Complex(0,
- -2.0 * Math.PI * AudioCommon.FREQUENCIES[i] / sampleRate).exp();
+ -2.0 * Math.PI * Common.FREQUENCIES[i] / sampleRate).exp();
Complex phasor = new Complex(1, 0);
for (int j = 0; j < pipPoints.length; j++) {
fourierCoeff = fourierCoeff.add(phasor.multiply(pipPoints[j]));
phasor = phasor.multiply(rotator);
}
fourierCoeff = fourierCoeff.multiply(1.0 / pipPoints.length);
- int j = AudioCommon.ORDER[i];
- power[j % AudioCommon.PIP_NUM][j / AudioCommon.PIP_NUM] =
+ int j = Common.ORDER[i];
+ power[j % Common.PIP_NUM][j / Common.PIP_NUM] =
fourierCoeff.multiply(fourierCoeff.conjugate()).abs();
}
// Calculate median of trials.
- double[] dB = new double[AudioCommon.PIP_NUM];
- for (int i = 0; i < AudioCommon.PIP_NUM; i++) {
+ double[] dB = new double[Common.PIP_NUM];
+ for (int i = 0; i < Common.PIP_NUM; i++) {
dB[i] = 10 * Math.log10(Util.median(power[i]));
}
return dB;
@@ -238,52 +218,41 @@
/**
* Align data using prefix. Package visible for unit testing.
*/
- public int alignData() {
+ int alignData() {
// Zeropadding samples to add in the correlation to avoid FFT wraparound.
- final int zeroPad =
- Util.toLength(AudioCommon.PREFIX_LENGTH_S, AudioCommon.RECORDING_SAMPLE_RATE_HZ) - 1;
- int fftSize = Util.nextPowerOfTwo((int) Math.round(sampleRate * (AudioCommon.PREFIX_LENGTH_S
- + AudioCommon.PAUSE_BEFORE_PREFIX_DURATION_S
- + AudioCommon.PAUSE_AFTER_PREFIX_DURATION_S + 0.5))
+ final int zeroPad = Util.toLength(Common.PREFIX_LENGTH_S, Common.RECORDING_SAMPLE_RATE_HZ) - 1;
+ int fftSize = Util.nextPowerOfTwo((int) Math.round(sampleRate * (Common.PREFIX_LENGTH_S
+ + Common.PAUSE_BEFORE_PREFIX_DURATION_S + Common.PAUSE_AFTER_PREFIX_DURATION_S + 0.5))
+ zeroPad);
double[] dataCut = new double[fftSize - zeroPad];
System.arraycopy(data, 0, dataCut, 0, fftSize - zeroPad);
double[] xCorrDataPrefix = Util.computeCrossCorrelation(
Util.padZeros(Util.toComplex(dataCut), fftSize),
- Util.padZeros(Util.toComplex(AudioCommon.PREFIX_FOR_RECORDER), fftSize));
+ Util.padZeros(Util.toComplex(Common.PREFIX_FOR_RECORDER), fftSize));
return Util.findMaxIndex(xCorrDataPrefix);
}
- public double[] getDB() {
+ double[] getDB() {
return dB;
}
- public double[][] getPower() {
+ double[][] getPower() {
return power;
}
- public double[] getNoiseDB() {
+ double[] getNoiseDB() {
return noiseDB;
}
- public double getThreshold() {
+ double getThreshold() {
return threshold;
}
- public boolean getResult() {
+ boolean getResult() {
return result;
}
- public boolean isSilence() {
- for (int i = 0; i < data.length; i++) {
- if (Math.abs(data[i]) > SILENCE_THRESHOLD) {
- return false;
- }
- }
- return true;
- }
-
/**
* An interface for listening a message publishing the progress of the analyzer.
*/
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/soundio/SoundGenerator.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/soundio/SoundGenerator.java
deleted file mode 100644
index 3ea5790..0000000
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/soundio/SoundGenerator.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.cts.verifier.audio.soundio;
-
-import com.android.cts.verifier.audio.audiolib.AudioCommon;
-import com.android.cts.verifier.audio.Util;
-
-/**
- * Sound generator.
- */
-public class SoundGenerator {
-
- private static SoundGenerator instance;
-
- private final byte[] generatedSound;
- private final double[] sample;
-
- private SoundGenerator() {
- // Initialize sample.
- int pipNum = AudioCommon.PIP_NUM;
- int prefixTotalLength =
- Util.toLength(AudioCommon.PREFIX_LENGTH_S, AudioCommon.PLAYING_SAMPLE_RATE_HZ)
- + Util.toLength(AudioCommon.PAUSE_BEFORE_PREFIX_DURATION_S,
- AudioCommon.PLAYING_SAMPLE_RATE_HZ)
- + Util.toLength(AudioCommon.PAUSE_AFTER_PREFIX_DURATION_S,
- AudioCommon.PLAYING_SAMPLE_RATE_HZ);
- int repetitionLength = pipNum * Util.toLength(
- AudioCommon.PIP_DURATION_S + AudioCommon.PAUSE_DURATION_S,
- AudioCommon.PLAYING_SAMPLE_RATE_HZ);
- int sampleLength = prefixTotalLength + AudioCommon.REPETITIONS * repetitionLength;
- sample = new double[sampleLength];
-
- // Fill sample with prefix.
- System.arraycopy(AudioCommon.PREFIX_FOR_PLAYER, 0, sample,
- Util.toLength(AudioCommon.PAUSE_BEFORE_PREFIX_DURATION_S,
- AudioCommon.PLAYING_SAMPLE_RATE_HZ),
- AudioCommon.PREFIX_FOR_PLAYER.length);
-
- // Fill the sample.
- for (int i = 0; i < pipNum * AudioCommon.REPETITIONS; i++) {
- double[] pip = getPip(AudioCommon.WINDOW_FOR_PLAYER, AudioCommon.FREQUENCIES[i]);
- System.arraycopy(pip, 0, sample,
- prefixTotalLength + i * Util.toLength(
- AudioCommon.PIP_DURATION_S + AudioCommon.PAUSE_DURATION_S,
- AudioCommon.PLAYING_SAMPLE_RATE_HZ),
- pip.length);
- }
-
- // Convert sample to byte.
- generatedSound = new byte[2 * sample.length];
- int i = 0;
- for (double dVal : sample) {
- short val = (short) ((dVal * 32767));
- generatedSound[i++] = (byte) (val & 0x00ff);
- generatedSound[i++] = (byte) ((val & 0xff00) >>> 8);
- }
- }
-
- public static SoundGenerator getInstance() {
- if (instance == null) {
- instance = new SoundGenerator();
- }
- return instance;
- }
-
- /**
- * Gets a pip sample.
- */
- private static double[] getPip(double[] window, double frequency) {
- int pipArrayLength = window.length;
- double[] pipArray = new double[pipArrayLength];
- double radPerSample = 2 * Math.PI / (AudioCommon.PLAYING_SAMPLE_RATE_HZ / frequency);
- for (int i = 0; i < pipArrayLength; i++) {
- pipArray[i] = window[i] * Math.sin(i * radPerSample);
- }
- return pipArray;
- }
-
- /**
- * Get generated sound in byte[].
- */
- public byte[] getByte() {
- return generatedSound;
- }
-
- /**
- * Get sample in double[].
- */
- public double[] getSample() {
- return sample;
- }
-}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/wavelib/RmsHelper.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/wavelib/RmsHelper.java
deleted file mode 100644
index 71d3ec1..0000000
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/wavelib/RmsHelper.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.cts.verifier.audio.wavelib;
-
-public class RmsHelper {
- private double mRmsCurrent;
- public int mBlockSize;
- private int mShoutCount;
- public boolean mRunning = false;
-
- private short[] mAudioShortArray;
-
- private DspBufferDouble mRmsSnapshots;
- private int mShotIndex;
-
- private final float mMinRmsDb;
- private final float mMinRmsVal;
-
- private static final float MAX_VAL = (float)(1 << 15);
-
- public RmsHelper(int blockSize, int shotCount, float minRmsDb, float minRmsVal) {
- mBlockSize = blockSize;
- mShoutCount = shotCount;
- mMinRmsDb = minRmsDb;
- mMinRmsVal = minRmsVal;
-
- reset();
- }
-
- public void reset() {
- mAudioShortArray = new short[mBlockSize];
- mRmsSnapshots = new DspBufferDouble(mShoutCount);
- mShotIndex = 0;
- mRmsCurrent = 0;
- mRunning = false;
- }
-
- public void captureShot() {
- if (mShotIndex >= 0 && mShotIndex < mRmsSnapshots.getSize()) {
- mRmsSnapshots.setValue(mShotIndex++, mRmsCurrent);
- }
- }
-
- public void setRunning(boolean running) {
- mRunning = running;
- }
-
- public double getRmsCurrent() {
- return mRmsCurrent;
- }
-
- public DspBufferDouble getRmsSnapshots() {
- return mRmsSnapshots;
- }
-
- public boolean updateRms(PipeShort pipe, int channelCount, int channel) {
- if (mRunning) {
- int samplesAvailable = pipe.availableToRead();
- while (samplesAvailable >= mBlockSize) {
- pipe.read(mAudioShortArray, 0, mBlockSize);
-
- double rmsTempSum = 0;
- int count = 0;
- for (int i = channel; i < mBlockSize; i += channelCount) {
- float value = mAudioShortArray[i] / MAX_VAL;
-
- rmsTempSum += value * value;
- count++;
- }
- float rms = count > 0 ? (float)Math.sqrt(rmsTempSum / count) : 0f;
- if (rms < mMinRmsVal) {
- rms = mMinRmsVal;
- }
-
- double alpha = 0.9;
- double total_rms = rms * alpha + mRmsCurrent * (1.0f - alpha);
- mRmsCurrent = total_rms;
-
- samplesAvailable = pipe.availableToRead();
- }
- return true;
- }
- return false;
- }
-}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/CameraMuteToggleActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/CameraMuteToggleActivity.java
deleted file mode 100644
index 90fa098..0000000
--- a/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/CameraMuteToggleActivity.java
+++ /dev/null
@@ -1,422 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.cts.verifier.camera.its;
-
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Color;
-import android.graphics.ImageFormat;
-import android.graphics.SurfaceTexture;
-import android.hardware.camera2.CameraAccessException;
-import android.hardware.camera2.CameraCaptureSession;
-import android.hardware.camera2.CameraCharacteristics;
-import android.hardware.camera2.CameraDevice;
-import android.hardware.camera2.CameraManager;
-import android.hardware.camera2.CaptureRequest;
-import android.hardware.camera2.CaptureResult;
-import android.hardware.camera2.TotalCaptureResult;
-import android.hardware.camera2.params.StreamConfigurationMap;
-import android.media.Image;
-import android.media.ImageReader;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.util.Log;
-import android.util.Size;
-import android.view.Surface;
-import android.view.TextureView;
-import android.widget.Button;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.android.compatibility.common.util.ResultType;
-import com.android.compatibility.common.util.ResultUnit;
-import com.android.cts.verifier.CtsVerifierReportLog;
-import com.android.cts.verifier.PassFailButtons;
-import com.android.cts.verifier.R;
-import com.android.ex.camera2.blocking.BlockingCameraManager;
-import com.android.ex.camera2.blocking.BlockingCameraManager.BlockingOpenException;
-import com.android.ex.camera2.blocking.BlockingSessionCallback;
-import com.android.ex.camera2.blocking.BlockingStateCallback;
-
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.List;
-
-/**
- * Test for manual verification of camera privacy hardware switches
- * This test verifies that devices which implement camera hardware
- * privacy toggles enforce sensor privacy when toggles are enabled.
- * - The video stream should be muted:
- * - camera preview & capture should be blank
- * - A dialog or notification should be shown that informs
- * the user that the sensor privacy is enabled.
- */
-public class CameraMuteToggleActivity extends PassFailButtons.Activity
- implements TextureView.SurfaceTextureListener,
- ImageReader.OnImageAvailableListener {
-
- private static final String TAG = "CameraMuteToggleActivity";
- private static final int SESSION_READY_TIMEOUT_MS = 5000;
- private static final int DEFAULT_CAMERA_IDX = 0;
-
- private TextureView mPreviewView;
- private SurfaceTexture mPreviewTexture;
- private Surface mPreviewSurface;
-
- private ImageView mImageView;
-
- private CameraManager mCameraManager;
- private HandlerThread mCameraThread;
- private Handler mCameraHandler;
- private BlockingCameraManager mBlockingCameraManager;
- private CameraCharacteristics mCameraCharacteristics;
- private BlockingStateCallback mCameraListener;
-
- private BlockingSessionCallback mSessionListener;
- private CaptureRequest.Builder mPreviewRequestBuilder;
- private CaptureRequest mPreviewRequest;
- private CaptureRequest.Builder mStillCaptureRequestBuilder;
- private CaptureRequest mStillCaptureRequest;
-
- private CameraCaptureSession mCaptureSession;
- private CameraDevice mCameraDevice;
-
- SizeComparator mSizeComparator = new SizeComparator();
-
- private Size mPreviewSize;
- private Size mJpegSize;
- private ImageReader mJpegImageReader;
-
- private CameraCaptureSession.CaptureCallback mCaptureCallback =
- new CameraCaptureSession.CaptureCallback() {
- };
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.cam_hw_toggle);
-
- setPassFailButtonClickListeners();
-
- mPreviewView = findViewById(R.id.preview_view);
- mImageView = findViewById(R.id.image_view);
-
- mPreviewView.setSurfaceTextureListener(this);
-
- mCameraManager = getSystemService(CameraManager.class);
-
- setInfoResources(R.string.camera_hw_toggle_test, R.string.camera_hw_toggle_test_info, -1);
-
- // Enable Pass button only after taking photo
- setPassButtonEnabled(false);
- setTakePictureButtonEnabled(false);
-
- mBlockingCameraManager = new BlockingCameraManager(mCameraManager);
- mCameraListener = new BlockingStateCallback();
- }
-
- @Override
- public void onResume() {
- super.onResume();
-
- startBackgroundThread();
-
- Exception cameraSetupException = null;
- boolean enablePassButton = false;
- try {
- final String[] camerasList = mCameraManager.getCameraIdList();
- if (camerasList.length > 0) {
- String cameraId = mCameraManager.getCameraIdList()[DEFAULT_CAMERA_IDX];
- setUpCamera(cameraId);
- } else {
- showCameraErrorText("");
- }
- } catch (CameraAccessException e) {
- cameraSetupException = e;
- // Enable Pass button for cameras that do not support mute patterns
- // and will disconnect clients if sensor privacy is enabled
- enablePassButton = (e.getReason() == CameraAccessException.CAMERA_DISABLED);
- } catch (BlockingOpenException e) {
- cameraSetupException = e;
- enablePassButton = e.wasDisconnected();
- } finally {
- if (cameraSetupException != null) {
- cameraSetupException.printStackTrace();
- showCameraErrorText(cameraSetupException.getMessage());
- setPassButtonEnabled(enablePassButton);
- }
- }
- }
-
- private void showCameraErrorText(String errorMsg) {
- TextView instructionsText = findViewById(R.id.instruction_text);
- instructionsText.setText(R.string.camera_hw_toggle_test_no_camera);
- instructionsText.append(errorMsg);
- setTakePictureButtonEnabled(false);
- }
-
- @Override
- public void onPause() {
- shutdownCamera();
- stopBackgroundThread();
-
- super.onPause();
- }
-
- @Override
- public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture,
- int width, int height) {
- mPreviewTexture = surfaceTexture;
-
- mPreviewSurface = new Surface(mPreviewTexture);
-
- if (mCameraDevice != null) {
- startPreview();
- }
- }
-
- @Override
- public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
- // Ignored, Camera does all the work for us
- Log.v(TAG, "onSurfaceTextureSizeChanged: " + width + " x " + height);
- }
-
- @Override
- public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
- mPreviewTexture = null;
- return true;
- }
-
- @Override
- public void onSurfaceTextureUpdated(SurfaceTexture surface) {
- // Invoked every time there's a new Camera preview frame
- }
-
- @Override
- public void onImageAvailable(ImageReader reader) {
- Image img = null;
- try {
- img = reader.acquireNextImage();
- if (img == null) {
- Log.d(TAG, "Invalid image!");
- return;
- }
- final int format = img.getFormat();
-
- Bitmap imgBitmap = null;
- if (format == ImageFormat.JPEG) {
- ByteBuffer jpegBuffer = img.getPlanes()[0].getBuffer();
- jpegBuffer.rewind();
- byte[] jpegData = new byte[jpegBuffer.limit()];
- jpegBuffer.get(jpegData);
- imgBitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length);
- img.close();
- } else {
- Log.i(TAG, "Unsupported image format: " + format);
- }
- if (imgBitmap != null) {
- final Bitmap bitmap = imgBitmap;
- final boolean isMuted = isBitmapMuted(imgBitmap);
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mImageView.setImageBitmap(bitmap);
- // enable pass button if image is muted (black)
- setPassButtonEnabled(isMuted);
- }
- });
- }
- } catch (java.lang.IllegalStateException e) {
- // Swallow exceptions
- e.printStackTrace();
- } finally {
- if (img != null) {
- img.close();
- }
- }
- }
-
- private boolean isBitmapMuted(final Bitmap imgBitmap) {
- // black images may have pixels with values > 0
- // because of JPEG compression artifacts
- final float COLOR_THRESHOLD = 0.02f;
- for (int y = 0; y < imgBitmap.getHeight(); y++) {
- for (int x = 0; x < imgBitmap.getWidth(); x++) {
- Color pixelColor = Color.valueOf(imgBitmap.getPixel(x, y));
- if (pixelColor.red() > COLOR_THRESHOLD || pixelColor.green() > COLOR_THRESHOLD
- || pixelColor.blue() > COLOR_THRESHOLD) {
- return false;
- }
- }
- }
- return true;
- }
-
- private class SizeComparator implements Comparator<Size> {
- @Override
- public int compare(Size lhs, Size rhs) {
- long lha = lhs.getWidth() * lhs.getHeight();
- long rha = rhs.getWidth() * rhs.getHeight();
- if (lha == rha) {
- lha = lhs.getWidth();
- rha = rhs.getWidth();
- }
- return (lha < rha) ? -1 : (lha > rha ? 1 : 0);
- }
- }
-
- private void setUpCamera(String cameraId) throws CameraAccessException, BlockingOpenException {
- shutdownCamera();
-
- mCameraCharacteristics = mCameraManager.getCameraCharacteristics(cameraId);
- mCameraDevice = mBlockingCameraManager.openCamera(cameraId,
- mCameraListener, mCameraHandler);
-
- StreamConfigurationMap config =
- mCameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
- Size[] jpegSizes = config.getOutputSizes(ImageFormat.JPEG);
- Arrays.sort(jpegSizes, mSizeComparator);
- // choose smallest image size, image capture is not the point of this test
- mJpegSize = jpegSizes[0];
-
- mJpegImageReader = ImageReader.newInstance(
- mJpegSize.getWidth(), mJpegSize.getHeight(), ImageFormat.JPEG, 1);
- mJpegImageReader.setOnImageAvailableListener(this, mCameraHandler);
-
- if (mPreviewTexture != null) {
- startPreview();
- }
- }
-
- private void shutdownCamera() {
- if (null != mCaptureSession) {
- mCaptureSession.close();
- mCaptureSession = null;
- }
- if (null != mCameraDevice) {
- mCameraDevice.close();
- mCameraDevice = null;
- }
- if (null != mJpegImageReader) {
- mJpegImageReader.close();
- mJpegImageReader = null;
- }
- }
-
- /**
- * Starts a background thread and its {@link Handler}.
- */
- private void startBackgroundThread() {
- mCameraThread = new HandlerThread("CameraThreadBackground");
- mCameraThread.start();
- mCameraHandler = new Handler(mCameraThread.getLooper());
- }
-
- /**
- * Stops the background thread and its {@link Handler}.
- */
- private void stopBackgroundThread() {
- mCameraThread.quitSafely();
- try {
- mCameraThread.join();
- mCameraThread = null;
- mCameraHandler = null;
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
-
- private Size getPreviewSize(int minWidth) {
- StreamConfigurationMap config =
- mCameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
- Size[] outputSizes = config.getOutputSizes(SurfaceTexture.class);
- Arrays.sort(outputSizes, mSizeComparator);
- // choose smallest image size that's at least minWidth
- // image capture is not the point of this test
- for (Size outputSize : outputSizes) {
- if (outputSize.getWidth() > minWidth) {
- return outputSize;
- }
- }
- return outputSizes[0];
- }
-
- private void startPreview() {
- try {
- mPreviewSize = getPreviewSize(256);
-
- mPreviewTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
- mPreviewRequestBuilder =
- mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
- mPreviewRequestBuilder.addTarget(mPreviewSurface);
-
- mStillCaptureRequestBuilder =
- mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
- mStillCaptureRequestBuilder.addTarget(mPreviewSurface);
- mStillCaptureRequestBuilder.addTarget(mJpegImageReader.getSurface());
-
- mSessionListener = new BlockingSessionCallback();
- List<Surface> outputSurfaces = new ArrayList<Surface>(/*capacity*/3);
- outputSurfaces.add(mPreviewSurface);
- outputSurfaces.add(mJpegImageReader.getSurface());
- mCameraDevice.createCaptureSession(outputSurfaces, mSessionListener, mCameraHandler);
- mCaptureSession = mSessionListener.waitAndGetSession(/*timeoutMs*/3000);
-
- mPreviewRequest = mPreviewRequestBuilder.build();
- mStillCaptureRequest = mStillCaptureRequestBuilder.build();
-
- mCaptureSession.setRepeatingRequest(mPreviewRequest, mCaptureCallback, mCameraHandler);
-
- setTakePictureButtonEnabled(true);
- } catch (CameraAccessException e) {
- e.printStackTrace();
- }
- }
-
- private void takePicture() {
- try {
- mCaptureSession.stopRepeating();
- mSessionListener.getStateWaiter().waitForState(
- BlockingSessionCallback.SESSION_READY, SESSION_READY_TIMEOUT_MS);
-
- mCaptureSession.capture(mStillCaptureRequest, mCaptureCallback, mCameraHandler);
- } catch (CameraAccessException e) {
- e.printStackTrace();
- }
- }
-
- private void setPassButtonEnabled(boolean enabled) {
- ImageButton pass_button = findViewById(R.id.pass_button);
- pass_button.setEnabled(enabled);
- }
-
- private void setTakePictureButtonEnabled(boolean enabled) {
- Button takePhoto = findViewById(R.id.take_picture_button);
- takePhoto.setOnClickListener(v -> takePicture());
- takePhoto.setEnabled(enabled);
- }
-
- @Override
- public void recordTestResults() {
- CtsVerifierReportLog reportLog = getReportLog();
- reportLog.submit();
- }
-}
diff --git a/common/device-side/bedstead/testapp/manifests/RemoteDPCManifest.xml b/common/device-side/bedstead/testapp/manifests/RemoteDPCManifest.xml
index f107562..8682ef6 100644
--- a/common/device-side/bedstead/testapp/manifests/RemoteDPCManifest.xml
+++ b/common/device-side/bedstead/testapp/manifests/RemoteDPCManifest.xml
@@ -34,6 +34,16 @@
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
</intent-filter>
</receiver>
+
+ <!-- This activity is used to show "Work Policy Info" in Settings for Managed Devices -->
+ <activity android:name=".WorkPolicyInfoActivity"
+ android:exported="true"
+ android:launchMode="singleTask">
+ <intent-filter>
+ <category android:name="android.intent.category.DEFAULT"/>
+ <action android:name="android.settings.SHOW_WORK_POLICY_INFO"/>
+ </intent-filter>
+ </activity>
</application>
<uses-sdk android:minSdkVersion="28" android:targetSdkVersion="28"/>
</manifest>
\ No newline at end of file
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/ApiLevelUtil.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/ApiLevelUtil.java
index b2d496c..a022936 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/ApiLevelUtil.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/ApiLevelUtil.java
@@ -31,36 +31,46 @@
private static final int FIRST_API_LEVEL =
SystemProperties.getInt("ro.product.first_api_level", 0);
+ private static void verifyVersion(int version) {
+ if (version == Build.VERSION_CODES.CUR_DEVELOPMENT) {
+ throw new RuntimeException("Invalid version: " + version);
+ }
+ }
+
public static boolean isBefore(int version) {
+ verifyVersion(version);
return Build.VERSION.SDK_INT < version;
}
public static boolean isBefore(String version) {
- return Build.VERSION.SDK_INT < resolveVersionString(version);
+ return isBefore(resolveVersionString(version));
}
public static boolean isAfter(int version) {
+ verifyVersion(version);
return Build.VERSION.SDK_INT > version;
}
public static boolean isAfter(String version) {
- return Build.VERSION.SDK_INT > resolveVersionString(version);
+ return isAfter(resolveVersionString(version));
}
public static boolean isAtLeast(int version) {
+ verifyVersion(version);
return Build.VERSION.SDK_INT >= version;
}
public static boolean isAtLeast(String version) {
- return Build.VERSION.SDK_INT >= resolveVersionString(version);
+ return isAtLeast(resolveVersionString(version));
}
public static boolean isAtMost(int version) {
+ verifyVersion(version);
return Build.VERSION.SDK_INT <= version;
}
public static boolean isAtMost(String version) {
- return Build.VERSION.SDK_INT <= resolveVersionString(version);
+ return isAtMost(resolveVersionString(version));
}
public static int getApiLevel() {
@@ -68,19 +78,21 @@
}
public static boolean isFirstApiBefore(int version) {
+ verifyVersion(version);
return FIRST_API_LEVEL < version;
}
public static boolean isFirstApiBefore(String version) {
- return FIRST_API_LEVEL < resolveVersionString(version);
+ return isFirstApiBefore(resolveVersionString(version));
}
public static boolean isFirstApiAfter(int version) {
+ verifyVersion(version);
return FIRST_API_LEVEL > version;
}
public static boolean isFirstApiAfter(String version) {
- return FIRST_API_LEVEL > resolveVersionString(version);
+ return isFirstApiAfter(resolveVersionString(version));
}
public static boolean isFirstApiAtLeast(int version) {
@@ -88,7 +100,7 @@
}
public static boolean isFirstApiAtLeast(String version) {
- return FIRST_API_LEVEL >= resolveVersionString(version);
+ return isFirstApiAtLeast(resolveVersionString(version));
}
public static boolean isFirstApiAtMost(int version) {
@@ -96,7 +108,7 @@
}
public static boolean isFirstApiAtMost(String version) {
- return FIRST_API_LEVEL <= resolveVersionString(version);
+ return isFirstApiAtMost(resolveVersionString(version));
}
public static int getFirstApiLevel() {
diff --git a/hostsidetests/appcompat/strictjavapackages/src/android/compat/sjp/cts/StrictJavaPackagesTest.java b/hostsidetests/appcompat/strictjavapackages/src/android/compat/sjp/cts/StrictJavaPackagesTest.java
index aea1248..7a1dcbc 100644
--- a/hostsidetests/appcompat/strictjavapackages/src/android/compat/sjp/cts/StrictJavaPackagesTest.java
+++ b/hostsidetests/appcompat/strictjavapackages/src/android/compat/sjp/cts/StrictJavaPackagesTest.java
@@ -30,12 +30,14 @@
import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
import com.android.modules.utils.build.testing.DeviceSdkLevel;
import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.INativeDevice;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
import com.android.tradefed.testtype.junit4.DeviceTestRunOptions;
+import com.android.tradefed.util.FileUtil;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableCollection;
@@ -52,10 +54,13 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -80,6 +85,7 @@
private static ImmutableList<SharedLibraryInfo> sSharedLibs;
private static ImmutableMultimap<String, String> sSharedLibsPathsToName;
private static ImmutableMultimap<String, String> sJarsToClasses;
+ private static ImmutableMultimap<String, String> sJarsToFiles;
private DeviceSdkLevel mDeviceSdkLevel;
@@ -201,6 +207,7 @@
"Landroid/os/IVoldListener;",
"Landroid/os/IVoldMountCallback;",
"Landroid/os/IVoldTaskListener;",
+ "Landroid/os/TouchOcclusionMode;",
"Landroid/os/storage/CrateMetadata;",
"Landroid/view/LayerMetadataKey;",
"Lcom/android/internal/annotations/CompositeRWLock;",
@@ -223,8 +230,16 @@
"Landroid/os/BlockUntrustedTouchesMode;",
"Landroid/os/IInputConstants;",
"Landroid/os/InputEventInjectionResult;",
- "Landroid/os/InputEventInjectionSync;"
-
+ "Landroid/os/InputEventInjectionSync;",
+ // TODO(b/242741880): Remove duplication between sdksandbox-service and
+ // sdk-sandbox-framework
+ "Landroid/app/sdksandbox/ILoadSdkCallback;",
+ "Landroid/app/sdksandbox/IRequestSurfacePackageCallback;",
+ "Landroid/app/sdksandbox/ISdkSandboxManager;",
+ "Landroid/app/sdksandbox/ISdkSandboxProcessDeathCallback;",
+ "Landroid/app/sdksandbox/ISendDataCallback;",
+ "Landroid/app/sdksandbox/ISharedPreferencesSyncCallback;",
+ "Landroid/app/sdksandbox/ISdkToServiceCallback;"
);
private static final String FEATURE_WEARABLE = "android.hardware.type.watch";
@@ -718,9 +733,14 @@
private static final ImmutableSet<String> ADSERVICES_SANDBOX_APK_IN_APEX_BURNDOWN_LIST =
ImmutableSet.of(
// /apex/com.android.adservices/javalib/service-sdksandbox.jar
+ "Landroid/app/sdksandbox/ISharedPreferencesSyncCallback;",
+ "Lcom/android/sdksandbox/IDataReceivedCallback;",
+ "Lcom/android/sdksandbox/ILoadSdkInSandboxCallback;",
+ "Lcom/android/sdksandbox/IRequestSurfacePackageFromSdkCallback;",
"Lcom/android/sdksandbox/ISdkSandboxManagerToSdkSandboxCallback;",
"Lcom/android/sdksandbox/ISdkSandboxService;",
- "Lcom/android/sdksandbox/ISdkSandboxToSdkSandboxManagerCallback;"
+ "Lcom/android/sdksandbox/SandboxLatencyInfo-IA;",
+ "Lcom/android/sdksandbox/SandboxLatencyInfo;"
);
private static final ImmutableMap<String, ImmutableSet<String>> FULL_APK_IN_APEX_BURNDOWN =
@@ -798,6 +818,8 @@
});
sSharedLibsPathsToName = sharedLibsPathsToName.build();
+ final ImmutableSetMultimap.Builder<String, String> jarsToFiles =
+ ImmutableSetMultimap.builder();
final ImmutableSetMultimap.Builder<String, String> jarsToClasses =
ImmutableSetMultimap.builder();
Stream.of(sBootclasspathJars.stream(),
@@ -805,21 +827,32 @@
sSharedLibJars.stream())
.reduce(Stream::concat).orElseGet(Stream::empty)
.parallel()
- .forEach(jar -> {
+ .forEach(jarPath -> {
+ File jar = null;
try {
+ jar = pullJarFromDevice(testInfo.getDevice(), jarPath);
+
+ ImmutableSet<String> files = getJarFileContents(jar);
+ synchronized (jarsToFiles) {
+ jarsToFiles.putAll(jarPath, files);
+ }
+
ImmutableSet<String> classes =
- Classpaths.getClassDefsFromJar(testInfo.getDevice(), jar).stream()
+ Classpaths.getClassDefsFromJar(jar).stream()
.map(ClassDef::getType)
// Inner classes always go with their parent.
.filter(className -> !className.contains("$"))
.collect(ImmutableSet.toImmutableSet());
synchronized (jarsToClasses) {
- jarsToClasses.putAll(jar, classes);
+ jarsToClasses.putAll(jarPath, classes);
}
} catch (DeviceNotAvailableException | IOException e) {
throw new RuntimeException(e);
+ } finally {
+ FileUtil.deleteFile(jar);
}
});
+ sJarsToFiles = jarsToFiles.build();
sJarsToClasses = jarsToClasses.build();
}
@@ -983,9 +1016,11 @@
Arrays.stream(collectApkInApexPaths())
.parallel()
.forEach(apk -> {
+ File apkFile = null;
try {
+ apkFile = pullJarFromDevice(getDevice(), apk);
final ImmutableSet<String> apkClasses =
- Classpaths.getClassDefsFromJar(getDevice(), apk).stream()
+ Classpaths.getClassDefsFromJar(apkFile).stream()
.map(ClassDef::getType)
.collect(ImmutableSet.toImmutableSet());
// b/226559955: The directory paths containing APKs contain the build ID,
@@ -1009,6 +1044,8 @@
}
} catch (Exception e) {
throw new RuntimeException(e);
+ } finally {
+ FileUtil.deleteFile(apkFile);
}
});
assertThat(perApkClasspathDuplicates).isEmpty();
@@ -1033,6 +1070,8 @@
.put("androidx.window.sidecar",
ImmutableSet.of("Landroidx/window/common/", "Landroidx/window/sidecar",
"Landroidx/window/util"))
+ .put("com.google.android.camera.experimental2019",
+ ImmutableSet.of("Landroidx/annotation"))
.put("com.google.android.camera.experimental2020_midyear",
ImmutableSet.of("Landroidx/annotation"))
.build();
@@ -1047,6 +1086,71 @@
).isEmpty();
}
+ /**
+ * Ensure that there are no kotlin files in BOOTCLASSPATH, SYSTEMSERVERCLASSPATH
+ * and shared library jars.
+ */
+ @Test
+ public void testNoKotlinFilesInClasspaths() throws Exception {
+ assumeTrue(mDeviceSdkLevel.isDeviceAtLeastT());
+ ImmutableList<String> kotlinFiles =
+ Stream.of(sBootclasspathJars.stream(),
+ sSystemserverclasspathJars.stream(),
+ sSharedLibJars.stream())
+ .reduce(Stream::concat).orElseGet(Stream::empty)
+ .parallel()
+ .filter(jarPath -> {
+ return sJarsToFiles
+ .get(jarPath)
+ .stream()
+ .anyMatch(file -> file.contains(".kotlin_builtins")
+ || file.contains(".kotlin_module"));
+ })
+ .collect(ImmutableList.toImmutableList());
+ assertThat(kotlinFiles).isEmpty();
+ }
+
+ /**
+ * Ensure that all classes from protobuf libraries are jarjared before
+ * included in BOOTCLASSPATH, SYSTEMSERVERCLASSPATH and shared library jars
+ */
+ @Test
+ public void testNoProtobufClassesWithoutJarjar() throws Exception {
+ assumeTrue(mDeviceSdkLevel.isDeviceAtLeastU());
+ assertWithMessage("Classes from protobuf libraries must not be included in bootclasspath "
+ + "and systemserverclasspath without being jarjared.")
+ .that(Stream.of(sBootclasspathJars.stream(),
+ sSystemserverclasspathJars.stream(),
+ sSharedLibJars.stream())
+ .reduce(Stream::concat).orElseGet(Stream::empty)
+ .parallel()
+ .filter(jarPath -> {
+ return sJarsToClasses
+ .get(jarPath)
+ .stream()
+ .anyMatch(cls -> cls.startsWith("Lcom/google/protobuf/"));
+ })
+ .collect(ImmutableList.toImmutableList())
+ ).isEmpty();
+ }
+
+ private static File pullJarFromDevice(INativeDevice device,
+ String remoteJarPath) throws DeviceNotAvailableException {
+ File jar = device.pullFile(remoteJarPath);
+ if (jar == null) {
+ throw new IllegalStateException("could not pull remote file " + remoteJarPath);
+ }
+ return jar;
+ }
+
+ private static ImmutableSet<String> getJarFileContents(File jar) throws IOException {
+ try (JarFile jarFile = new JarFile(jar)) {
+ return jarFile.stream()
+ .map(JarEntry::getName)
+ .collect(ImmutableSet.toImmutableSet());
+ }
+ }
+
private boolean isLegacyAndroidxDependency(
ImmutableMap<String, ImmutableSet<String>> legacyExemptAndroidxSharedLibsNamesToClasses,
String path, String className) {
diff --git a/hostsidetests/appsecurity/OWNERS b/hostsidetests/appsecurity/OWNERS
index d027e21..37c15ac 100644
--- a/hostsidetests/appsecurity/OWNERS
+++ b/hostsidetests/appsecurity/OWNERS
@@ -14,6 +14,17 @@
# Bug component: 36137 = per-file SharedUserIdTest.java
# Bug component: 36137 = per-file SplitTests.java
+# Storage bug component
+# Bug component: 46626 = per-file *Adoptable*
+# Bug component: 46626 = per-file *DirectBoot*
+# Bug component: 46626 = per-file *Storage*
+# Bug component: 46626 = per-file *Documents*
+# Bug component: 46626 = per-file ScopedDirectoryAccessTest.java
+
+# DocsUI bug component
+# Bug component: 46626 = per-file *Documents*
+# Bug component: 46626 = per-file ScopedDirectoryAccessTest.java
+
patb@google.com
per-file AccessSerialNumberTest.java = ewol@google.com
per-file ApexSignatureVerificationTest.java = dariofreni@google.com
diff --git a/hostsidetests/scopedstorage/Android.bp b/hostsidetests/scopedstorage/Android.bp
index 527e8ef..0e2a887 100644
--- a/hostsidetests/scopedstorage/Android.bp
+++ b/hostsidetests/scopedstorage/Android.bp
@@ -91,7 +91,7 @@
// Tag as a CTS artifact
test_suites: [
"general-tests",
- "mts",
+ "mts-mediaprovider",
"cts",
],
}
@@ -155,7 +155,7 @@
// Tag as a CTS artifact
test_suites: [
"general-tests",
- "mts",
+ "mts-mediaprovider",
"cts",
],
}
@@ -171,7 +171,7 @@
// Tag as a CTS artifact
test_suites: [
"general-tests",
- "mts",
+ "mts-mediaprovider",
"cts",
],
}
@@ -187,7 +187,7 @@
// Tag as a CTS artifact
test_suites: [
"general-tests",
- "mts",
+ "mts-mediaprovider",
"cts",
],
}
diff --git a/hostsidetests/scopedstorage/ScopedStorageTestHelper/src/android/scopedstorage/cts/ScopedStorageTestHelper.java b/hostsidetests/scopedstorage/ScopedStorageTestHelper/src/android/scopedstorage/cts/ScopedStorageTestHelper.java
index 717cf95..6fbf5f3 100644
--- a/hostsidetests/scopedstorage/ScopedStorageTestHelper/src/android/scopedstorage/cts/ScopedStorageTestHelper.java
+++ b/hostsidetests/scopedstorage/ScopedStorageTestHelper/src/android/scopedstorage/cts/ScopedStorageTestHelper.java
@@ -16,7 +16,7 @@
package android.scopedstorage.cts;
import static android.scopedstorage.cts.lib.RedactionTestHelper.EXIF_METADATA_QUERY;
-import static android.scopedstorage.cts.lib.RedactionTestHelper.getExifMetadata;
+import static android.scopedstorage.cts.lib.RedactionTestHelper.getExifMetadataFromFile;
import static android.scopedstorage.cts.lib.TestUtils.CAN_OPEN_FILE_FOR_READ_QUERY;
import static android.scopedstorage.cts.lib.TestUtils.CAN_OPEN_FILE_FOR_WRITE_QUERY;
import static android.scopedstorage.cts.lib.TestUtils.CAN_READ_WRITE_QUERY;
@@ -253,7 +253,7 @@
if (getIntent().hasExtra(INTENT_EXTRA_PATH)) {
final String filePath = getIntent().getStringExtra(INTENT_EXTRA_PATH);
if (EXIF_METADATA_QUERY.equals(queryType)) {
- intent.putExtra(queryType, getExifMetadata(new File(filePath)));
+ intent.putExtra(queryType, getExifMetadataFromFile(new File(filePath)));
}
} else {
throw new IllegalStateException(
diff --git a/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageDeviceTest.java b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageDeviceTest.java
index 6a65c5b..f5c6af5 100644
--- a/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageDeviceTest.java
+++ b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageDeviceTest.java
@@ -21,7 +21,7 @@
import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
import static android.scopedstorage.cts.lib.RedactionTestHelper.assertExifMetadataMatch;
import static android.scopedstorage.cts.lib.RedactionTestHelper.assertExifMetadataMismatch;
-import static android.scopedstorage.cts.lib.RedactionTestHelper.getExifMetadata;
+import static android.scopedstorage.cts.lib.RedactionTestHelper.getExifMetadataFromFile;
import static android.scopedstorage.cts.lib.RedactionTestHelper.getExifMetadataFromRawResource;
import static android.scopedstorage.cts.lib.TestUtils.BYTES_DATA2;
import static android.scopedstorage.cts.lib.TestUtils.STR_DATA2;
@@ -79,6 +79,7 @@
import static android.scopedstorage.cts.lib.TestUtils.isAppInstalled;
import static android.scopedstorage.cts.lib.TestUtils.listAs;
import static android.scopedstorage.cts.lib.TestUtils.openWithMediaProvider;
+import static android.scopedstorage.cts.lib.TestUtils.queryAudioFile;
import static android.scopedstorage.cts.lib.TestUtils.queryFile;
import static android.scopedstorage.cts.lib.TestUtils.queryFileExcludingPending;
import static android.scopedstorage.cts.lib.TestUtils.queryImageFile;
@@ -134,6 +135,7 @@
import android.os.storage.StorageManager;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
+import android.scopedstorage.cts.lib.RedactionTestHelper;
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructStat;
@@ -875,7 +877,7 @@
// EXIF tags and might misleadingly think there are not tags to redact
out.getFD().sync();
- HashMap<String, String> exif = getExifMetadata(jpgFile);
+ HashMap<String, String> exif = getExifMetadataFromFile(jpgFile);
assertExifMetadataMatch(exif, originalExif);
HashMap<String, String> exifFromTestApp =
@@ -1131,6 +1133,11 @@
}
@Test
+ // There is a minor bug which, alghough fixed in sc-dev (aosp/1834457),
+ // cannot be propagated to the already released sc-release branche
+ // (b/234145920), where mainline-modules are tested.
+ // Skip this test in S to avoid failures in outdated targets.
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void testAppendUpdatesMtime() throws Exception {
writeAndCheckMtime(true);
}
@@ -1319,7 +1326,7 @@
// Sync file to disk to ensure file is fully written to the lower fs.
out.getFD().sync();
}
- HashMap<String, String> exif = getExifMetadata(imgFile);
+ HashMap<String, String> exif = getExifMetadataFromFile(imgFile);
assertExifMetadataMatch(exif, originalExif);
// Install test app
@@ -1514,6 +1521,10 @@
// Assert we can read from the file
assertFileContent(otherAppImageFile, BYTES_DATA1);
+ // Assert has access to redacted information
+ RedactionTestHelper.assertConsistentNonRedactedAccess(otherAppImageFile,
+ R.raw.img_with_metadata);
+
// Assert we can delete the file
assertThat(otherAppImageFile.delete()).isTrue();
assertThat(otherAppImageFile.exists()).isFalse();
@@ -1767,6 +1778,37 @@
}
/**
+ * Test that renaming file paths to an external directory such as Android/* and Android/* /*
+ * except Android/media/* /* is not allowed.
+ */
+ @Test
+ public void testRenameFileToAppSpecificDir() throws Exception {
+ final File testFile = new File(getExternalMediaDir(), IMAGE_FILE_NAME);
+ final File testFileNew = new File(getExternalMediaDir(), NONMEDIA_FILE_NAME);
+
+ try {
+ // Create a file in app's external media directory
+ if (!testFile.exists()) {
+ assertThat(testFile.createNewFile()).isTrue();
+ }
+
+ final String androidDirPath = getExternalStorageDir().getPath() + "/Android";
+
+ // Verify that we can't rename a file to Android/ or Android/data or
+ // Android/media directory
+ assertCantRenameFile(testFile, new File(androidDirPath, IMAGE_FILE_NAME));
+ assertCantRenameFile(testFile, new File(androidDirPath + "/data", IMAGE_FILE_NAME));
+ assertCantRenameFile(testFile, new File(androidDirPath + "/media", IMAGE_FILE_NAME));
+
+ // Verify that we can rename a file to app specific media directory.
+ assertCanRenameFile(testFile, testFileNew);
+ } finally {
+ testFile.delete();
+ testFileNew.delete();
+ }
+ }
+
+ /**
* Test that renaming directories is allowed and aligns to default directory restrictions.
*/
@Test
@@ -2987,6 +3029,45 @@
}
}
+ /**
+ * Test that renaming a file to {@link Environment#DIRECTORY_RINGTONES} sets
+ * {@link MediaStore.Audio.AudioColumns#IS_RINGTONE}
+ */
+
+ @Test
+ public void testRenameToRingtoneDirectory() throws Exception {
+ final File fileInDownloads = new File(getDownloadDir(), AUDIO_FILE_NAME);
+ final File fileInRingtones = new File(getRingtonesDir(), AUDIO_FILE_NAME);
+
+ try {
+ assertThat(fileInDownloads.createNewFile()).isTrue();
+ assertThat(MediaStore.scanFile(getContentResolver(), fileInDownloads)).isNotNull();
+
+ assertCanRenameFile(fileInDownloads, fileInRingtones);
+
+ try (Cursor c = queryAudioFile(fileInRingtones,
+ MediaStore.Audio.AudioColumns.IS_RINGTONE)) {
+ assertTrue(c.moveToFirst());
+ assertWithMessage("Expected " + MediaStore.Audio.AudioColumns.IS_RINGTONE
+ + " to be set after renaming to " + fileInRingtones)
+ .that(c.getInt(0)).isEqualTo(1);
+ }
+
+ assertCanRenameFile(fileInRingtones, fileInDownloads);
+
+ try (Cursor c = queryAudioFile(fileInDownloads,
+ MediaStore.Audio.AudioColumns.IS_RINGTONE)) {
+ assertTrue(c.moveToFirst());
+ assertWithMessage("Expected " + MediaStore.Audio.AudioColumns.IS_RINGTONE
+ + " to be unset after renaming to " + fileInDownloads)
+ .that(c.getInt(0)).isEqualTo(0);
+ }
+ } finally {
+ fileInDownloads.delete();
+ fileInRingtones.delete();
+ }
+ }
+
@Test
@SdkSuppress(minSdkVersion = 31, codeName = "S")
public void testTransformsDirFileOperations() throws Exception {
diff --git a/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/StableUrisTest.java b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/StableUrisTest.java
index 41a47b9..4bff21b 100644
--- a/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/StableUrisTest.java
+++ b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/StableUrisTest.java
@@ -33,6 +33,8 @@
import android.app.Instrumentation;
import android.content.ContentResolver;
import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
import android.provider.MediaStore;
import android.scopedstorage.cts.lib.TestUtils;
import android.util.Log;
@@ -121,7 +123,7 @@
Log.d(TAG, "maxRowIdOfExternalDbBeforeReset:" + maxRowIdOfExternalDbBeforeReset);
// Clear MediaProvider package data to trigger DB recreation.
- mDevice.executeShellCommand("pm clear com.google.android.providers.media.module");
+ mDevice.executeShellCommand("pm clear " + getMediaProviderPackageName());
waitForMountedAndIdleState(mContentResolver);
MediaStore.scanVolume(mContentResolver, mVolumeName);
@@ -172,4 +174,11 @@
return files;
}
+ private static String getMediaProviderPackageName() {
+ final Instrumentation inst = androidx.test.InstrumentationRegistry.getInstrumentation();
+ final PackageManager packageManager = inst.getContext().getPackageManager();
+ final ProviderInfo providerInfo = packageManager.resolveContentProvider(
+ MediaStore.AUTHORITY, PackageManager.MATCH_ALL);
+ return providerInfo.packageName;
+ }
}
diff --git a/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/ScopedStorageHostTest.java b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/ScopedStorageHostTest.java
index cd9378d..c7887f8 100644
--- a/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/ScopedStorageHostTest.java
+++ b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/ScopedStorageHostTest.java
@@ -95,6 +95,16 @@
}
@Test
+ public void testManageExternalStorageCanReadRedactedContents() throws Exception {
+ allowAppOps("android:manage_external_storage");
+ try {
+ runDeviceTest("testManageExternalStorageCanReadRedactedContents");
+ } finally {
+ denyAppOps("android:manage_external_storage");
+ }
+ }
+
+ @Test
public void testManageExternalStorageCanRenameOtherAppsContents() throws Exception {
allowAppOps("android:manage_external_storage");
try {
diff --git a/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/RedactionTestHelper.java b/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/RedactionTestHelper.java
index b7509e6..ee0d203 100644
--- a/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/RedactionTestHelper.java
+++ b/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/RedactionTestHelper.java
@@ -21,11 +21,15 @@
import static org.junit.Assert.fail;
import android.media.ExifInterface;
+import android.net.Uri;
+import android.os.FileUtils;
+import android.provider.MediaStore;
import androidx.annotation.NonNull;
import androidx.annotation.RawRes;
import java.io.File;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
@@ -56,12 +60,26 @@
* Retrieve the EXIF metadata from the given file.
*/
@NonNull
- public static HashMap<String, String> getExifMetadata(@NonNull File file) throws IOException {
+ public static HashMap<String, String> getExifMetadataFromFile(@NonNull File file)
+ throws IOException {
final ExifInterface exif = new ExifInterface(file);
return dumpExifGpsTagsToMap(exif);
}
/**
+ * Retrieve the EXIF metadata from the given uri.
+ */
+ @NonNull
+ private static HashMap<String, String> getExifMetadataFromUri(@NonNull Uri uri)
+ throws IOException {
+ try (InputStream is = getContext().getContentResolver().openInputStream(uri)) {
+ final ExifInterface exif = new ExifInterface(is);
+ return dumpExifGpsTagsToMap(exif);
+ }
+ }
+
+
+ /**
* Retrieve the EXIF metadata from the given resource.
*/
@NonNull
@@ -116,4 +134,26 @@
}
return res;
}
+
+ public static void assertConsistentNonRedactedAccess(File file, int metadataResId)
+ throws Exception {
+ // Write some meta-data to the file to assert on redacted information access
+ try (InputStream in =
+ getContext().getResources().openRawResource(metadataResId);
+ FileOutputStream out = new FileOutputStream(file)) {
+ FileUtils.copy(in, out);
+ out.getFD().sync();
+ }
+
+ HashMap<String, String> originalExif = getExifMetadataFromRawResource(metadataResId);
+
+ // Using File API
+ HashMap<String, String> exifFromFilePath = getExifMetadataFromFile(file);
+ assertExifMetadataMatch(exifFromFilePath, originalExif);
+
+ Uri uri = MediaStore.scanFile(getContext().getContentResolver(), file);
+ // Using ContentResolver API
+ HashMap<String, String> exifFromContentResolver = getExifMetadataFromUri(uri);
+ assertExifMetadataMatch(exifFromContentResolver, originalExif);
+ }
}
diff --git a/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/TestUtils.java b/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/TestUtils.java
index 0670639..2e694ed 100644
--- a/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/TestUtils.java
+++ b/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/TestUtils.java
@@ -751,6 +751,17 @@
}
/**
+ * Queries {@link ContentResolver} for an audio file and returns a {@link Cursor} with the given
+ * columns.
+ */
+ @NonNull
+ public static Cursor queryAudioFile(File file, String... projection) {
+ return queryFile(getContentResolver(),
+ MediaStore.Audio.Media.getContentUri(sStorageVolumeName), file,
+ /*includePending*/ true, projection);
+ }
+
+ /**
* Queries {@link ContentResolver} for a file and returns the corresponding mime type for its
* entry in the database.
*/
diff --git a/hostsidetests/scopedstorage/src/android/scopedstorage/cts/ScopedStorageTest.java b/hostsidetests/scopedstorage/src/android/scopedstorage/cts/ScopedStorageTest.java
index 6bcaa5a..1bb701b 100644
--- a/hostsidetests/scopedstorage/src/android/scopedstorage/cts/ScopedStorageTest.java
+++ b/hostsidetests/scopedstorage/src/android/scopedstorage/cts/ScopedStorageTest.java
@@ -92,6 +92,7 @@
import android.os.storage.StorageManager;
import android.platform.test.annotations.AppModeInstant;
import android.provider.MediaStore;
+import android.scopedstorage.cts.lib.RedactionTestHelper;
import android.system.ErrnoException;
import android.system.Os;
import android.util.Log;
@@ -224,6 +225,25 @@
}
@Test
+ public void testManageExternalStorageCanReadRedactedContents() throws Exception {
+ pollForManageExternalStorageAllowed();
+
+ final File otherAppImage = new File(getDcimDir(), "other" + IMAGE_FILE_NAME);
+
+ try {
+ // Create file as another app
+ assertThat(createFileAs(APP_B_NO_PERMS, otherAppImage.getPath())).isTrue();
+
+ // Assert has access to redacted information
+ RedactionTestHelper.assertConsistentNonRedactedAccess(otherAppImage,
+ R.raw.img_with_metadata);
+
+ } finally {
+ deleteFileAsNoThrow(APP_B_NO_PERMS, otherAppImage.getAbsolutePath());
+ }
+ }
+
+ @Test
public void testManageExternalStorageCantReadWriteOtherAppExternalDir() throws Exception {
pollForManageExternalStorageAllowed();
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/lib/ConfigUtils.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/lib/ConfigUtils.java
index be665ba..7f29d44 100644
--- a/hostsidetests/statsdatom/src/android/cts/statsdatom/lib/ConfigUtils.java
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/lib/ConfigUtils.java
@@ -69,10 +69,6 @@
.addAllowedLogSource("AID_SYSTEM")
.addAllowedLogSource("AID_BLUETOOTH")
.addAllowedLogSource("com.android.bluetooth")
- // TODO(b/236681553): Remove this.
- .addAllowedLogSource("com.android.bluetooth.services")
- // TODO(b/236681553): Remove this.
- .addAllowedLogSource("com.google.android.bluetooth.services")
.addAllowedLogSource("AID_LMKD")
.addAllowedLogSource("AID_MEDIA")
.addAllowedLogSource("AID_RADIO")
diff --git a/tests/PhotoPicker/TEST_MAPPING b/tests/PhotoPicker/TEST_MAPPING
index f48e90c..2a55a28 100644
--- a/tests/PhotoPicker/TEST_MAPPING
+++ b/tests/PhotoPicker/TEST_MAPPING
@@ -1,6 +1,26 @@
{
+ "mainline-presubmit": [
+ {
+ "name": "CtsPhotoPickerTest[com.google.android.mediaprovider.apex]",
+ "options": [
+ {
+ "exclude-annotation": "androidx.test.filters.LargeTest"
+ }
+ ]
+ }
+ ],
"presubmit": [
{
+ "name": "CtsPhotoPickerTest",
+ "options": [
+ {
+ "exclude-annotation": "androidx.test.filters.LargeTest"
+ }
+ ]
+ }
+ ],
+ "postsubmit": [
+ {
"name": "CtsPhotoPickerTest"
}
]
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/ActionGetContentOnlyTest.java b/tests/PhotoPicker/src/android/photopicker/cts/ActionGetContentOnlyTest.java
new file mode 100644
index 0000000..c7824bb
--- /dev/null
+++ b/tests/PhotoPicker/src/android/photopicker/cts/ActionGetContentOnlyTest.java
@@ -0,0 +1,318 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.photopicker.cts;
+
+import static android.photopicker.cts.util.GetContentActivityAliasUtils.clearPackageData;
+import static android.photopicker.cts.util.GetContentActivityAliasUtils.getDocumentsUiPackageName;
+import static android.photopicker.cts.util.PhotoPickerAssertionsUtils.assertReadOnlyAccess;
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImagesAndGetUriAndPath;
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.deleteMedia;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.SHORT_TIMEOUT;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.clickAndWait;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.findAndClickBrowse;
+
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.content.ClipData;
+import android.content.Intent;
+import android.net.Uri;
+import android.photopicker.cts.util.GetContentActivityAliasUtils;
+import android.photopicker.cts.util.PhotoPickerUiUtils;
+import android.util.Pair;
+
+import androidx.test.uiautomator.UiObject;
+import androidx.test.uiautomator.UiObjectNotFoundException;
+import androidx.test.uiautomator.UiScrollable;
+import androidx.test.uiautomator.UiSelector;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Photo Picker tests for PhotoPicker launched via {@link Intent#ACTION_GET_CONTENT} intent
+ * exclusively.
+ */
+public class ActionGetContentOnlyTest extends PhotoPickerBaseTest {
+
+ public static final String TAG = "ActionGetContentOnlyTest";
+
+ private static String sDocumentsUiPackageName;
+ private static int sGetContentTakeOverActivityAliasState;
+
+ private List<Uri> mUriList = new ArrayList<>();
+
+ @After
+ public void tearDown() throws Exception {
+ for (Uri uri : mUriList) {
+ deleteMedia(uri, mContext);
+ }
+ mUriList.clear();
+
+ if (mActivity != null) {
+ mActivity.finish();
+ }
+ }
+
+ @BeforeClass
+ public static void setUpBeforeClass() throws Exception {
+ sDocumentsUiPackageName = getDocumentsUiPackageName();
+ sGetContentTakeOverActivityAliasState = GetContentActivityAliasUtils.enableAndGetOldState();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ clearPackageData(sDocumentsUiPackageName);
+ }
+
+ @AfterClass
+ public static void tearDownAfterClass() throws Exception {
+ GetContentActivityAliasUtils.restoreState(sGetContentTakeOverActivityAliasState);
+ }
+
+ @Test
+ public void testMimeTypeFilter() throws Exception {
+ final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setType("audio/*");
+ mActivity.startActivityForResult(intent, REQUEST_CODE);
+ mDevice.waitForIdle();
+ // Should open documentsUi
+ assertThatShowsDocumentsUiButtons();
+
+ // We don't test the result of the picker here because the intention of the test is only to
+ // test that DocumentsUi is opened.
+ }
+
+ @Test
+ public void testExtraMimeTypeFilter() throws Exception {
+ final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setType("image/*");
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"video/*", "audio/*"});
+ mActivity.startActivityForResult(intent, REQUEST_CODE);
+ mDevice.waitForIdle();
+ // Should open documentsUi
+ assertThatShowsDocumentsUiButtons();
+
+ // We don't test the result of the picker here because the intention of the test is only to
+ // test that DocumentsUi is opened.
+ }
+
+ @Test
+ public void testBrowse_singleSelect() throws Exception {
+ final int itemCount = 1;
+ List<Pair<Uri, String>> createdImagesData = createImagesAndGetUriAndPath(itemCount,
+ mContext.getUserId(), /* isFavorite */ false);
+
+ final List<String> fileNameList = new ArrayList<>();
+ for (Pair<Uri, String> createdImageData: createdImagesData) {
+ mUriList.add(createdImageData.first);
+ fileNameList.add(createdImageData.second);
+ }
+
+ final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType("image/*");
+ mActivity.startActivityForResult(intent, REQUEST_CODE);
+
+ findAndClickBrowse(mDevice);
+
+ findAndClickFilesInDocumentsUi(fileNameList);
+
+ final Uri uri = mActivity.getResult().data.getData();
+
+ assertReadOnlyAccess(uri, mContext.getContentResolver());
+ }
+
+ @Test
+ public void testBrowse_multiSelect() throws Exception {
+ final int itemCount = 3;
+ List<Pair<Uri, String>> createdImagesData = createImagesAndGetUriAndPath(itemCount,
+ mContext.getUserId(), /* isFavorite */ false);
+
+ final List<String> fileNameList = new ArrayList<>();
+ for (Pair<Uri, String> createdImageData: createdImagesData) {
+ mUriList.add(createdImageData.first);
+ fileNameList.add(createdImageData.second);
+ }
+
+ final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
+ intent.setType("image/*");
+ mActivity.startActivityForResult(intent, REQUEST_CODE);
+
+ findAndClickBrowse(mDevice);
+
+ findAndClickFilesInDocumentsUi(fileNameList);
+
+ final ClipData clipData = mActivity.getResult().data.getClipData();
+ final int count = clipData.getItemCount();
+ assertThat(count).isEqualTo(itemCount);
+ for (int i = 0; i < count; i++) {
+ assertReadOnlyAccess(clipData.getItemAt(i).getUri(), mContext.getContentResolver());
+ }
+ }
+
+ @Test
+ public void testChooserIntent_mediaFilter() throws Exception {
+ final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType("image/*");
+ mActivity.startActivityForResult(Intent.createChooser(intent, TAG), REQUEST_CODE);
+
+ // Should open Picker
+ assertThatShowsPickerUi();
+ }
+
+ @Test
+ public void testChooserIntent_nonMediaFilter() throws Exception {
+ final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType("*/*");
+ mActivity.startActivityForResult(Intent.createChooser(intent, TAG), REQUEST_CODE);
+
+ // Should open DocumentsUi
+ assertThatShowsDocumentsUiButtons();
+ }
+
+ @Test
+ public void testPickerSupportedFromDocumentsUi() throws Exception {
+ final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType("*/*");
+ mActivity.startActivityForResult(Intent.createChooser(intent, TAG), REQUEST_CODE);
+
+ findAndClickMediaIcon();
+
+ // Should open Picker
+ assertThatShowsPickerUi();
+ }
+
+ private void findAndClickMediaIcon() throws Exception {
+ final UiSelector appList = new UiSelector().resourceId(sDocumentsUiPackageName
+ + ":id/apps_row");
+
+ // Wait for the first app list item to appear
+ assertWithMessage("Waiting for app list to appear in DocumentsUi").that(
+ new UiObject(appList).waitForExists(SHORT_TIMEOUT)).isTrue();
+
+ String photoPickerAppName = "Media";
+ UiObject mediaButton = mDevice.findObject(new UiSelector().text(photoPickerAppName));
+
+ assertWithMessage("Timed out waiting for " + photoPickerAppName + " app icon to appear")
+ .that(new UiScrollable(appList).scrollIntoView(mediaButton)).isTrue();
+ mDevice.waitForIdle();
+
+ clickAndWait(mDevice, mediaButton);
+ }
+
+ private void assertThatShowsPickerUi() {
+ // Assert that Search bar for DocumentsUi shows
+ // Add a short timeout wait for DocumentsUi to show
+ assertThat(new UiObject(new UiSelector().resourceIdMatches(
+ PhotoPickerUiUtils.REGEX_PACKAGE_NAME + ":id/bottom_sheet"))
+ .waitForExists(SHORT_TIMEOUT)).isTrue();
+
+ // Assert that "Recent files" header for DocumentsUi shows
+ assertThat(new UiObject(new UiSelector().resourceIdMatches(
+ PhotoPickerUiUtils.REGEX_PACKAGE_NAME + ":id/privacy_text"))
+ .exists()).isTrue();
+
+ // Assert that Documents list UiObject for DocumentsUi shows
+ assertThat(new UiObject(new UiSelector().text("Photos")).exists()).isTrue();
+ assertThat(new UiObject(new UiSelector().text("Albums")).exists()).isTrue();
+ }
+
+ private void assertThatShowsDocumentsUiButtons() {
+ // Assert that "Recent files" header for DocumentsUi shows
+ // Add a short timeout wait for DocumentsUi to show
+ assertThat(new UiObject(new UiSelector().resourceId(sDocumentsUiPackageName
+ + ":id/header_title")).waitForExists(SHORT_TIMEOUT)).isTrue();
+ }
+
+ private UiObject findSaveButton() {
+ return new UiObject(new UiSelector().resourceId(
+ sDocumentsUiPackageName + ":id/container_save")
+ .childSelector(new UiSelector().resourceId("android:id/button1")));
+ }
+
+ private void findAndClickFilesInDocumentsUi(List<String> fileNameList) throws Exception {
+ for (String fileName : fileNameList) {
+ findAndClickFileInDocumentsUi(fileName);
+ }
+ findAndClickSelect();
+ }
+
+ private void findAndClickSelect() throws Exception {
+ final UiObject selectButton = new UiObject(new UiSelector().resourceId(
+ sDocumentsUiPackageName + ":id/action_menu_select"));
+ clickAndWait(mDevice, selectButton);
+ }
+
+ private void findAndClickFileInDocumentsUi(String fileName) throws Exception {
+ final UiSelector docList = new UiSelector().resourceId(sDocumentsUiPackageName
+ + ":id/dir_list");
+
+ // Wait for the first list item to appear
+ assertWithMessage("First list item").that(
+ new UiObject(docList.childSelector(new UiSelector()))
+ .waitForExists(SHORT_TIMEOUT)).isTrue();
+
+ try {
+ // Enforce to set the list mode
+ // Because UiScrollable can't reach the real bottom (when WEB_LINKABLE_FILE item)
+ // in grid mode when screen landscape mode
+ clickAndWait(mDevice, new UiObject(new UiSelector().resourceId(sDocumentsUiPackageName
+ + ":id/sub_menu_list")));
+ } catch (UiObjectNotFoundException ignored) {
+ // Do nothing, already be in list mode.
+ }
+
+ // Repeat swipe gesture to find our item
+ // (UiScrollable#scrollIntoView does not seem to work well with SwipeRefreshLayout)
+ UiObject targetObject = new UiObject(docList.childSelector(new UiSelector()
+ .textContains(fileName)));
+ UiObject saveButton = findSaveButton();
+ int stepLimit = 10;
+ while (stepLimit-- > 0) {
+ if (targetObject.exists()) {
+ boolean targetObjectFullyVisible = !saveButton.exists()
+ || targetObject.getVisibleBounds().bottom
+ <= saveButton.getVisibleBounds().top;
+ if (targetObjectFullyVisible) {
+ break;
+ }
+ }
+
+ mDevice.swipe(/* startX= */ mDevice.getDisplayWidth() / 2,
+ /* startY= */ mDevice.getDisplayHeight() / 2,
+ /* endX= */ mDevice.getDisplayWidth() / 2,
+ /* endY= */ 0,
+ /* steps= */ 40);
+ }
+
+ targetObject.longClick();
+ }
+}
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/ActionPickImagesOnlyTest.java b/tests/PhotoPicker/src/android/photopicker/cts/ActionPickImagesOnlyTest.java
new file mode 100644
index 0000000..0f61dc7
--- /dev/null
+++ b/tests/PhotoPicker/src/android/photopicker/cts/ActionPickImagesOnlyTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.photopicker.cts;
+
+import static android.photopicker.cts.util.PhotoPickerAssertionsUtils.assertPersistedGrant;
+import static android.photopicker.cts.util.PhotoPickerAssertionsUtils.assertPickerUriFormat;
+import static android.photopicker.cts.util.PhotoPickerAssertionsUtils.assertRedactedReadOnlyAccess;
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImagesAndGetUris;
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.deleteMedia;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.SHORT_TIMEOUT;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.clickAndWait;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.findAddButton;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.findItemList;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertThrows;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.ClipData;
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.MediaStore;
+
+import androidx.test.runner.AndroidJUnit4;
+import androidx.test.uiautomator.UiObject;
+import androidx.test.uiautomator.UiSelector;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Photo Picker tests for {@link MediaStore#ACTION_PICK_IMAGES} intent exclusively
+ */
+@RunWith(AndroidJUnit4.class)
+public class ActionPickImagesOnlyTest extends PhotoPickerBaseTest {
+
+ private List<Uri> mUriList = new ArrayList<>();
+
+ @After
+ public void tearDown() throws Exception {
+ for (Uri uri : mUriList) {
+ deleteMedia(uri, mContext);
+ }
+ mUriList.clear();
+
+ if (mActivity != null) {
+ mActivity.finish();
+ }
+ }
+
+ @Test
+ public void testMultiSelect_invalidParam() throws Exception {
+ final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
+ intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit() + 1);
+ mActivity.startActivityForResult(intent, REQUEST_CODE);
+ final GetResultActivity.Result res = mActivity.getResult();
+ assertThat(res.resultCode).isEqualTo(Activity.RESULT_CANCELED);
+ }
+
+ @Test
+ public void testMultiSelect_invalidNegativeParam() throws Exception {
+ final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
+ intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, -1);
+ mActivity.startActivityForResult(intent, REQUEST_CODE);
+ final GetResultActivity.Result res = mActivity.getResult();
+ assertThat(res.resultCode).isEqualTo(Activity.RESULT_CANCELED);
+ }
+
+ @Test
+ public void testMultiSelect_returnsNotMoreThanMax() throws Exception {
+ final int maxCount = 2;
+ final int imageCount = maxCount + 1;
+ mUriList.addAll(createImagesAndGetUris(imageCount, mContext.getUserId()));
+
+ final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
+ intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, maxCount);
+ mActivity.startActivityForResult(intent, REQUEST_CODE);
+
+ final List<UiObject> itemList = findItemList(imageCount);
+ final int itemCount = itemList.size();
+ assertThat(itemCount).isEqualTo(imageCount);
+ // Select maxCount + 1 item
+ for (int i = 0; i < itemCount; i++) {
+ clickAndWait(mDevice, itemList.get(i));
+ }
+
+ UiObject snackbarTextView = mDevice.findObject(new UiSelector().text(
+ "Select up to 2 items"));
+ assertWithMessage("Timed out while waiting for snackbar to appear").that(
+ snackbarTextView.waitForExists(SHORT_TIMEOUT)).isTrue();
+
+ assertWithMessage("Timed out waiting for snackbar to disappear").that(
+ snackbarTextView.waitUntilGone(SHORT_TIMEOUT)).isTrue();
+
+ clickAndWait(mDevice, findAddButton());
+
+ final ClipData clipData = mActivity.getResult().data.getClipData();
+ final int count = clipData.getItemCount();
+ assertThat(count).isEqualTo(maxCount);
+ }
+
+ @Test
+ public void testDoesNotRespectExtraAllowMultiple() throws Exception {
+ final int imageCount = 2;
+ mUriList.addAll(createImagesAndGetUris(imageCount, mContext.getUserId()));
+ final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
+ mActivity.startActivityForResult(intent, REQUEST_CODE);
+
+ final List<UiObject> itemList = findItemList(imageCount);
+ final int itemCount = itemList.size();
+ assertThat(itemCount).isEqualTo(imageCount);
+ // Select 1 item
+ clickAndWait(mDevice, itemList.get(0));
+
+ final Uri uri = mActivity.getResult().data.getData();
+ assertPickerUriFormat(uri, mContext.getUserId());
+ assertPersistedGrant(uri, mContext.getContentResolver());
+ assertRedactedReadOnlyAccess(uri);
+ }
+
+ @Test
+ public void testMimeTypeFilter() throws Exception {
+ final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
+ intent.setType("audio/*");
+ assertThrows(ActivityNotFoundException.class,
+ () -> mActivity.startActivityForResult(intent, REQUEST_CODE));
+ }
+
+ @Test
+ public void testExtraMimeTypeFilter() throws Exception {
+ final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"audio/*"});
+ mActivity.startActivityForResult(intent, REQUEST_CODE);
+ final GetResultActivity.Result res = mActivity.getResult();
+ assertThat(res.resultCode).isEqualTo(Activity.RESULT_CANCELED);
+ }
+}
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/CloudPhotoPickerTest.java b/tests/PhotoPicker/src/android/photopicker/cts/CloudPhotoPickerTest.java
index aa0c954..ceaf3b2 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/CloudPhotoPickerTest.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/CloudPhotoPickerTest.java
@@ -21,7 +21,7 @@
import static android.photopicker.cts.PickerProviderMediaGenerator.setCloudProvider;
import static android.photopicker.cts.PickerProviderMediaGenerator.syncCloudProvider;
import static android.photopicker.cts.util.PhotoPickerAssertionsUtils.assertRedactedReadOnlyAccess;
-import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImages;
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImagesAndGetUris;
import static android.photopicker.cts.util.PhotoPickerFilesUtils.deleteMedia;
import static android.photopicker.cts.util.PhotoPickerUiUtils.findAddButton;
import static android.photopicker.cts.util.PhotoPickerUiUtils.findItemList;
@@ -120,7 +120,7 @@
@Test
public void testCloudPlusLocalSyncWithoutDedupe() throws Exception {
- createImages(1, mContext.getUserId(), mUriList);
+ mUriList.addAll(createImagesAndGetUris(1, mContext.getUserId()));
initPrimaryCloudProviderWithImage(Pair.create(null, CLOUD_ID1));
final ClipData clipData = fetchPickerMedia(2);
@@ -131,7 +131,7 @@
@Test
public void testCloudPlusLocalSyncWithDedupe() throws Exception {
- createImages(1, mContext.getUserId(), mUriList);
+ mUriList.addAll(createImagesAndGetUris(1, mContext.getUserId()));
initPrimaryCloudProviderWithImage(Pair.create(mUriList.get(0).getLastPathSegment(),
CLOUD_ID1));
@@ -295,7 +295,7 @@
// Create a placeholder local image to ensure that the picker UI is never empty.
// The PhotoPickerUiUtils#findItemList needs to select an item and it times out if the
// Picker UI is empty.
- createImages(1, mContext.getUserId(), mUriList);
+ mUriList.addAll(createImagesAndGetUris(1, mContext.getUserId()));
// Cloud provider isn't set
assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(mContext.getContentResolver(),
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerCrossProfileTest.java b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerCrossProfileTest.java
index 1092406..48c1ea1 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerCrossProfileTest.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerCrossProfileTest.java
@@ -18,7 +18,7 @@
import static android.photopicker.cts.util.PhotoPickerAssertionsUtils.assertPickerUriFormat;
import static android.photopicker.cts.util.PhotoPickerAssertionsUtils.assertRedactedReadOnlyAccess;
-import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImages;
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImagesAndGetUris;
import static android.photopicker.cts.util.PhotoPickerFilesUtils.deleteMedia;
import static android.photopicker.cts.util.PhotoPickerUiUtils.SHORT_TIMEOUT;
import static android.photopicker.cts.util.PhotoPickerUiUtils.findAddButton;
@@ -33,6 +33,7 @@
import android.net.Uri;
import android.provider.MediaStore;
+import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
import androidx.test.uiautomator.UiObject;
import androidx.test.uiautomator.UiSelector;
@@ -55,6 +56,7 @@
* Photo Picker Device only tests for cross profile interaction flows.
*/
@RunWith(BedsteadJUnit4.class)
+@LargeTest
public class PhotoPickerCrossProfileTest extends PhotoPickerBaseTest {
@ClassRule @Rule
public static final DeviceState sDeviceState = new DeviceState();
@@ -79,7 +81,7 @@
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testWorkApp_canAccessPersonalProfileContents() throws Exception {
final int imageCount = 2;
- createImages(imageCount, sDeviceState.primaryUser().id(), mUriList);
+ mUriList.addAll(createImagesAndGetUris(imageCount, sDeviceState.primaryUser().id()));
Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, imageCount);
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerTest.java b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerTest.java
index cc8022d3..440e472 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerTest.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerTest.java
@@ -16,16 +16,20 @@
package android.photopicker.cts;
+import static android.photopicker.cts.util.GetContentActivityAliasUtils.clearPackageData;
+import static android.photopicker.cts.util.GetContentActivityAliasUtils.getDocumentsUiPackageName;
+import static android.photopicker.cts.util.PhotoPickerAssertionsUtils.assertContainsMimeType;
import static android.photopicker.cts.util.PhotoPickerAssertionsUtils.assertMimeType;
import static android.photopicker.cts.util.PhotoPickerAssertionsUtils.assertPersistedGrant;
import static android.photopicker.cts.util.PhotoPickerAssertionsUtils.assertPickerUriFormat;
import static android.photopicker.cts.util.PhotoPickerAssertionsUtils.assertRedactedReadOnlyAccess;
-import static android.photopicker.cts.util.PhotoPickerFilesUtils.createDNGVideos;
-import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImages;
-import static android.photopicker.cts.util.PhotoPickerFilesUtils.createVideos;
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.createDNGVideosAndGetUris;
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImagesAndGetUris;
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.createVideosAndGetUris;
import static android.photopicker.cts.util.PhotoPickerFilesUtils.deleteMedia;
import static android.photopicker.cts.util.PhotoPickerUiUtils.REGEX_PACKAGE_NAME;
import static android.photopicker.cts.util.PhotoPickerUiUtils.SHORT_TIMEOUT;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.clickAndWait;
import static android.photopicker.cts.util.PhotoPickerUiUtils.findAddButton;
import static android.photopicker.cts.util.PhotoPickerUiUtils.findItemList;
import static android.photopicker.cts.util.PhotoPickerUiUtils.findPreviewAddButton;
@@ -34,32 +38,62 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-import android.app.Activity;
import android.content.ClipData;
import android.content.Intent;
+import android.media.AudioAttributes;
+import android.media.AudioFocusRequest;
+import android.media.AudioManager;
import android.net.Uri;
+import android.photopicker.cts.util.GetContentActivityAliasUtils;
import android.provider.MediaStore;
-import androidx.test.runner.AndroidJUnit4;
import androidx.test.uiautomator.UiObject;
import androidx.test.uiautomator.UiObjectNotFoundException;
import androidx.test.uiautomator.UiSelector;
import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
/**
* Photo Picker Device only tests for common flows.
*/
-@RunWith(AndroidJUnit4.class)
+@RunWith(Parameterized.class)
public class PhotoPickerTest extends PhotoPickerBaseTest {
+
+ @Parameter(0)
+ public String mAction;
+
+ @Parameters(name = "intent={0}")
+ public static Iterable<? extends Object> data() {
+ return getTestParameters();
+ }
+
private List<Uri> mUriList = new ArrayList<>();
+ private static int sGetContentTakeOverActivityAliasState;
+
+ @BeforeClass
+ public static void setUpBeforeClass() throws Exception {
+ sGetContentTakeOverActivityAliasState = GetContentActivityAliasUtils.enableAndGetOldState();
+ clearPackageData(getDocumentsUiPackageName());
+ }
+
+ @AfterClass
+ public static void tearDownAfterClass() throws Exception {
+ GetContentActivityAliasUtils.restoreState(sGetContentTakeOverActivityAliasState);
+ }
+
@After
public void tearDown() throws Exception {
for (Uri uri : mUriList) {
@@ -75,13 +109,13 @@
@Test
public void testSingleSelect() throws Exception {
final int itemCount = 1;
- createImages(itemCount, mContext.getUserId(), mUriList);
+ mUriList.addAll(createImagesAndGetUris(itemCount, mContext.getUserId()));
- final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
- mActivity.startActivityForResult(intent, REQUEST_CODE);
+ final Intent intent = new Intent(mAction);
+ launchPhotoPickerForIntent(intent);
final UiObject item = findItemList(itemCount).get(0);
- clickAndWait(item);
+ clickAndWait(mDevice, item);
final Uri uri = mActivity.getResult().data.getData();
assertPickerUriFormat(uri, mContext.getUserId());
@@ -92,19 +126,20 @@
@Test
public void testSingleSelectForFavoritesAlbum() throws Exception {
final int itemCount = 1;
- createImages(itemCount, mContext.getUserId(), mUriList, true);
+ mUriList.addAll(createImagesAndGetUris(itemCount, mContext.getUserId(),
+ /* isFavorite */ true));
- final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
- mActivity.startActivityForResult(intent, REQUEST_CODE);
+ final Intent intent = new Intent(mAction);
+ launchPhotoPickerForIntent(intent);
UiObject albumsTab = mDevice.findObject(new UiSelector().text(
"Albums"));
- clickAndWait(albumsTab);
+ clickAndWait(mDevice, albumsTab);
final UiObject album = findItemList(1).get(0);
- clickAndWait(album);
+ clickAndWait(mDevice, album);
final UiObject item = findItemList(itemCount).get(0);
- clickAndWait(item);
+ clickAndWait(mDevice, item);
final Uri uri = mActivity.getResult().data.getData();
assertPickerUriFormat(uri, mContext.getUserId());
@@ -114,18 +149,18 @@
@Test
public void testLaunchPreviewMultipleForVideoAlbum() throws Exception {
final int videoCount = 2;
- createVideos(videoCount, mContext.getUserId(), mUriList);
+ mUriList.addAll(createVideosAndGetUris(videoCount, mContext.getUserId()));
- final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
+ Intent intent = new Intent(mAction);
intent.setType("video/*");
- intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit());
- mActivity.startActivityForResult(intent, REQUEST_CODE);
+ addMultipleSelectionFlag(intent);
+ launchPhotoPickerForIntent(intent);
UiObject albumsTab = mDevice.findObject(new UiSelector().text(
"Albums"));
- clickAndWait(albumsTab);
+ clickAndWait(mDevice, albumsTab);
final UiObject album = findItemList(1).get(0);
- clickAndWait(album);
+ clickAndWait(mDevice, album);
final List<UiObject> itemList = findItemList(videoCount);
final int itemCount = itemList.size();
@@ -133,10 +168,10 @@
assertThat(itemCount).isEqualTo(videoCount);
for (int i = 0; i < itemCount; i++) {
- clickAndWait(itemList.get(i));
+ clickAndWait(mDevice, itemList.get(i));
}
- clickAndWait(findViewSelectedButton());
+ clickAndWait(mDevice, findViewSelectedButton());
// Wait for playback to start. This is needed in some devices where playback
// buffering -> ready state takes around 10s.
@@ -147,10 +182,10 @@
@Test
public void testSingleSelectWithPreview() throws Exception {
final int itemCount = 1;
- createImages(itemCount, mContext.getUserId(), mUriList);
+ mUriList.addAll(createImagesAndGetUris(itemCount, mContext.getUserId()));
- final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
- mActivity.startActivityForResult(intent, REQUEST_CODE);
+ final Intent intent = new Intent(mAction);
+ launchPhotoPickerForIntent(intent);
final UiObject item = findItemList(itemCount).get(0);
item.longClick();
@@ -158,7 +193,7 @@
final UiObject addButton = findPreviewAddOrSelectButton();
assertThat(addButton.waitForExists(1000)).isTrue();
- clickAndWait(addButton);
+ clickAndWait(mDevice, addButton);
final Uri uri = mActivity.getResult().data.getData();
assertPickerUriFormat(uri, mContext.getUserId());
@@ -166,91 +201,21 @@
}
@Test
- public void testMultiSelect_invalidParam() throws Exception {
- final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
- intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit() + 1);
- mActivity.startActivityForResult(intent, REQUEST_CODE);
- final GetResultActivity.Result res = mActivity.getResult();
- assertThat(res.resultCode).isEqualTo(Activity.RESULT_CANCELED);
- }
-
- @Test
- public void testMultiSelect_invalidNegativeParam() throws Exception {
- final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
- intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, -1);
- mActivity.startActivityForResult(intent, REQUEST_CODE);
- final GetResultActivity.Result res = mActivity.getResult();
- assertThat(res.resultCode).isEqualTo(Activity.RESULT_CANCELED);
- }
-
- @Test
- public void testMultiSelect_returnsNotMoreThanMax() throws Exception {
- final int maxCount = 2;
- final int imageCount = maxCount + 1;
- createImages(imageCount, mContext.getUserId(), mUriList);
- final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
- intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, maxCount);
- mActivity.startActivityForResult(intent, REQUEST_CODE);
-
- final List<UiObject> itemList = findItemList(imageCount);
- final int itemCount = itemList.size();
- assertThat(itemCount).isEqualTo(imageCount);
- // Select maxCount + 1 item
- for (int i = 0; i < itemCount; i++) {
- clickAndWait(itemList.get(i));
- }
-
- UiObject snackbarTextView = mDevice.findObject(new UiSelector().text(
- "Select up to 2 items"));
- assertWithMessage("Timed out while waiting for snackbar to appear").that(
- snackbarTextView.waitForExists(SHORT_TIMEOUT)).isTrue();
-
- assertWithMessage("Timed out waiting for snackbar to disappear").that(
- snackbarTextView.waitUntilGone(SHORT_TIMEOUT)).isTrue();
-
- clickAndWait(findAddButton());
-
- final ClipData clipData = mActivity.getResult().data.getClipData();
- final int count = clipData.getItemCount();
- assertThat(count).isEqualTo(maxCount);
- }
-
- @Test
- public void testDoesNotRespectExtraAllowMultiple() throws Exception {
- final int imageCount = 2;
- createImages(imageCount, mContext.getUserId(), mUriList);
- final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
- intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
- mActivity.startActivityForResult(intent, REQUEST_CODE);
-
- final List<UiObject> itemList = findItemList(imageCount);
- final int itemCount = itemList.size();
- assertThat(itemCount).isEqualTo(imageCount);
- // Select 1 item
- clickAndWait(itemList.get(0));
-
- final Uri uri = mActivity.getResult().data.getData();
- assertPickerUriFormat(uri, mContext.getUserId());
- assertPersistedGrant(uri, mContext.getContentResolver());
- assertRedactedReadOnlyAccess(uri);
- }
-
- @Test
public void testMultiSelect() throws Exception {
final int imageCount = 4;
- createImages(imageCount, mContext.getUserId(), mUriList);
- final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
- intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit());
- mActivity.startActivityForResult(intent, REQUEST_CODE);
+ mUriList.addAll(createImagesAndGetUris(imageCount, mContext.getUserId()));
+ Intent intent = new Intent(mAction);
+ addMultipleSelectionFlag(intent);
+ launchPhotoPickerForIntent(intent);
final List<UiObject> itemList = findItemList(imageCount);
final int itemCount = itemList.size();
assertThat(itemCount).isEqualTo(imageCount);
for (int i = 0; i < itemCount; i++) {
- clickAndWait(itemList.get(i));
+ clickAndWait(mDevice, itemList.get(i));
}
- clickAndWait(findAddButton());
+ clickAndWait(mDevice, findAddButton());
final ClipData clipData = mActivity.getResult().data.getClipData();
final int count = clipData.getItemCount();
@@ -266,18 +231,19 @@
@Test
public void testMultiSelect_longPress() throws Exception {
final int videoCount = 3;
- createDNGVideos(videoCount, mContext.getUserId(), mUriList);
- final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
- intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit());
+ mUriList.addAll(createDNGVideosAndGetUris(videoCount, mContext.getUserId()));
+
+ Intent intent = new Intent(mAction);
intent.setType("video/*");
- mActivity.startActivityForResult(intent, REQUEST_CODE);
+ addMultipleSelectionFlag(intent);
+ launchPhotoPickerForIntent(intent);
final List<UiObject> itemList = findItemList(videoCount);
final int itemCount = itemList.size();
assertThat(itemCount).isEqualTo(videoCount);
// Select one item from Photo grid
- clickAndWait(itemList.get(0));
+ clickAndWait(mDevice, itemList.get(0));
// Preview the item
UiObject item = itemList.get(1);
@@ -289,14 +255,14 @@
.that(addOrSelectButton.waitForExists(1000)).isTrue();
// Select the item from Preview
- clickAndWait(addOrSelectButton);
+ clickAndWait(mDevice, addOrSelectButton);
mDevice.pressBack();
// Select one more item from Photo grid
- clickAndWait(itemList.get(2));
+ clickAndWait(mDevice, itemList.get(2));
- clickAndWait(findAddButton());
+ clickAndWait(mDevice, findAddButton());
// Verify that all 3 items are returned
final ClipData clipData = mActivity.getResult().data.getClipData();
@@ -313,19 +279,20 @@
@Test
public void testMultiSelect_preview() throws Exception {
final int imageCount = 4;
- createImages(imageCount, mContext.getUserId(), mUriList);
- final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
- intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit());
- mActivity.startActivityForResult(intent, REQUEST_CODE);
+ mUriList.addAll(createImagesAndGetUris(imageCount, mContext.getUserId()));
+
+ Intent intent = new Intent(mAction);
+ addMultipleSelectionFlag(intent);
+ launchPhotoPickerForIntent(intent);
final List<UiObject> itemList = findItemList(imageCount);
final int itemCount = itemList.size();
assertThat(itemCount).isEqualTo(imageCount);
for (int i = 0; i < itemCount; i++) {
- clickAndWait(itemList.get(i));
+ clickAndWait(mDevice, itemList.get(i));
}
- clickAndWait(findViewSelectedButton());
+ clickAndWait(mDevice, findViewSelectedButton());
// Swipe left three times
swipeLeftAndWait();
@@ -333,10 +300,10 @@
swipeLeftAndWait();
// Deselect one item
- clickAndWait(findPreviewSelectedCheckButton());
+ clickAndWait(mDevice, findPreviewSelectedCheckButton());
// Return selected items
- clickAndWait(findPreviewAddButton());
+ clickAndWait(mDevice, findPreviewAddButton());
final ClipData clipData = mActivity.getResult().data.getClipData();
final int count = clipData.getItemCount();
@@ -383,20 +350,31 @@
// Test 2: Click Mute Button
// Click to unmute the audio
- clickAndWait(muteButton);
+ clickAndWait(mDevice, muteButton);
+
+ waitForBinderCallsToComplete();
+
// Check that mute button state is unmute, i.e., it shows `volume up` icon
assertMuteButtonState(muteButton, /* isMuted */ false);
// Click on the muteButton and check that mute button status is now 'mute'
- clickAndWait(muteButton);
+ clickAndWait(mDevice, muteButton);
+
+ waitForBinderCallsToComplete();
+
assertMuteButtonState(muteButton, /* isMuted */ true);
// Click on the muteButton and check that mute button status is now unmute
- clickAndWait(muteButton);
+ clickAndWait(mDevice, muteButton);
+
+ waitForBinderCallsToComplete();
+
assertMuteButtonState(muteButton, /* isMuted */ false);
// Test 3: Next preview resumes mute state
// Go back and launch preview again
mDevice.pressBack();
- clickAndWait(findViewSelectedButton());
+ clickAndWait(mDevice, findViewSelectedButton());
+
+ waitForBinderCallsToComplete();
// check that player controls are visible
assertPlayerControlsVisible(playPauseButton, muteButton);
@@ -421,16 +399,25 @@
assertMuteButtonState(muteButton, /* isMuted */ true);
// Swipe to next page and check that muteButton is in mute state.
swipeLeftAndWait();
+
+ waitForBinderCallsToComplete();
+
// set-up and wait for player controls to be sticky
setUpAndAssertStickyPlayerControls(playerView, playPauseButton, muteButton);
assertMuteButtonState(muteButton, /* isMuted */ true);
// Test 2: Swipe resumes mute state, with state of mute button 'volume up' / 'unmute'
// Click muteButton again to check the next video resumes the previous video's mute state
- clickAndWait(muteButton);
+ clickAndWait(mDevice, muteButton);
+
+ waitForBinderCallsToComplete();
+
assertMuteButtonState(muteButton, /* isMuted */ false);
// check that next video resumed previous video's mute state
swipeLeftAndWait();
+
+ waitForBinderCallsToComplete();
+
// Wait for 1s before checking Play/Pause button's visibility
playPauseButton.waitForExists(1000);
// check that player controls are visible
@@ -442,6 +429,78 @@
}
@Test
+ public void testVideoPreviewAudioFocus() throws Exception {
+ final int[] focusStateForTest = new int[1];
+ final AudioManager audioManager = mContext.getSystemService(AudioManager.class);
+ AudioFocusRequest audioFocusRequest =
+ new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
+ .setAudioAttributes(new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.USAGE_MEDIA)
+ .setUsage(AudioAttributes.CONTENT_TYPE_MOVIE)
+ .build())
+ .setWillPauseWhenDucked(true)
+ .setAcceptsDelayedFocusGain(false)
+ .setOnAudioFocusChangeListener(focusChange -> {
+ if (focusChange == AudioManager.AUDIOFOCUS_LOSS
+ || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT
+ || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
+ focusStateForTest[0] = focusChange;
+ }
+ })
+ .build();
+
+ // Request AudioFocus
+ assertWithMessage("Expected requestAudioFocus result")
+ .that(audioManager.requestAudioFocus(audioFocusRequest))
+ .isEqualTo(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+
+ // Launch Preview
+ launchPreviewMultipleWithVideos(/* videoCount */ 2);
+ // Video preview launches in mute mode, hence, test's audio focus shouldn't be lost when
+ // video preview starts
+ assertThat(focusStateForTest[0]).isEqualTo(0);
+
+ final UiObject muteButton = findMuteButton();
+ // unmute the audio of video preview
+ clickAndWait(mDevice, muteButton);
+
+ // Remote video preview involves binder calls
+ // Wait for Binder calls to complete and device to be idle
+ MediaStore.waitForIdle(mContext.getContentResolver());
+ mDevice.waitForIdle();
+
+ assertMuteButtonState(muteButton, /* isMuted */ false);
+
+ // Verify that test lost the audio focus because PhotoPicker has requested audio focus now.
+ assertThat(focusStateForTest[0]).isEqualTo(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
+
+ // Reset the focusStateForTest to verify test loses audio focus when video preview is
+ // launched with unmute state
+ focusStateForTest[0] = 0;
+ // Abandon the audio focus before requesting again. This is necessary to reduce test flakes
+ audioManager.abandonAudioFocusRequest(audioFocusRequest);
+ // Request AudioFocus from test again
+ assertWithMessage("Expected requestAudioFocus result")
+ .that(audioManager.requestAudioFocus(audioFocusRequest))
+ .isEqualTo(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+
+ // Wait for PhotoPicker to lose Audio Focus
+ findPlayButton().waitForExists(SHORT_TIMEOUT);
+ // Test requesting audio focus will make PhotoPicker lose audio focus, Verify video is
+ // paused when PhotoPicker loses audio focus.
+ assertWithMessage("PlayPause button's content description")
+ .that(findPlayPauseButton().getContentDescription())
+ .isEqualTo("Play");
+
+ // Swipe to next video and verify preview gains audio focus
+ swipeLeftAndWait();
+ findPauseButton().waitForExists(SHORT_TIMEOUT);
+ // Video preview is now in unmute mode. Hence, PhotoPicker will request audio focus. Verify
+ // that test lost the audio focus.
+ assertThat(focusStateForTest[0]).isEqualTo(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
+ }
+
+ @Test
@Ignore("Re-enable once we find work around for b/226318844")
public void testMultiSelect_previewVideoControlsVisibility() throws Exception {
launchPreviewMultipleWithVideos(/* videoCount */ 3);
@@ -453,7 +512,7 @@
final UiObject playerView = findPlayerView();
// Click on StyledPlayerView to make the video controls visible
- clickAndWait(playerView);
+ clickAndWait(mDevice, playerView);
assertPlayerControlsVisible(playPauseButton, muteButton);
// Wait for 1s and check that controls are still visible
@@ -470,7 +529,7 @@
assertPlayerControlsHidden(playPauseButton, muteButton);
// Click on the StyledPlayerView and check that controls appear
- clickAndWait(playerView);
+ clickAndWait(mDevice, playerView);
assertPlayerControlsVisible(playPauseButton, muteButton);
// Swipe left to check that controls are now visible on swipe
@@ -487,26 +546,26 @@
@Test
public void testMimeTypeFilter() throws Exception {
final int videoCount = 2;
- createDNGVideos(videoCount, mContext.getUserId(), mUriList);
+ mUriList.addAll(createDNGVideosAndGetUris(videoCount, mContext.getUserId()));
final int imageCount = 1;
- createImages(imageCount, mContext.getUserId(), mUriList);
+ mUriList.addAll(createImagesAndGetUris(imageCount, mContext.getUserId()));
final String mimeType = "video/dng";
- final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
- intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit());
+ Intent intent = new Intent(mAction);
+ addMultipleSelectionFlag(intent);
intent.setType(mimeType);
- mActivity.startActivityForResult(intent, REQUEST_CODE);
+ launchPhotoPickerForIntent(intent);
// find all items
final List<UiObject> itemList = findItemList(-1);
final int itemCount = itemList.size();
assertThat(itemCount).isAtLeast(videoCount);
for (int i = 0; i < itemCount; i++) {
- clickAndWait(itemList.get(i));
+ clickAndWait(mDevice, itemList.get(i));
}
- clickAndWait(findAddButton());
+ clickAndWait(mDevice, findAddButton());
final ClipData clipData = mActivity.getResult().data.getClipData();
final int count = clipData.getItemCount();
@@ -520,6 +579,89 @@
}
}
+ @Test
+ public void testExtraMimeTypeFilter() throws Exception {
+ final int dngVideoCount = 2;
+ // Creates 2 videos with mime type: "video/dng"
+ mUriList.addAll(createDNGVideosAndGetUris(dngVideoCount, mContext.getUserId()));
+
+ final int mp4VideoCount = 3;
+ // Creates 3 videos with mime type: "video/mp4"
+ mUriList.addAll(createVideosAndGetUris(mp4VideoCount, mContext.getUserId()));
+
+ final int imageCount = 4;
+ // Creates 4 images with mime type: "image/dng"
+ mUriList.addAll(createImagesAndGetUris(imageCount, mContext.getUserId()));
+
+ Intent intent = new Intent(mAction);
+ addMultipleSelectionFlag(intent);
+
+ if (Intent.ACTION_GET_CONTENT.equals(intent.getAction())) {
+ intent.setType("*/*");
+ }
+ final String[] mimeTypes = new String[]{"video/dng", "image/dng"};
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
+ launchPhotoPickerForIntent(intent);
+
+ final int totalCount = dngVideoCount + imageCount;
+ final List<UiObject> itemList = findItemList(totalCount);
+ final int itemCount = itemList.size();
+ assertThat(itemCount).isAtLeast(totalCount);
+ for (int i = 0; i < itemCount; i++) {
+ clickAndWait(mDevice, itemList.get(i));
+ }
+
+ clickAndWait(mDevice, findAddButton());
+
+ final ClipData clipData = mActivity.getResult().data.getClipData();
+ assertWithMessage("Expected number of items returned to be: " + itemCount)
+ .that(clipData.getItemCount()).isEqualTo(itemCount);
+ for (int i = 0; i < itemCount; i++) {
+ final Uri uri = clipData.getItemAt(i).getUri();
+ assertPickerUriFormat(uri, mContext.getUserId());
+ assertPersistedGrant(uri, mContext.getContentResolver());
+ assertRedactedReadOnlyAccess(uri);
+ assertContainsMimeType(uri, mimeTypes);
+ }
+ }
+
+ @Test
+ public void testMimeTypeFilterPriority() throws Exception {
+ final int videoCount = 2;
+ mUriList.addAll(createDNGVideosAndGetUris(videoCount, mContext.getUserId()));
+ final int imageCount = 1;
+ mUriList.addAll(createImagesAndGetUris(imageCount, mContext.getUserId()));
+
+ Intent intent = new Intent(mAction);
+ addMultipleSelectionFlag(intent);
+ // setType has lower priority than EXTRA_MIME_TYPES filters.
+ intent.setType("image/*");
+ final String mimeType = "video/dng";
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {mimeType});
+ launchPhotoPickerForIntent(intent);
+
+ // find all items
+ final List<UiObject> itemList = findItemList(-1);
+ final int itemCount = itemList.size();
+ assertThat(itemCount).isAtLeast(videoCount);
+ for (int i = 0; i < itemCount; i++) {
+ clickAndWait(mDevice, itemList.get(i));
+ }
+
+ clickAndWait(mDevice, findAddButton());
+
+ final ClipData clipData = mActivity.getResult().data.getClipData();
+ assertWithMessage("Expected number of items returned to be: " + itemCount)
+ .that(clipData.getItemCount()).isEqualTo(itemCount);
+ for (int i = 0; i < itemCount; i++) {
+ final Uri uri = clipData.getItemAt(i).getUri();
+ assertPickerUriFormat(uri, mContext.getUserId());
+ assertPersistedGrant(uri, mContext.getContentResolver());
+ assertRedactedReadOnlyAccess(uri);
+ assertMimeType(uri, mimeType);
+ }
+ }
+
private void assertMuteButtonState(UiObject muteButton, boolean isMuted)
throws UiObjectNotFoundException {
// We use content description to assert the state of the mute button, there is no other way
@@ -539,26 +681,27 @@
assertPlayerControlsAutoHide(playPauseButton, muteButton);
// Click on StyledPlayerView to make the video controls visible
- clickAndWait(findPlayerView());
+ clickAndWait(mDevice, findPlayerView());
// PlayPause button is now pause button, click the button to pause the video.
- clickAndWait(playPauseButton);
+ clickAndWait(mDevice, playPauseButton);
// Wait for 1s and check that play button is not auto hidden
assertPlayerControlsDontAutoHide(playPauseButton, muteButton);
// PlayPause button is now play button, click the button to play the video.
- clickAndWait(playPauseButton);
+ clickAndWait(mDevice, playPauseButton);
// Check that pause button auto-hides in 1s.
assertPlayerControlsAutoHide(playPauseButton, muteButton);
}
private void launchPreviewMultipleWithVideos(int videoCount) throws Exception {
- createVideos(videoCount, mContext.getUserId(), mUriList);
- final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
- intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit());
+ mUriList.addAll(createVideosAndGetUris(videoCount, mContext.getUserId()));
+
+ Intent intent = new Intent(mAction);
intent.setType("video/*");
- mActivity.startActivityForResult(intent, REQUEST_CODE);
+ addMultipleSelectionFlag(intent);
+ launchPhotoPickerForIntent(intent);
final List<UiObject> itemList = findItemList(videoCount);
final int itemCount = itemList.size();
@@ -566,16 +709,20 @@
assertThat(itemCount).isEqualTo(videoCount);
for (int i = 0; i < itemCount; i++) {
- clickAndWait(itemList.get(i));
+ clickAndWait(mDevice, itemList.get(i));
}
- clickAndWait(findViewSelectedButton());
+ clickAndWait(mDevice, findViewSelectedButton());
// Wait for playback to start. This is needed in some devices where playback
// buffering -> ready state takes around 10s.
final long playbackStartTimeout = 10000;
(findPreviewVideoImageView()).waitUntilGone(playbackStartTimeout);
+ waitForBinderCallsToComplete();
+ }
+
+ private void waitForBinderCallsToComplete() {
// Wait for Binder calls to complete and device to be idle
MediaStore.waitForIdle(mContext.getContentResolver());
mDevice.waitForIdle();
@@ -588,7 +735,7 @@
// Wait for 1s or Play/Pause button to hide
playPauseButton.waitUntilGone(1000);
// Click on StyledPlayerView to make the video controls visible
- clickAndWait(playerView);
+ clickAndWait(mDevice, playerView);
assertPlayerControlsVisible(playPauseButton, muteButton);
}
@@ -650,20 +797,54 @@
REGEX_PACKAGE_NAME + ":id/exo_play_pause"));
}
+ private static UiObject findPauseButton() {
+ return new UiObject(new UiSelector().descriptionContains("Pause"));
+ }
+
+ private static UiObject findPlayButton() {
+ return new UiObject(new UiSelector().descriptionContains("Play"));
+ }
+
private static UiObject findPreviewVideoImageView() {
return new UiObject(new UiSelector().resourceIdMatches(
REGEX_PACKAGE_NAME + ":id/preview_video_image"));
}
- private void clickAndWait(UiObject uiObject) throws Exception {
- uiObject.click();
- mDevice.waitForIdle();
- }
-
private void swipeLeftAndWait() {
final int width = mDevice.getDisplayWidth();
final int height = mDevice.getDisplayHeight();
mDevice.swipe(15 * width / 20, height / 2, width / 20, height / 2, 10);
mDevice.waitForIdle();
}
+
+ private static List<String> getTestParameters() {
+ return Arrays.asList(
+ MediaStore.ACTION_PICK_IMAGES,
+ Intent.ACTION_GET_CONTENT
+ );
+ }
+
+ private void addMultipleSelectionFlag(Intent intent) {
+ switch (intent.getAction()) {
+ case MediaStore.ACTION_PICK_IMAGES:
+ intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX,
+ MediaStore.getPickImagesMaxLimit());
+ break;
+ case Intent.ACTION_GET_CONTENT:
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
+ break;
+ default:
+ // do nothing
+ }
+ }
+
+ private void launchPhotoPickerForIntent(Intent intent) throws Exception {
+ // GET_CONTENT needs to have setType
+ if (Intent.ACTION_GET_CONTENT.equals(intent.getAction()) && intent.getType() == null) {
+ intent.setType("*/*");
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/*", "video/*"});
+ }
+
+ mActivity.startActivityForResult(intent, REQUEST_CODE);
+ }
}
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/util/GetContentActivityAliasUtils.java b/tests/PhotoPicker/src/android/photopicker/cts/util/GetContentActivityAliasUtils.java
new file mode 100644
index 0000000..0ccdd3f
--- /dev/null
+++ b/tests/PhotoPicker/src/android/photopicker/cts/util/GetContentActivityAliasUtils.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.photopicker.cts.util;
+
+import android.Manifest;
+import android.app.Instrumentation;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.content.pm.ResolveInfo;
+import android.provider.MediaStore;
+
+import androidx.test.InstrumentationRegistry;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Supplier;
+
+public class GetContentActivityAliasUtils {
+
+ private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(5);
+ private static final long POLLING_SLEEP_MILLIS = 100;
+
+ private static ComponentName sComponentName = new ComponentName(
+ getMediaProviderPackageName(),
+ "com.android.providers.media.photopicker.PhotoPickerGetContentActivity");
+
+ public static int enableAndGetOldState() throws Exception {
+ final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
+ final PackageManager packageManager = inst.getContext().getPackageManager();
+ if (isComponentEnabledSetAsExpected(packageManager,
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED)) {
+ return PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
+ }
+
+ final int currentState = packageManager.getComponentEnabledSetting(sComponentName);
+
+ updateComponentEnabledSetting(packageManager,
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
+
+ return currentState;
+ }
+
+ public static void restoreState(int oldState) throws Exception {
+ final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
+ updateComponentEnabledSetting(inst.getContext().getPackageManager(), oldState);
+ }
+
+ /**
+ * Clears the package data.
+ */
+ public static void clearPackageData(String packageName) throws Exception {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .executeShellCommand("pm clear " + packageName);
+
+ // We should ideally be listening to an effective measure to know if package data was
+ // cleared, like listening to a broadcasts or checking a value. But that information is
+ // very package private and not available.
+ Thread.sleep(500);
+ }
+
+ public static String getDocumentsUiPackageName() {
+ final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+ intent.setType("*/*");
+ final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
+ final ResolveInfo ri = inst.getContext().getPackageManager().resolveActivity(intent, 0);
+ return ri.activityInfo.packageName;
+ }
+
+ private static void updateComponentEnabledSetting(PackageManager packageManager,
+ int state) throws Exception {
+ final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
+ inst.getUiAutomation().adoptShellPermissionIdentity(
+ Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE);
+ try {
+ packageManager.setComponentEnabledSetting(sComponentName, state,
+ PackageManager.DONT_KILL_APP);
+ } finally {
+ inst.getUiAutomation().dropShellPermissionIdentity();
+ }
+ waitForComponentToBeInExpectedState(packageManager, state);
+ }
+
+ private static void waitForComponentToBeInExpectedState(PackageManager packageManager,
+ int state) throws Exception {
+ pollForCondition(() -> isComponentEnabledSetAsExpected(packageManager, state),
+ "Timed out while waiting for component to be enabled");
+ }
+
+ private static void pollForCondition(Supplier<Boolean> condition, String errorMessage)
+ throws Exception {
+ for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) {
+ if (condition.get()) {
+ return;
+ }
+ Thread.sleep(POLLING_SLEEP_MILLIS);
+ }
+ throw new TimeoutException(errorMessage);
+ }
+
+ private static boolean isComponentEnabledSetAsExpected(PackageManager packageManager,
+ int state) {
+ return packageManager.getComponentEnabledSetting(sComponentName) == state;
+ }
+
+ private static String getMediaProviderPackageName() {
+ final Instrumentation inst = androidx.test.InstrumentationRegistry.getInstrumentation();
+ final PackageManager packageManager = inst.getContext().getPackageManager();
+ final ProviderInfo providerInfo = packageManager.resolveContentProvider(
+ MediaStore.AUTHORITY, PackageManager.MATCH_ALL);
+ return providerInfo.packageName;
+ }
+}
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerAssertionsUtils.java b/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerAssertionsUtils.java
index 9eb7d59..6d86cee 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerAssertionsUtils.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerAssertionsUtils.java
@@ -80,6 +80,12 @@
assertThat(resultMimeType).isEqualTo(expectedMimeType);
}
+ public static void assertContainsMimeType(Uri uri, String[] expectedMimeTypes) {
+ final Context context = InstrumentationRegistry.getTargetContext();
+ final String resultMimeType = context.getContentResolver().getType(uri);
+ assertThat(Arrays.asList(expectedMimeTypes).contains(resultMimeType)).isTrue();
+ }
+
public static void assertRedactedReadOnlyAccess(Uri uri) throws Exception {
assertThat(uri).isNotNull();
final String[] projection = new String[]{ PickerMediaColumns.MIME_TYPE };
@@ -120,11 +126,7 @@
.that(xmp.contains("13166/7763")).isTrue();
}
- // assert no write access
- try (ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "w")) {
- fail("Does not grant write access to uri " + uri.toString());
- } catch (SecurityException | FileNotFoundException expected) {
- }
+ assertNoWriteAccess(uri, resolver);
}
private static void assertImageRedactedReadOnlyAccess(Uri uri, ContentResolver resolver)
@@ -153,12 +155,7 @@
assertImageExifRedacted(is);
}
- // Assert no write access
- try (ParcelFileDescriptor pfd =
- ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE)) {
- fail("Does not grant write access to file " + file);
- } catch (IOException e) {
- }
+ assertNoWriteAccess(uri, resolver);
}
}
@@ -179,4 +176,19 @@
assertWithMessage("Redacted non-location XMP")
.that(xmp.contains("LensDefaults")).isTrue();
}
+
+ public static void assertReadOnlyAccess(Uri uri, ContentResolver resolver) throws Exception {
+ try (ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r")) {
+ assertThat(pfd).isNotNull();
+ }
+
+ assertNoWriteAccess(uri, resolver);
+ }
+
+ private static void assertNoWriteAccess(Uri uri, ContentResolver resolver) throws Exception {
+ try (ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "w")) {
+ fail("Does not grant write access to uri " + uri.toString());
+ } catch (SecurityException | FileNotFoundException expected) {
+ }
+ }
}
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerFilesUtils.java b/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerFilesUtils.java
index 3705ddd..2732df7 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerFilesUtils.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerFilesUtils.java
@@ -26,6 +26,7 @@
import android.provider.MediaStore;
import android.provider.cts.ProviderTestUtils;
import android.provider.cts.media.MediaStoreUtils;
+import android.util.Pair;
import androidx.test.InstrumentationRegistry;
@@ -34,6 +35,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.ArrayList;
import java.util.List;
/**
@@ -42,24 +44,55 @@
public class PhotoPickerFilesUtils {
public static final String DISPLAY_NAME_PREFIX = "ctsPhotoPicker";
- public static void createImages(int count, int userId, List<Uri> uriList)
+ public static List<Uri> createImagesAndGetUris(int count, int userId)
throws Exception {
- createImages(count, userId, uriList, false);
+ return createImagesAndGetUris(count, userId, /* isFavorite */ false);
}
- public static void createImages(int count, int userId, List<Uri> uriList, boolean isFavorite)
+ public static List<Uri> createImagesAndGetUris(int count, int userId, boolean isFavorite)
throws Exception {
+ List<Pair<Uri, String>> createdImagesData = createImagesAndGetUriAndPath(count, userId,
+ isFavorite);
+
+ List<Uri> uriList = new ArrayList<>();
+ for (Pair<Uri, String> createdImageData: createdImagesData) {
+ uriList.add(createdImageData.first);
+ }
+
+ return uriList;
+ }
+
+ /**
+ * Create multiple images according to the given parameters and returns the uris and filenames
+ * of the images created.
+ *
+ * @param count number of images to create
+ * @param userId user id to create images in
+ *
+ * @return list of data (uri and filename pair) about the images created
+ */
+ public static List<Pair<Uri, String>> createImagesAndGetUriAndPath(int count, int userId,
+ boolean isFavorite) throws Exception {
+ List<Pair<Uri, String>> createdImagesData = new ArrayList<>();
+
for (int i = 0; i < count; i++) {
- final Uri uri = createImage(userId, isFavorite);
- uriList.add(uri);
- clearMediaOwner(uri, userId);
+ Pair<Uri, String> createdImageData = createImage(userId, isFavorite);
+ createdImagesData.add(createdImageData);
+ clearMediaOwner(createdImageData.first, userId);
}
// Wait for Picker db sync to complete
MediaStore.waitForIdle(InstrumentationRegistry.getContext().getContentResolver());
+
+ return createdImagesData;
}
- public static void createDNGVideos(int count, int userId, List<Uri> uriList)
- throws Exception {
+ public static Pair<Uri, String> createImage(int userId, boolean isFavorite) throws Exception {
+ return getPermissionAndStageMedia(R.raw.lg_g4_iso_800_jpg,
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/jpeg", userId, isFavorite);
+ }
+
+ public static List<Uri> createDNGVideosAndGetUris(int count, int userId) throws Exception {
+ List<Uri> uriList = new ArrayList<>();
for (int i = 0; i < count; i++) {
final Uri uri = createDNGVideo(userId);
uriList.add(uri);
@@ -67,10 +100,12 @@
}
// Wait for Picker db sync to complete
MediaStore.waitForIdle(InstrumentationRegistry.getContext().getContentResolver());
+
+ return uriList;
}
- public static void createVideos(int count, int userId, List<Uri> uriList)
- throws Exception {
+ public static List<Uri> createVideosAndGetUris(int count, int userId) throws Exception {
+ List<Uri> uriList = new ArrayList<>();
for (int i = 0; i < count; i++) {
final Uri uri = createVideo(userId);
uriList.add(uri);
@@ -78,6 +113,8 @@
}
// Wait for Picker db sync to complete
MediaStore.waitForIdle(InstrumentationRegistry.getContext().getContentResolver());
+
+ return uriList;
}
public static void deleteMedia(Uri uri, Context context) throws Exception {
@@ -95,26 +132,19 @@
}
private static Uri createDNGVideo(int userId) throws Exception {
- final Uri uri = stageMedia(R.raw.test_video_dng,
- MediaStore.Video.Media.EXTERNAL_CONTENT_URI, "video/mp4", userId);
- return uri;
+ return getPermissionAndStageMedia(R.raw.test_video_dng,
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI, "video/mp4", userId,
+ /* isFavorite */ false).first;
}
private static Uri createVideo(int userId) throws Exception {
- final Uri uri = stageMedia(R.raw.test_video,
- MediaStore.Video.Media.EXTERNAL_CONTENT_URI, "video/mp4", userId);
- return uri;
+ return getPermissionAndStageMedia(R.raw.test_video,
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI, "video/mp4", userId,
+ /* isFavorite */ false).first;
}
- private static Uri createImage(int userId, boolean isFavorite) throws Exception {
- final Uri uri = stageMedia(R.raw.lg_g4_iso_800_jpg,
- MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/jpeg", userId, isFavorite);
- return uri;
- }
-
- private static Uri stageMedia(int resId, Uri collectionUri, String mimeType, int userId,
- boolean isFavorite) throws
- Exception {
+ private static Pair<Uri, String> getPermissionAndStageMedia(int resId, Uri collectionUri,
+ String mimeType, int userId, boolean isFavorite) throws Exception {
UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
uiAutomation.adoptShellPermissionIdentity(
android.Manifest.permission.INTERACT_ACROSS_USERS,
@@ -130,14 +160,8 @@
}
}
- private static Uri stageMedia(int resId, Uri collectionUri, String mimeType, int userId) throws
- Exception {
- return stageMedia(resId, collectionUri, mimeType, userId, false);
- }
-
- private static Uri stageMedia(int resId, Uri collectionUri, String mimeType, Context context,
- boolean isFavorite)
- throws IOException {
+ private static Pair<Uri, String> stageMedia(int resId, Uri collectionUri, String mimeType,
+ Context context, boolean isFavorite) throws IOException {
final String displayName = DISPLAY_NAME_PREFIX + System.nanoTime();
final MediaStoreUtils.PendingParams params = new MediaStoreUtils.PendingParams(
collectionUri, displayName, mimeType);
@@ -149,7 +173,7 @@
OutputStream target = session.openOutputStream()) {
FileUtils.copy(source, target);
}
- return session.publish();
+ return new Pair(session.publish(), displayName);
}
}
}
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerUiUtils.java b/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerUiUtils.java
index d20dcd6..8f58f3e 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerUiUtils.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerUiUtils.java
@@ -20,8 +20,8 @@
import android.text.format.DateUtils;
+import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject;
-import androidx.test.uiautomator.UiObjectNotFoundException;
import androidx.test.uiautomator.UiScrollable;
import androidx.test.uiautomator.UiSelector;
@@ -35,6 +35,7 @@
public static final long SHORT_TIMEOUT = 5 * DateUtils.SECOND_IN_MILLIS;
private static final long TIMEOUT = 30 * DateUtils.SECOND_IN_MILLIS;
+
public static final String REGEX_PACKAGE_NAME =
"com(.google)?.android.providers.media(.module)?";
@@ -92,4 +93,22 @@
return new UiObject(new UiSelector().resourceIdMatches(
REGEX_PACKAGE_NAME + ":id/profile_button"));
}
+
+ public static void findAndClickBrowse(UiDevice uiDevice) throws Exception {
+ assertWithMessage("Timed out waiting for overflow menu to appear")
+ .that(new UiObject(new UiSelector().description("More options"))
+ .waitForExists(SHORT_TIMEOUT))
+ .isTrue();
+
+ final UiObject overflowMenu = new UiObject(new UiSelector().description("More options"));
+ clickAndWait(uiDevice, overflowMenu);
+
+ final UiObject browseButton = new UiObject(new UiSelector().textContains("Browse"));
+ clickAndWait(uiDevice, browseButton);
+ }
+
+ public static void clickAndWait(UiDevice uiDevice, UiObject uiObject) throws Exception {
+ uiObject.click();
+ uiDevice.waitForIdle();
+ }
}
diff --git a/tests/appsearch/src/com/android/cts/appsearch/external/app/AppSearchSessionCtsTestBase.java b/tests/appsearch/src/com/android/cts/appsearch/external/app/AppSearchSessionCtsTestBase.java
index 70b68b4..affdaa8 100644
--- a/tests/appsearch/src/com/android/cts/appsearch/external/app/AppSearchSessionCtsTestBase.java
+++ b/tests/appsearch/src/com/android/cts/appsearch/external/app/AppSearchSessionCtsTestBase.java
@@ -676,6 +676,7 @@
.build())
.build();
mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
// Creates a large batch of Documents, since we have max document size in Framework which is
// 512KiB, we will create 1KiB * 4000 docs = 4MiB total size > 1MiB binder transaction limit
char[] chars = new char[1024]; // 1KiB
@@ -3851,4 +3852,104 @@
documents = convertSearchResultsToDocuments(searchResults);
assertThat(documents).containsExactly(inEmail1);
}
+
+ @Test
+ public void testSetSchemaWithIncompatibleNestedSchema() throws Exception {
+ // 1. Set the original schema. This should succeed without any problems.
+ AppSearchSchema originalNestedSchema =
+ new AppSearchSchema.Builder("TypeA")
+ .addProperty(
+ new StringPropertyConfig.Builder("prop1")
+ .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+ .build())
+ .build();
+ SetSchemaRequest originalRequest =
+ new SetSchemaRequest.Builder().addSchemas(originalNestedSchema).build();
+ mDb1.setSchemaAsync(originalRequest).get();
+
+ // 2. Set a new schema with a new type that refers to "TypeA" and an incompatible change to
+ // "TypeA". This should fail.
+ AppSearchSchema newNestedSchema =
+ new AppSearchSchema.Builder("TypeA")
+ .addProperty(
+ new StringPropertyConfig.Builder("prop1")
+ .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+ .build())
+ .build();
+ AppSearchSchema newSchema =
+ new AppSearchSchema.Builder("TypeB")
+ .addProperty(
+ new AppSearchSchema.DocumentPropertyConfig.Builder("prop2", "TypeA")
+ .build())
+ .build();
+ final SetSchemaRequest newRequest =
+ new SetSchemaRequest.Builder().addSchemas(newNestedSchema, newSchema).build();
+ Throwable throwable =
+ assertThrows(ExecutionException.class, () -> mDb1.setSchemaAsync(newRequest).get())
+ .getCause();
+ assertThat(throwable).isInstanceOf(AppSearchException.class);
+ AppSearchException exception = (AppSearchException) throwable;
+ assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_SCHEMA);
+ assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
+ assertThat(exception).hasMessageThat().contains("Incompatible types: {TypeA}");
+
+ // 3. Now set that same set of schemas but with forceOverride=true. This should succeed.
+ SetSchemaRequest newRequestForced =
+ new SetSchemaRequest.Builder()
+ .addSchemas(newNestedSchema, newSchema)
+ .setForceOverride(true)
+ .build();
+ mDb1.setSchemaAsync(newRequestForced).get();
+ }
+
+ @Test
+ public void testEmojiSnippet() throws Exception {
+ // Schema registration
+ mDb1.setSchemaAsync(
+ new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+ .get();
+
+ // String: "Luca Brasi sleeps with the 🐟🐟🐟."
+ // ^ ^ ^ ^ ^ ^ ^ ^ ^
+ // UTF8 idx: 0 5 11 18 23 27 3135 39
+ // UTF16 idx: 0 5 11 18 23 27 2931 33
+ // Breaks into segments: "Luca", "Brasi", "sleeps", "with", "the", "🐟", "🐟"
+ // and "🐟".
+ // Index a document to instance 1.
+ String sicilianMessage = "Luca Brasi sleeps with the 🐟🐟🐟.";
+ AppSearchEmail inEmail1 =
+ new AppSearchEmail.Builder("namespace", "uri1").setBody(sicilianMessage).build();
+ checkIsBatchResultSuccess(
+ mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
+
+ AppSearchEmail inEmail2 =
+ new AppSearchEmail.Builder("namespace", "uri2")
+ .setBody("Some other content.")
+ .build();
+ checkIsBatchResultSuccess(
+ mDb1.putAsync(
+ new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
+
+ // Query for "🐟"
+ SearchResultsShim searchResults =
+ mDb1.search(
+ "🐟",
+ new SearchSpec.Builder()
+ .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+ .setSnippetCount(1)
+ .setSnippetCountPerProperty(1)
+ .build());
+ List<SearchResult> page = searchResults.getNextPageAsync().get();
+ assertThat(page).hasSize(1);
+ assertThat(page.get(0).getGenericDocument()).isEqualTo(inEmail1);
+ List<SearchResult.MatchInfo> matches = page.get(0).getMatchInfos();
+ assertThat(matches).hasSize(1);
+ assertThat(matches.get(0).getPropertyPath()).isEqualTo("body");
+ assertThat(matches.get(0).getFullText()).isEqualTo(sicilianMessage);
+ assertThat(matches.get(0).getExactMatch()).isEqualTo("🐟");
+ if (mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)) {
+ assertThat(matches.get(0).getSubmatch()).isEqualTo("🐟");
+ }
+ }
}
diff --git a/tests/appsearch/src/com/android/cts/appsearch/external/app/GenericDocumentCtsTest.java b/tests/appsearch/src/com/android/cts/appsearch/external/app/GenericDocumentCtsTest.java
index 5dd2eac..0bfa693 100644
--- a/tests/appsearch/src/com/android/cts/appsearch/external/app/GenericDocumentCtsTest.java
+++ b/tests/appsearch/src/com/android/cts/appsearch/external/app/GenericDocumentCtsTest.java
@@ -576,6 +576,30 @@
}
@Test
+ public void testNestedProperties_buildBlankPaths() {
+ Exception e =
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ new GenericDocument.Builder<>("namespace", "id1", "schema1")
+ .setPropertyString("", "foo"));
+ assertThat(e.getMessage()).isEqualTo("Property name cannot be blank.");
+
+ e =
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ new GenericDocument.Builder<>("namespace", "id1", "schema1")
+ .setPropertyDocument(
+ "propDoc",
+ new GenericDocument.Builder<>(
+ "namespace", "id2", "schema1")
+ .setPropertyString("", "Bat", "Hawk")
+ .build()));
+ assertThat(e.getMessage()).isEqualTo("Property name cannot be blank.");
+ }
+
+ @Test
public void testNestedProperties_invalidPaths() {
GenericDocument doc =
new GenericDocument.Builder<>("namespace", "id1", "schema1")
@@ -592,25 +616,28 @@
.build())
.build();
- // Some paths are invalid because they don't apply to the given document --- these should
+ // These paths are invalid because they don't apply to the given document --- these should
// return null. It's not the querier's fault.
assertThat(doc.getPropertyStringArray("propString.propInts")).isNull();
assertThat(doc.getPropertyStringArray("propDocs.propFoo")).isNull();
assertThat(doc.getPropertyStringArray("propDocs.propNestedString.propFoo")).isNull();
+ }
- // Some paths are invalid because they are malformed. These throw an exception --- the
- // querier shouldn't provide such paths.
- assertThrows(
- IllegalArgumentException.class, () -> doc.getPropertyStringArray("propString[0"));
- assertThrows(
- IllegalArgumentException.class, () -> doc.getPropertyStringArray("propString[0.]"));
- assertThrows(
- IllegalArgumentException.class,
- () -> doc.getPropertyStringArray("propString[banana]"));
- assertThrows(
- IllegalArgumentException.class, () -> doc.getPropertyStringArray("propString[-1]"));
- assertThrows(
- IllegalArgumentException.class, () -> doc.getPropertyStringArray("propDocs[0]cat"));
+ @Test
+ public void testNestedProperties_arrayTypesInvalidPath() {
+ GenericDocument doc = new GenericDocument.Builder<>("namespace", "id1", "schema1").build();
+ assertThrows(IllegalArgumentException.class, () -> doc.getPropertyString("."));
+ assertThrows(IllegalArgumentException.class, () -> doc.getPropertyDocument("."));
+ assertThrows(IllegalArgumentException.class, () -> doc.getPropertyBoolean("."));
+ assertThrows(IllegalArgumentException.class, () -> doc.getPropertyDouble("."));
+ assertThrows(IllegalArgumentException.class, () -> doc.getPropertyLong("."));
+ assertThrows(IllegalArgumentException.class, () -> doc.getPropertyBytes("."));
+ assertThrows(IllegalArgumentException.class, () -> doc.getPropertyStringArray("."));
+ assertThrows(IllegalArgumentException.class, () -> doc.getPropertyDocumentArray("."));
+ assertThrows(IllegalArgumentException.class, () -> doc.getPropertyBooleanArray("."));
+ assertThrows(IllegalArgumentException.class, () -> doc.getPropertyDoubleArray("."));
+ assertThrows(IllegalArgumentException.class, () -> doc.getPropertyLongArray("."));
+ assertThrows(IllegalArgumentException.class, () -> doc.getPropertyBytesArray("."));
}
@Test
diff --git a/tests/appsearch/src/com/android/cts/appsearch/external/app/GlobalSearchSessionCtsTestBase.java b/tests/appsearch/src/com/android/cts/appsearch/external/app/GlobalSearchSessionCtsTestBase.java
index 71822ca..47337ca 100644
--- a/tests/appsearch/src/com/android/cts/appsearch/external/app/GlobalSearchSessionCtsTestBase.java
+++ b/tests/appsearch/src/com/android/cts/appsearch/external/app/GlobalSearchSessionCtsTestBase.java
@@ -1641,6 +1641,11 @@
@Test
public void testAddObserver_schemaChange_added() throws Exception {
+ assumeTrue(
+ mDb1.getFeatures()
+ .isFeatureSupported(
+ Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
+
// Register an observer
TestObserverCallback observer = new TestObserverCallback();
mGlobalSearchSession.registerObserverCallback(
@@ -1688,6 +1693,11 @@
@Test
public void testAddObserver_schemaChange_removed() throws Exception {
+ assumeTrue(
+ mDb1.getFeatures()
+ .isFeatureSupported(
+ Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
+
// Add a schema type
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
@@ -1723,6 +1733,11 @@
@Test
public void testAddObserver_schemaChange_contents() throws Exception {
+ assumeTrue(
+ mDb1.getFeatures()
+ .isFeatureSupported(
+ Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
+
// Add a schema
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
@@ -1794,6 +1809,11 @@
@Test
public void testAddObserver_schemaChange_contents_skipBySpec() throws Exception {
+ assumeTrue(
+ mDb1.getFeatures()
+ .isFeatureSupported(
+ Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
+
// Add a schema
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
@@ -1862,6 +1882,11 @@
@Test
public void testRegisterObserver_schemaMigration() throws Exception {
+ assumeTrue(
+ mDb1.getFeatures()
+ .isFeatureSupported(
+ Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
+
// Add a schema with two types
mDb1.setSchemaAsync(
new SetSchemaRequest.Builder()
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsAnimationSynchronicityTests.java b/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsAnimationSynchronicityTests.java
index 51033bb..bb83db1 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsAnimationSynchronicityTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsAnimationSynchronicityTests.java
@@ -64,7 +64,6 @@
import com.android.compatibility.common.util.SystemUtil;
import com.android.compatibility.common.util.WindowUtil;
-import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
@@ -84,13 +83,11 @@
private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
- @Ignore("b/168446060")
@Test
public void testShowAndHide_renderSynchronouslyBetweenImeWindowAndAppContent() throws Throwable {
runTest(false /* useControlApi */);
}
- @Ignore("b/168446060")
@Test
public void testControl_rendersSynchronouslyBetweenImeWindowAndAppContent() throws Throwable {
runTest(true /* useControlApi */);
diff --git a/tests/media/src/android/mediav2/cts/CodecInfoTest.java b/tests/media/src/android/mediav2/cts/CodecInfoTest.java
index c3a1b09..e38094c 100644
--- a/tests/media/src/android/mediav2/cts/CodecInfoTest.java
+++ b/tests/media/src/android/mediav2/cts/CodecInfoTest.java
@@ -171,7 +171,8 @@
// For devices launching with Android T, if a codec supports an HDR profile and device
// supports HDR display, it must advertise P010 support
int[] HdrProfileArray = mProfileHdrMap.get(mMediaType);
- if (VNDK_IS_AT_LEAST_T && HdrProfileArray != null && DISPLAY_HDR_TYPES.length > 0) {
+ if (FIRST_SDK_IS_AT_LEAST_T && VNDK_IS_AT_LEAST_T
+ && HdrProfileArray != null && DISPLAY_HDR_TYPES.length > 0) {
for (CodecProfileLevel pl : caps.profileLevels) {
if (IntStream.of(HdrProfileArray).anyMatch(x -> x == pl.profile)) {
assertFalse(mCodecInfo.getName() + " supports HDR profile " + pl.profile + "," +
diff --git a/tests/media/src/android/mediav2/cts/CodecTestBase.java b/tests/media/src/android/mediav2/cts/CodecTestBase.java
index 6065c5c..9d1d839 100644
--- a/tests/media/src/android/mediav2/cts/CodecTestBase.java
+++ b/tests/media/src/android/mediav2/cts/CodecTestBase.java
@@ -30,7 +30,9 @@
import android.media.MediaCodecList;
import android.media.MediaExtractor;
import android.media.MediaFormat;
+import android.media.MediaMuxer;
import android.os.Build;
+import android.os.Bundle;
import android.os.PersistableBundle;
import android.os.SystemProperties;
import android.util.Log;
@@ -590,17 +592,12 @@
abstract class CodecTestBase {
public static final boolean IS_Q = ApiLevelUtil.getApiLevel() == Build.VERSION_CODES.Q;
public static final boolean IS_AT_LEAST_R = ApiLevelUtil.isAtLeast(Build.VERSION_CODES.R);
- // Checking for CODENAME helps in cases when build version on the development branch isn't
- // updated yet but CODENAME is updated.
public static final boolean IS_AT_LEAST_T =
- ApiLevelUtil.isAtLeast(Build.VERSION_CODES.TIRAMISU) ||
- ApiLevelUtil.codenameEquals("Tiramisu");
- // TODO (b/223868241) Update the following to check for Build.VERSION_CODES.TIRAMISU once
- // TIRAMISU is set correctly
+ ApiLevelUtil.isAtLeast(Build.VERSION_CODES.TIRAMISU);
public static final boolean FIRST_SDK_IS_AT_LEAST_T =
- ApiLevelUtil.isFirstApiAfter(Build.VERSION_CODES.S_V2);
+ ApiLevelUtil.isFirstApiAtLeast(Build.VERSION_CODES.TIRAMISU);
public static final boolean VNDK_IS_AT_LEAST_T =
- SystemProperties.getInt("ro.vndk.version", 0) > Build.VERSION_CODES.S_V2;
+ SystemProperties.getInt("ro.vndk.version", 0) >= Build.VERSION_CODES.TIRAMISU;
public static final boolean IS_HDR_EDITING_SUPPORTED = isHDREditingSupported();
private static final String LOG_TAG = CodecTestBase.class.getSimpleName();
enum SupportClass {
@@ -609,30 +606,14 @@
CODEC_DEFAULT, // Default codec must support
CODEC_OPTIONAL // Codec support is optional
}
+
+ static final ArrayList<String> HDR_INFO_IN_BITSTREAM_CODECS = new ArrayList<>();
static final String HDR_STATIC_INFO =
- "00 d0 84 80 3e c2 33 c4 86 4c 1d b8 0b 13 3d 42 40 e8 03 64 00 e8 03 2c 01";
- static final String[] HDR_DYNAMIC_INFO = new String[]{
- "b5 00 3c 00 01 04 00 40 00 0c 80 4e 20 27 10 00" +
- "0a 00 00 24 08 00 00 28 00 00 50 00 28 c8 00 c9" +
- "90 02 aa 58 05 ca d0 0c 0a f8 16 83 18 9c 18 00" +
- "40 78 13 64 d5 7c 2e 2c c3 59 de 79 6e c3 c2 00",
-
- "b5 00 3c 00 01 04 00 40 00 0c 80 4e 20 27 10 00" +
- "0a 00 00 24 08 00 00 28 00 00 50 00 28 c8 00 c9" +
- "90 02 aa 58 05 ca d0 0c 0a f8 16 83 18 9c 18 00" +
- "40 78 13 64 d5 7c 2e 2c c3 59 de 79 6e c3 c2 00",
-
- "b5 00 3c 00 01 04 00 40 00 0c 80 4e 20 27 10 00" +
- "0e 80 00 24 08 00 00 28 00 00 50 00 28 c8 00 c9" +
- "90 02 aa 58 05 ca d0 0c 0a f8 16 83 18 9c 18 00" +
- "40 78 13 64 d5 7c 2e 2c c3 59 de 79 6e c3 c2 00",
-
- "b5 00 3c 00 01 04 00 40 00 0c 80 4e 20 27 10 00" +
- "0e 80 00 24 08 00 00 28 00 00 50 00 28 c8 00 c9" +
- "90 02 aa 58 05 ca d0 0c 0a f8 16 83 18 9c 18 00" +
- "40 78 13 64 d5 7c 2e 2c c3 59 de 79 6e c3 c2 00",
- };
- boolean mTestDynamicMetadata = false;
+ "00 d0 84 80 3e c2 33 c4 86 4c 1d b8 0b 13 3d 42 40 a0 0f 32 00 10 27 df 0d";
+ static final String HDR_STATIC_INCORRECT_INFO =
+ "00 d0 84 80 3e c2 33 c4 86 10 27 d0 07 13 3d 42 40 a0 0f 32 00 10 27 df 0d";
+ static final HashMap<Integer, String> HDR_DYNAMIC_INFO = new HashMap<>();
+ static final HashMap<Integer, String> HDR_DYNAMIC_INCORRECT_INFO = new HashMap<>();
static final String CODEC_PREFIX_KEY = "codec-prefix";
static final String MEDIA_TYPE_PREFIX_KEY = "media-type-prefix";
static final String MIME_SEL_KEY = "mime-sel";
@@ -788,6 +769,45 @@
mProfileMap.put(MediaFormat.MIMETYPE_VIDEO_VP9, VP9_PROFILES);
mProfileMap.put(MediaFormat.MIMETYPE_VIDEO_AV1, AV1_PROFILES);
mProfileMap.put(MediaFormat.MIMETYPE_AUDIO_AAC, AAC_PROFILES);
+
+ HDR_INFO_IN_BITSTREAM_CODECS.add(MediaFormat.MIMETYPE_VIDEO_AV1);
+ HDR_INFO_IN_BITSTREAM_CODECS.add(MediaFormat.MIMETYPE_VIDEO_AVC);
+ HDR_INFO_IN_BITSTREAM_CODECS.add(MediaFormat.MIMETYPE_VIDEO_HEVC);
+
+ HDR_DYNAMIC_INFO.put(0, "b5 00 3c 00 01 04 00 40 00 0c 80 4e 20 27 10 00" +
+ "0a 00 00 24 08 00 00 28 00 00 50 00 28 c8 00 c9" +
+ "90 02 aa 58 05 ca d0 0c 0a f8 16 83 18 9c 18 00" +
+ "40 78 13 64 d5 7c 2e 2c c3 59 de 79 6e c3 c2 00");
+ HDR_DYNAMIC_INFO.put(4, "b5 00 3c 00 01 04 00 40 00 0c 80 4e 20 27 10 00" +
+ "0a 00 00 24 08 00 00 28 00 00 50 00 28 c8 00 c9" +
+ "90 02 aa 58 05 ca d0 0c 0a f8 16 83 18 9c 18 00" +
+ "40 78 13 64 d5 7c 2e 2c c3 59 de 79 6e c3 c2 00");
+ HDR_DYNAMIC_INFO.put(12, "b5 00 3c 00 01 04 00 40 00 0c 80 4e 20 27 10 00" +
+ "0e 80 00 24 08 00 00 28 00 00 50 00 28 c8 00 c9" +
+ "90 02 aa 58 05 ca d0 0c 0a f8 16 83 18 9c 18 00" +
+ "40 78 13 64 d5 7c 2e 2c c3 59 de 79 6e c3 c2 00");
+ HDR_DYNAMIC_INFO.put(22, "b5 00 3c 00 01 04 00 40 00 0c 80 4e 20 27 10 00" +
+ "0e 80 00 24 08 00 00 28 00 00 50 00 28 c8 00 c9" +
+ "90 02 aa 58 05 ca d0 0c 0a f8 16 83 18 9c 18 00" +
+ "40 78 13 64 d5 7c 2e 2c c3 59 de 79 6e c3 c2 00");
+
+
+ HDR_DYNAMIC_INCORRECT_INFO.put(0, "b5 00 3c 00 01 04 00 40 00 0c 80 4e 20 27 10 00" +
+ "0a 00 00 24 08 00 00 28 00 00 50 00 28 c8 00 c9" +
+ "90 02 aa 58 05 ca d0 0c 0a f8 16 83 18 9c 18 00" +
+ "40 78 13 64 d5 7c 2e 2c c3 59 de 79 6e c3 c2 00");
+ HDR_DYNAMIC_INCORRECT_INFO.put(4, "b5 00 3c 00 01 04 00 40 00 0c 80 4e 20 27 10 00" +
+ "0a 00 00 24 08 00 00 28 00 00 50 00 28 c8 00 c9" +
+ "90 02 aa 58 05 ca d0 0c 0a f8 16 83 18 9c 18 00" +
+ "40 78 13 64 d5 7c 2e 2c c3 59 de 79 6e c3 c2 01");
+ HDR_DYNAMIC_INCORRECT_INFO.put(12, "b5 00 3c 00 01 04 00 40 00 0c 80 4e 20 27 10 00" +
+ "0e 80 00 24 08 00 00 28 00 00 50 00 28 c8 00 c9" +
+ "90 02 aa 58 05 ca d0 0c 0a f8 16 83 18 9c 18 00" +
+ "40 78 13 64 d5 7c 2e 2c c3 59 de 79 6e c3 c2 02");
+ HDR_DYNAMIC_INCORRECT_INFO.put(22, "b5 00 3c 00 01 04 00 40 00 0c 80 4e 20 27 10 00" +
+ "0e 80 00 24 08 00 00 28 00 00 50 00 28 c8 00 c9" +
+ "90 02 aa 58 05 ca d0 0c 0a f8 16 83 18 9c 18 00" +
+ "40 78 13 64 d5 7c 2e 2c c3 59 de 79 6e c3 c2 03");
}
static int[] combine(int[] first, int[] second) {
@@ -1366,6 +1386,12 @@
return Arrays.copyOfRange(tempArray, 0, i);
}
+ void insertHdrDynamicInfo(byte[] info) {
+ final Bundle params = new Bundle();
+ params.putByteArray(MediaFormat.KEY_HDR10_PLUS_INFO, info);
+ mCodec.setParameters(params);
+ }
+
boolean isFormatSimilar(MediaFormat inpFormat, MediaFormat outFormat) {
if (inpFormat == null || outFormat == null) return false;
String inpMime = inpFormat.getString(MediaFormat.KEY_MIME);
@@ -1424,41 +1450,26 @@
}
}
- void validateHDRStaticMetaData(MediaFormat fmt, ByteBuffer hdrStaticRef) {
- ByteBuffer hdrStaticInfo = fmt.getByteBuffer(MediaFormat.KEY_HDR_STATIC_INFO, null);
- assertNotNull("No HDR static metadata present in format : " + fmt, hdrStaticInfo);
- if (!hdrStaticRef.equals(hdrStaticInfo)) {
+ void validateHDRInfo(MediaFormat fmt, String hdrInfoKey, ByteBuffer hdrInfoRef) {
+ ByteBuffer hdrInfo = fmt.getByteBuffer(hdrInfoKey, null);
+ assertNotNull("No " + hdrInfoKey + " present in format : " + fmt, hdrInfo);
+ if (!hdrInfoRef.equals(hdrInfo)) {
StringBuilder refString = new StringBuilder("");
StringBuilder testString = new StringBuilder("");
- byte[] ref = new byte[hdrStaticRef.capacity()];
- hdrStaticRef.get(ref);
- byte[] test = new byte[hdrStaticInfo.capacity()];
- hdrStaticInfo.get(test);
- for (int i = 0; i < Math.min(ref.length, test.length); i++) {
+ byte[] ref = new byte[hdrInfoRef.capacity()];
+ hdrInfoRef.get(ref);
+ hdrInfoRef.rewind();
+ byte[] test = new byte[hdrInfo.capacity()];
+ hdrInfo.get(test);
+ hdrInfo.rewind();
+ for (int i = 0; i < ref.length; i++) {
refString.append(String.format("%2x ", ref[i]));
+ }
+ for (int i = 0; i < test.length; i++) {
testString.append(String.format("%2x ", test[i]));
}
- fail("hdr static info mismatch" + "\n" + "ref static info : " + refString + "\n" +
- "test static info : " + testString);
- }
- }
-
- void validateHDRDynamicMetaData(MediaFormat fmt, ByteBuffer hdrDynamicRef) {
- ByteBuffer hdrDynamicInfo = fmt.getByteBuffer(MediaFormat.KEY_HDR10_PLUS_INFO, null);
- assertNotNull("No HDR dynamic metadata present in format : " + fmt, hdrDynamicInfo);
- if (!hdrDynamicRef.equals(hdrDynamicInfo)) {
- StringBuilder refString = new StringBuilder("");
- StringBuilder testString = new StringBuilder("");
- byte[] ref = new byte[hdrDynamicRef.capacity()];
- hdrDynamicRef.get(ref);
- byte[] test = new byte[hdrDynamicInfo.capacity()];
- hdrDynamicInfo.get(test);
- for (int i = 0; i < Math.min(ref.length, test.length); i++) {
- refString.append(String.format("%2x ", ref[i]));
- testString.append(String.format("%2x ", test[i]));
- }
- fail("hdr dynamic info mismatch" + "\n" + "ref dynamic info : " + refString + "\n" +
- "test dynamic info : " + testString);
+ fail(hdrInfoKey + " mismatch in codec " + mCodecName + "\nref info : " + refString +
+ "\n test info : " + testString);
}
}
@@ -1680,15 +1691,8 @@
int height = format.getInteger(MediaFormat.KEY_HEIGHT);
int stride = format.getInteger(MediaFormat.KEY_STRIDE);
mOutputBuff.checksum(buf, info.size, width, height, stride, bytesPerSample);
-
- if (mTestDynamicMetadata) {
- validateHDRDynamicMetaData(mCodec.getOutputFormat(), ByteBuffer
- .wrap(loadByteArrayFromString(HDR_DYNAMIC_INFO[mOutputCount])));
-
- }
}
}
-
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
mSawOutputEOS = true;
}
@@ -1808,57 +1812,18 @@
mCodec.release();
mExtractor.release();
}
-
- void validateHDRStaticMetaData(String parent, String name, ByteBuffer HDRStatic,
- boolean ignoreContainerStaticInfo)
- throws IOException, InterruptedException {
- mOutputBuff = new OutputManager();
- MediaFormat format = setUpSource(parent, name);
- if (ignoreContainerStaticInfo) {
- format.removeKey(MediaFormat.KEY_HDR_STATIC_INFO);
- }
- mCodec = MediaCodec.createByCodecName(mCodecName);
- configureCodec(format, true, true, false);
- mCodec.start();
- doWork(10);
- queueEOS();
- waitForAllOutputs();
- validateHDRStaticMetaData(mCodec.getOutputFormat(), HDRStatic);
- mCodec.stop();
- mCodec.release();
- mExtractor.release();
- }
-
- void validateHDRDynamicMetaData(String parent, String name, boolean ignoreContainerDynamicInfo)
- throws IOException, InterruptedException {
- mOutputBuff = new OutputManager();
- MediaFormat format = setUpSource(parent, name);
- if (ignoreContainerDynamicInfo) {
- format.removeKey(MediaFormat.KEY_HDR10_PLUS_INFO);
- }
- mCodec = MediaCodec.createByCodecName(mCodecName);
- configureCodec(format, true, true, false);
- mCodec.start();
- doWork(10);
- queueEOS();
- waitForAllOutputs();
- mCodec.stop();
- mCodec.release();
- mExtractor.release();
- }
}
class CodecEncoderTestBase extends CodecTestBase {
private static final String LOG_TAG = CodecEncoderTestBase.class.getSimpleName();
// files are in WorkDir.getMediaDirString();
- private static final String INPUT_AUDIO_FILE = "bbb_2ch_44kHz_s16le.raw";
- private static final String INPUT_VIDEO_FILE = "bbb_cif_yuv420p_30fps.yuv";
+ protected static final String INPUT_AUDIO_FILE = "bbb_2ch_44kHz_s16le.raw";
+ protected static final String INPUT_VIDEO_FILE = "bbb_cif_yuv420p_30fps.yuv";
protected static final String INPUT_AUDIO_FILE_HBD = "audio/sd_2ch_48kHz_f32le.raw";
protected static final String INPUT_VIDEO_FILE_HBD = "cosmat_cif_24fps_yuv420p16le.yuv";
-
- private final int INP_FRM_WIDTH = 352;
- private final int INP_FRM_HEIGHT = 288;
+ protected final int INP_FRM_WIDTH = 352;
+ protected final int INP_FRM_HEIGHT = 288;
final String mMime;
final int[] mBitrates;
@@ -2217,3 +2182,241 @@
return cdtb.mOutputBuff.getBuffer();
}
}
+
+class HDRDecoderTestBase extends CodecDecoderTestBase {
+ private static final String LOG_TAG = HDRDecoderTestBase.class.getSimpleName();
+
+ private ByteBuffer mHdrStaticInfoRef;
+ private ByteBuffer mHdrStaticInfoStream;
+ private ByteBuffer mHdrStaticInfoContainer;
+ private Map<Integer, String> mHdrDynamicInfoRef;
+ private Map<Integer, String> mHdrDynamicInfoStream;
+ private Map<Integer, String> mHdrDynamicInfoContainer;
+ private String mHdrDynamicInfoCurrent;
+
+ public HDRDecoderTestBase(String decoder, String mime, String testFile) {
+ super(decoder, mime, testFile);
+ }
+
+ void enqueueInput(int bufferIndex) {
+ if (mHdrDynamicInfoContainer != null && mHdrDynamicInfoContainer.containsKey(mInputCount) &&
+ mExtractor.getSampleSize() != -1) {
+ insertHdrDynamicInfo(
+ loadByteArrayFromString(mHdrDynamicInfoContainer.get(mInputCount)));
+ }
+ super.enqueueInput(bufferIndex);
+ }
+
+ void dequeueOutput(int bufferIndex, MediaCodec.BufferInfo info) {
+ if (info.size > 0 && mHdrDynamicInfoRef != null) {
+ MediaFormat format = mCodec.getOutputFormat(bufferIndex);
+ if (mHdrDynamicInfoRef.containsKey(mOutputCount)) {
+ mHdrDynamicInfoCurrent = mHdrDynamicInfoRef.get(mOutputCount);
+ }
+ validateHDRInfo(format, MediaFormat.KEY_HDR10_PLUS_INFO,
+ ByteBuffer.wrap(loadByteArrayFromString(mHdrDynamicInfoCurrent)));
+ }
+ super.dequeueOutput(bufferIndex, info);
+ }
+
+ void validateHDRInfo(String hdrStaticInfoStream, String hdrStaticInfoContainer,
+ Map<Integer, String> hdrDynamicInfoStream,
+ Map<Integer, String> hdrDynamicInfoContainer) throws IOException,
+ InterruptedException {
+ mHdrStaticInfoStream = hdrStaticInfoStream != null ?
+ ByteBuffer.wrap(loadByteArrayFromString(hdrStaticInfoStream)) : null;
+ mHdrStaticInfoContainer = hdrStaticInfoContainer != null ?
+ ByteBuffer.wrap(loadByteArrayFromString(hdrStaticInfoContainer)) : null;
+ mHdrStaticInfoRef = mHdrStaticInfoStream == null ? mHdrStaticInfoContainer :
+ mHdrStaticInfoStream;
+ mHdrDynamicInfoStream = hdrDynamicInfoStream;
+ mHdrDynamicInfoContainer = hdrDynamicInfoContainer;
+ mHdrDynamicInfoRef = hdrDynamicInfoStream == null ? hdrDynamicInfoContainer :
+ hdrDynamicInfoStream;
+
+ assertTrue("reference hdr10/hdr10+ info is not supplied for validation",
+ mHdrDynamicInfoRef != null || mHdrStaticInfoRef != null);
+
+ if (mHdrDynamicInfoStream != null || mHdrDynamicInfoContainer != null) {
+ Assume.assumeNotNull("Test is only applicable to codecs that have HDR10+ profiles",
+ mProfileHdr10PlusMap.get(mMime));
+ }
+ if (mHdrStaticInfoStream != null || mHdrStaticInfoContainer != null) {
+ Assume.assumeNotNull("Test is only applicable to codecs that have HDR10 profiles",
+ mProfileHdr10Map.get(mMime));
+ }
+
+ File fObj = new File(mTestFile);
+ String parent = fObj.getParent();
+ if (parent != null) parent += File.separator;
+ else parent = mInpPrefix;
+ Preconditions.assertTestFileExists(parent + fObj.getName());
+ // For decoders, if you intend to supply hdr10+ info using external means like json, make
+ // sure that info that is being supplied is in sync with SEI info
+ if (mHdrDynamicInfoStream != null && mHdrDynamicInfoContainer != null) {
+ assertEquals("Container hdr10+ info size and elementary stream SEI hdr10+ " +
+ "info size are unequal", mHdrDynamicInfoStream.size(),
+ mHdrDynamicInfoContainer.size());
+ for (Map.Entry<Integer, String> element : mHdrDynamicInfoStream.entrySet()) {
+ assertTrue("Container hdr10+ info and elementary stream SEI hdr10+ " +
+ "info frame positions are not in sync",
+ mHdrDynamicInfoContainer.containsKey(element.getKey()));
+ }
+ }
+ mOutputBuff = new OutputManager();
+ MediaFormat format = setUpSource(parent, fObj.getName());
+ if (mHdrDynamicInfoStream != null || mHdrDynamicInfoContainer != null) {
+ format.setInteger(MediaFormat.KEY_PROFILE, mProfileHdr10PlusMap.get(mMime)[0]);
+ } else {
+ format.setInteger(MediaFormat.KEY_PROFILE, mProfileHdr10Map.get(mMime)[0]);
+ }
+ ArrayList<MediaFormat> formatList = new ArrayList<>();
+ formatList.add(format);
+ Assume.assumeTrue(mCodecName + " does not support HDR10/HDR10+ profile",
+ areFormatsSupported(mCodecName, mMime, formatList));
+ mCodec = MediaCodec.createByCodecName(mCodecName);
+ configureCodec(format, false, true, false);
+ mCodec.start();
+ doWork(Integer.MAX_VALUE);
+ queueEOS();
+ waitForAllOutputs();
+ if (mHdrStaticInfoRef != null) {
+ validateHDRInfo(mCodec.getOutputFormat(), MediaFormat.KEY_HDR_STATIC_INFO,
+ mHdrStaticInfoRef);
+ }
+ mCodec.stop();
+ mCodec.release();
+ mExtractor.release();
+ }
+}
+
+class HDREncoderTestBase extends CodecEncoderTestBase {
+ private static final String LOG_TAG = HDREncoderTestBase.class.getSimpleName();
+
+ private ByteBuffer mHdrStaticInfo;
+ private Map<Integer, String> mHdrDynamicInfo;
+
+ private MediaMuxer mMuxer;
+ private int mTrackID = -1;
+
+ public HDREncoderTestBase(String encoderName, String mediaType, int bitrate, int width,
+ int height) {
+ super(encoderName, mediaType, new int[]{bitrate}, new int[]{width}, new int[]{height});
+ }
+
+ void enqueueInput(int bufferIndex) {
+ if (mHdrDynamicInfo != null && mHdrDynamicInfo.containsKey(mInputCount)) {
+ insertHdrDynamicInfo(loadByteArrayFromString(mHdrDynamicInfo.get(mInputCount)));
+ }
+ super.enqueueInput(bufferIndex);
+ }
+
+ void dequeueOutput(int bufferIndex, MediaCodec.BufferInfo info) {
+ MediaFormat bufferFormat = mCodec.getOutputFormat(bufferIndex);
+ if (info.size > 0) {
+ ByteBuffer buf = mCodec.getOutputBuffer(bufferIndex);
+ if (mMuxer != null) {
+ if (mTrackID == -1) {
+ mTrackID = mMuxer.addTrack(bufferFormat);
+ mMuxer.start();
+ }
+ mMuxer.writeSampleData(mTrackID, buf, info);
+ }
+ }
+ super.dequeueOutput(bufferIndex, info);
+ }
+
+ void validateHDRInfo(String hdrStaticInfo, Map<Integer, String> hdrDynamicInfo)
+ throws IOException, InterruptedException {
+ mHdrStaticInfo = hdrStaticInfo != null ?
+ ByteBuffer.wrap(loadByteArrayFromString(hdrStaticInfo)) : null;
+ mHdrDynamicInfo = hdrDynamicInfo;
+
+ setUpParams(1);
+
+ MediaFormat format = mFormats.get(0);
+ format.setInteger(MediaFormat.KEY_COLOR_FORMAT, COLOR_FormatYUVP010);
+ format.setInteger(MediaFormat.KEY_COLOR_RANGE, MediaFormat.COLOR_RANGE_LIMITED);
+ format.setInteger(MediaFormat.KEY_COLOR_STANDARD, MediaFormat.COLOR_STANDARD_BT2020);
+ format.setInteger(MediaFormat.KEY_COLOR_TRANSFER, MediaFormat.COLOR_TRANSFER_ST2084);
+ int profile = (mHdrDynamicInfo != null) ? mProfileHdr10PlusMap.get(mMime)[0] :
+ mProfileHdr10Map.get(mMime)[0];
+ format.setInteger(MediaFormat.KEY_PROFILE, profile);
+
+ if (mHdrStaticInfo != null) {
+ format.setByteBuffer(MediaFormat.KEY_HDR_STATIC_INFO, mHdrStaticInfo);
+ }
+ Assume.assumeTrue(mCodecName + " does not support HDR10/HDR10+ profile " + profile,
+ areFormatsSupported(mCodecName, mMime, mFormats));
+ Assume.assumeTrue(mCodecName + " does not support color format COLOR_FormatYUVP010",
+ hasSupportForColorFormat(mCodecName, mMime, COLOR_FormatYUVP010));
+
+ mBytesPerSample = 2;
+ setUpSource(INPUT_VIDEO_FILE_HBD);
+
+ int frameLimit = 4;
+ if (mHdrDynamicInfo != null) {
+ Integer lastHdr10PlusFrame =
+ Collections.max(HDR_DYNAMIC_INFO.entrySet(), Map.Entry.comparingByKey())
+ .getKey();
+ frameLimit = lastHdr10PlusFrame + 10;
+ }
+ int maxNumFrames = mInputData.length / (INP_FRM_WIDTH * INP_FRM_HEIGHT * mBytesPerSample);
+ assertTrue("HDR info tests require input file with at least " + frameLimit +
+ " frames. " + INPUT_VIDEO_FILE_HBD + " has " + maxNumFrames + " frames.",
+ frameLimit <= maxNumFrames);
+
+ mOutputBuff = new OutputManager();
+ mCodec = MediaCodec.createByCodecName(mCodecName);
+ File tmpFile;
+ int muxerFormat;
+ if (mMime.equals(MediaFormat.MIMETYPE_VIDEO_VP9)) {
+ muxerFormat = MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM;
+ tmpFile = File.createTempFile("tmp10bit", ".webm");
+ } else {
+ muxerFormat = MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4;
+ tmpFile = File.createTempFile("tmp10bit", ".mp4");
+ }
+ mMuxer = new MediaMuxer(tmpFile.getAbsolutePath(), muxerFormat);
+ configureCodec(format, true, true, true);
+ mCodec.start();
+ doWork(frameLimit);
+ queueEOS();
+ waitForAllOutputs();
+ if (mTrackID != -1) {
+ mMuxer.stop();
+ mTrackID = -1;
+ }
+ if (mMuxer != null) {
+ mMuxer.release();
+ mMuxer = null;
+ }
+ String log = String.format("format: %s \n codec: %s:: ", format, mCodecName);
+ assertTrue(log + "unexpected error", !mAsyncHandle.hasSeenError());
+ assertTrue(log + "no input sent", 0 != mInputCount);
+ assertTrue(log + "output received", 0 != mOutputCount);
+
+ MediaFormat fmt = mCodec.getOutputFormat();
+
+ mCodec.stop();
+ mCodec.release();
+ if (mHdrStaticInfo != null) {
+ // verify if the out fmt contains HDR Static info as expected
+ validateHDRInfo(fmt, MediaFormat.KEY_HDR_STATIC_INFO, mHdrStaticInfo);
+ }
+
+ // verify if the muxed file contains HDR Dynamic info as expected
+ MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
+ String decoder = codecList.findDecoderForFormat(format);
+ assertNotNull("Device advertises support for encoding " + format + " but not decoding it",
+ decoder);
+
+ HDRDecoderTestBase decoderTest = new HDRDecoderTestBase(decoder, mMime,
+ tmpFile.getAbsolutePath());
+ decoderTest.validateHDRInfo(hdrStaticInfo, hdrStaticInfo, mHdrDynamicInfo, mHdrDynamicInfo);
+ if (HDR_INFO_IN_BITSTREAM_CODECS.contains(mMime)) {
+ decoderTest.validateHDRInfo(hdrStaticInfo, null, mHdrDynamicInfo, null);
+ }
+ tmpFile.delete();
+ }
+}
diff --git a/tests/media/src/android/mediav2/cts/DecoderHDRInfoTest.java b/tests/media/src/android/mediav2/cts/DecoderHDRInfoTest.java
index 3dd28aa..4994a9b 100644
--- a/tests/media/src/android/mediav2/cts/DecoderHDRInfoTest.java
+++ b/tests/media/src/android/mediav2/cts/DecoderHDRInfoTest.java
@@ -22,6 +22,8 @@
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
+import com.android.compatibility.common.util.CddTest;
+
import org.junit.Assume;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -29,34 +31,34 @@
import java.io.IOException;
import java.nio.ByteBuffer;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
+import java.util.Map;
/**
- * Test to validate hdr static metadata in decoders
+ * Test to validate hdr static info in decoders
*/
@RunWith(Parameterized.class)
// P010 support was added in Android T, hence limit the following tests to Android T and above
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
-public class DecoderHDRInfoTest extends CodecDecoderTestBase {
+public class DecoderHDRInfoTest extends HDRDecoderTestBase {
private static final String LOG_TAG = DecoderHDRInfoTest.class.getSimpleName();
- private static final String HDR_STATIC_INFO =
- "00 d0 84 80 3e c2 33 c4 86 4c 1d b8 0b 13 3d 42 40 a0 0f 32 00 10 27 df 0d";
- private static final String HDR_STATIC_INCORRECT_INFO =
- "00 d0 84 80 3e c2 33 c4 86 10 27 d0 07 13 3d 42 40 a0 0f 32 00 10 27 df 0d";
- private final ByteBuffer mHDRStaticInfoStream;
- private final ByteBuffer mHDRStaticInfoContainer;
+ private String mHDRStaticInfoStream;
+ private String mHDRStaticInfoContainer;
+ private Map<Integer, String> mHDRDynamicInfoStream;
+ private Map<Integer, String> mHDRDynamicInfoContainer;
public DecoderHDRInfoTest(String codecName, String mediaType, String testFile,
- String hdrStaticInfoStream, String hdrStaticInfoContainer) {
+ String hdrStaticInfoStream, String hdrStaticInfoContainer,
+ Map<Integer, String> HDRDynamicInfoStream,
+ Map<Integer, String> HDRDynamicInfoContainer) {
super(codecName, mediaType, testFile);
- mHDRStaticInfoStream = hdrStaticInfoStream != null ?
- ByteBuffer.wrap(loadByteArrayFromString(hdrStaticInfoStream)) : null;
- mHDRStaticInfoContainer = hdrStaticInfoContainer != null ?
- ByteBuffer.wrap(loadByteArrayFromString(hdrStaticInfoContainer)) : null;
+ mHDRStaticInfoStream = hdrStaticInfoStream;
+ mHDRStaticInfoContainer = hdrStaticInfoContainer;
+ mHDRDynamicInfoStream = HDRDynamicInfoStream;
+ mHDRDynamicInfoContainer = HDRDynamicInfoContainer;
}
@Parameterized.Parameters(name = "{index}({0}_{1})")
@@ -65,64 +67,50 @@
final boolean needAudio = false;
final boolean needVideo = true;
final List<Object[]> exhaustiveArgsList = Arrays.asList(new Object[][]{
- // codecMediaType, testFile, hdrInfo in stream, hdrInfo in container
+ // codecMediaType, testFile, hdrStaticInfo in stream, hdrStaticInfo in container,
+ // hdrDynamicInfo in stream, hdrDynamicInfo in container
{MediaFormat.MIMETYPE_VIDEO_HEVC,
"cosmat_352x288_hdr10_stream_and_container_correct_hevc.mkv",
- HDR_STATIC_INFO, HDR_STATIC_INFO},
+ HDR_STATIC_INFO, HDR_STATIC_INFO, null, null},
{MediaFormat.MIMETYPE_VIDEO_HEVC,
"cosmat_352x288_hdr10_stream_correct_container_incorrect_hevc.mkv",
- HDR_STATIC_INFO, HDR_STATIC_INCORRECT_INFO},
+ HDR_STATIC_INFO, HDR_STATIC_INCORRECT_INFO, null, null},
{MediaFormat.MIMETYPE_VIDEO_HEVC, "cosmat_352x288_hdr10_only_stream_hevc.mkv",
- HDR_STATIC_INFO, null},
+ HDR_STATIC_INFO, null, null, null},
{MediaFormat.MIMETYPE_VIDEO_HEVC, "cosmat_352x288_hdr10_only_container_hevc.mkv",
- null, HDR_STATIC_INFO},
+ null, HDR_STATIC_INFO, null, null},
{MediaFormat.MIMETYPE_VIDEO_VP9, "cosmat_352x288_hdr10_only_container_vp9.mkv",
- null, HDR_STATIC_INFO},
+ null, HDR_STATIC_INFO, null, null},
{MediaFormat.MIMETYPE_VIDEO_AV1,
"cosmat_352x288_hdr10_stream_and_container_correct_av1.mkv",
- HDR_STATIC_INFO, HDR_STATIC_INFO},
+ HDR_STATIC_INFO, HDR_STATIC_INFO, null, null},
{MediaFormat.MIMETYPE_VIDEO_AV1,
"cosmat_352x288_hdr10_stream_correct_container_incorrect_av1.mkv",
- HDR_STATIC_INFO, HDR_STATIC_INCORRECT_INFO},
+ HDR_STATIC_INFO, HDR_STATIC_INCORRECT_INFO, null, null},
{MediaFormat.MIMETYPE_VIDEO_AV1, "cosmat_352x288_hdr10_only_stream_av1.mkv",
- HDR_STATIC_INFO, null},
+ HDR_STATIC_INFO, null, null, null},
{MediaFormat.MIMETYPE_VIDEO_AV1, "cosmat_352x288_hdr10_only_container_av1.mkv",
- null, HDR_STATIC_INFO},
+ null, HDR_STATIC_INFO, null, null},
+ {MediaFormat.MIMETYPE_VIDEO_HEVC, "cosmat_352x288_hdr10plus_hevc.mp4",
+ null, null, HDR_DYNAMIC_INFO, null},
+ {MediaFormat.MIMETYPE_VIDEO_HEVC, "cosmat_352x288_hdr10plus_hevc.mp4",
+ null, null, HDR_DYNAMIC_INFO, HDR_DYNAMIC_INCORRECT_INFO},
+ {MediaFormat.MIMETYPE_VIDEO_AV1, "cosmat_352x288_hdr10plus_av1.mkv",
+ null, null, HDR_DYNAMIC_INFO, null},
+ {MediaFormat.MIMETYPE_VIDEO_AV1, "cosmat_352x288_hdr10plus_av1.mkv",
+ null, null, HDR_DYNAMIC_INFO, HDR_DYNAMIC_INCORRECT_INFO},
+ {MediaFormat.MIMETYPE_VIDEO_VP9, "cosmat_352x288_hdr10_only_container_vp9.mkv",
+ null, null, null, HDR_DYNAMIC_INFO},
+
});
return prepareParamList(exhaustiveArgsList, isEncoder, needAudio, needVideo, false);
}
@SmallTest
@Test(timeout = PER_TEST_TIMEOUT_SMALL_TEST_MS)
- public void testHDRMetadata() throws IOException, InterruptedException {
- int[] Hdr10Profiles = mProfileHdr10Map.get(mMime);
- Assume.assumeNotNull("Test is only applicable to codecs that have HDR10 profiles",
- Hdr10Profiles);
- MediaFormat format = setUpSource(mTestFile);
- mExtractor.release();
- ArrayList<MediaFormat> formats = new ArrayList<>();
- formats.add(format);
-
- // When HDR metadata isn't present in the container, but included in the bitstream,
- // extractors may not be able to populate HDR10/HDR10+ profiles correctly.
- // In such cases, override the profile
- if (mHDRStaticInfoContainer == null && mHDRStaticInfoStream != null) {
- int profile = Hdr10Profiles[0];
- format.setInteger(MediaFormat.KEY_PROFILE, profile);
- }
- Assume.assumeTrue(areFormatsSupported(mCodecName, mMime, formats));
-
- if (mHDRStaticInfoContainer != null) {
- validateHDRStaticMetaData(format, mHDRStaticInfoContainer);
- }
-
- validateHDRStaticMetaData(mInpPrefix, mTestFile,
- mHDRStaticInfoStream == null ? mHDRStaticInfoContainer : mHDRStaticInfoStream,
- false);
- if (mHDRStaticInfoStream != null) {
- if (EncoderHDRInfoTest.mCheckESList.contains(mMime)) {
- validateHDRStaticMetaData(mInpPrefix, mTestFile, mHDRStaticInfoStream, true);
- }
- }
+ @CddTest(requirements = {"5.3.5/C-3-1", "5.3.7/C-4-1", "5.3.9"})
+ public void testHDRInfo() throws IOException, InterruptedException {
+ validateHDRInfo(mHDRStaticInfoStream, mHDRStaticInfoContainer, mHDRDynamicInfoStream,
+ mHDRDynamicInfoContainer);
}
}
diff --git a/tests/media/src/android/mediav2/cts/EncoderHDRInfoTest.java b/tests/media/src/android/mediav2/cts/EncoderHDRInfoTest.java
index 26c6eb5..66916e5 100644
--- a/tests/media/src/android/mediav2/cts/EncoderHDRInfoTest.java
+++ b/tests/media/src/android/mediav2/cts/EncoderHDRInfoTest.java
@@ -21,84 +21,59 @@
import android.media.MediaFormat;
import android.media.MediaMuxer;
import android.os.Build;
-import android.os.Bundle;
+import android.util.Log;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
+import com.android.compatibility.common.util.CddTest;
+
+import org.junit.After;
import org.junit.Assume;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
-import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.List;
+import java.util.Map;
import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUVP010;
-import static android.media.MediaCodecInfo.CodecProfileLevel.*;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/**
- * Test to validate hdr static and dynamic metadata in encoders
+ * HDR10 Metadata is an aid for a display device to show the content in an optimal manner. It
+ * contains the HDR content and mastering device properties that are used by the display device
+ * to map the content according to its own color gamut and peak brightness. This information is
+ * part of the elementary stream. Generally this information is placed at scene change intervals
+ * or even at every frame level. If the encoder is configured with hdr info, then it is
+ * expected to place this information in the elementary stream as-is. This test validates the
+ * same. The test feeds per-frame or per-scene info at various points and expects the encoder
+ * to place the hdr info in the elementary stream at exactly those points
+ *
+ * Restrict hdr info test for Android T and above
*/
@RunWith(Parameterized.class)
// P010 support was added in Android T, hence limit the following tests to Android T and above
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
-public class EncoderHDRInfoTest extends CodecEncoderTestBase {
+public class EncoderHDRInfoTest extends HDREncoderTestBase {
private static final String LOG_TAG = EncoderHDRInfoTest.class.getSimpleName();
- private MediaMuxer mMuxer;
- private int mTrackID = -1;
- static final ArrayList<String> mCheckESList = new ArrayList<>();
+ private String mHDRStaticInfo;
+ private Map<Integer, String> mHDRDynamicInfo;
- static {
- mCheckESList.add(MediaFormat.MIMETYPE_VIDEO_AV1);
- mCheckESList.add(MediaFormat.MIMETYPE_VIDEO_AVC);
- mCheckESList.add(MediaFormat.MIMETYPE_VIDEO_HEVC);
- }
-
- public EncoderHDRInfoTest(String encoderName, String mediaType, int bitrate, int width,
- int height, boolean testDynamicMetadata) {
- super(encoderName, mediaType, new int[]{bitrate}, new int[]{width}, new int[]{height});
- mTestDynamicMetadata = testDynamicMetadata;
- }
-
- void enqueueInput(int bufferIndex) {
- if(mTestDynamicMetadata){
- final Bundle params = new Bundle();
- byte[] info = loadByteArrayFromString(HDR_DYNAMIC_INFO[mInputCount]);
- params.putByteArray(MediaFormat.KEY_HDR10_PLUS_INFO, info);
- mCodec.setParameters(params);
- if (mInputCount >= HDR_DYNAMIC_INFO.length) {
- mSawInputEOS = true;
- }
- }
- super.enqueueInput(bufferIndex);
- }
- void dequeueOutput(int bufferIndex, MediaCodec.BufferInfo info) {
- MediaFormat bufferFormat = mCodec.getOutputFormat(bufferIndex);
- if (info.size > 0) {
- ByteBuffer buf = mCodec.getOutputBuffer(bufferIndex);
- if (mMuxer != null) {
- if (mTrackID == -1) {
- mTrackID = mMuxer.addTrack(bufferFormat);
- mMuxer.start();
- }
- mMuxer.writeSampleData(mTrackID, buf, info);
- }
- }
- super.dequeueOutput(bufferIndex, info);
- // verify if the out fmt contains HDR Dynamic metadata as expected
- if (mTestDynamicMetadata && mOutputCount > 0) {
- validateHDRDynamicMetaData(bufferFormat,
- ByteBuffer.wrap(loadByteArrayFromString(HDR_DYNAMIC_INFO[mOutputCount - 1])));
- }
+ public EncoderHDRInfoTest(String encoderName, String mediaType, int bitrate,
+ int width, int height, String HDRStaticInfo,
+ Map<Integer, String> HDRDynamicInfo) {
+ super(encoderName, mediaType, bitrate, width, height);
+ mHDRStaticInfo = HDRStaticInfo;
+ mHDRDynamicInfo = HDRDynamicInfo;
}
@Parameterized.Parameters(name = "{index}({0}_{1})")
@@ -106,111 +81,28 @@
final boolean isEncoder = true;
final boolean needAudio = false;
final boolean needVideo = true;
+ final String[] mediaTypes = new String[]{
+ MediaFormat.MIMETYPE_VIDEO_AV1,
+ MediaFormat.MIMETYPE_VIDEO_HEVC,
+ MediaFormat.MIMETYPE_VIDEO_VP9
+ };
- final List<Object[]> exhaustiveArgsList = Arrays.asList(new Object[][]{
- {MediaFormat.MIMETYPE_VIDEO_AV1, 512000, 352, 288, false},
- {MediaFormat.MIMETYPE_VIDEO_VP9, 512000, 352, 288, false},
- {MediaFormat.MIMETYPE_VIDEO_HEVC, 512000, 352, 288, false},
-
- {MediaFormat.MIMETYPE_VIDEO_AV1, 512000, 352, 288, true},
- {MediaFormat.MIMETYPE_VIDEO_VP9, 512000, 352, 288, true},
- {MediaFormat.MIMETYPE_VIDEO_HEVC, 512000, 352, 288, true},
- });
+ final List<Object[]> exhaustiveArgsList = new ArrayList<>();
+ for (String mediaType : mediaTypes) {
+ // mediaType, bitrate, width, height, hdrStaticInfo, hdrDynamicInfo
+ exhaustiveArgsList.add(new Object[]{mediaType, 512000, 352, 288, HDR_STATIC_INFO,
+ null});
+ exhaustiveArgsList.add(new Object[]{mediaType, 512000, 352, 288, null,
+ HDR_DYNAMIC_INFO});
+ }
return prepareParamList(exhaustiveArgsList, isEncoder, needAudio, needVideo, false);
}
@SmallTest
@Test(timeout = PER_TEST_TIMEOUT_SMALL_TEST_MS)
- public void testHDRMetadata() throws IOException, InterruptedException {
- int profile;
- setUpParams(1);
- MediaFormat format = mFormats.get(0);
- final ByteBuffer hdrStaticInfo = ByteBuffer.wrap(loadByteArrayFromString(HDR_STATIC_INFO));
- if (mTestDynamicMetadata) {
- profile = mProfileHdr10PlusMap.getOrDefault(mMime, new int[]{-1})[0];
- } else {
- profile = mProfileHdr10Map.getOrDefault(mMime, new int[]{-1})[0];
- }
- format.setInteger(MediaFormat.KEY_PROFILE, profile);
- format.setInteger(MediaFormat.KEY_COLOR_FORMAT, COLOR_FormatYUVP010);
- format.setInteger(MediaFormat.KEY_COLOR_RANGE, MediaFormat.COLOR_RANGE_LIMITED);
- format.setInteger(MediaFormat.KEY_COLOR_STANDARD, MediaFormat.COLOR_STANDARD_BT2020);
- format.setInteger(MediaFormat.KEY_COLOR_TRANSFER, MediaFormat.COLOR_TRANSFER_ST2084);
- format.setByteBuffer(MediaFormat.KEY_HDR_STATIC_INFO, hdrStaticInfo);
- mFormats.clear();
- mFormats.add(format);
- Assume.assumeTrue(mCodecName + " does not support this HDR profile",
- areFormatsSupported(mCodecName, mMime, mFormats));
- Assume.assumeTrue(mCodecName + " does not support color format COLOR_FormatYUVP010",
- hasSupportForColorFormat(mCodecName, mMime, COLOR_FormatYUVP010));
- mBytesPerSample = 2;
- setUpSource(INPUT_VIDEO_FILE_HBD);
- mOutputBuff = new OutputManager();
- mCodec = MediaCodec.createByCodecName(mCodecName);
- mOutputBuff.reset();
- String log = String.format("format: %s \n codec: %s:: ", format, mCodecName);
- File tmpFile;
- int muxerFormat;
- if (mMime.equals(MediaFormat.MIMETYPE_VIDEO_VP9)) {
- muxerFormat = MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM;
- tmpFile = File.createTempFile("tmp10bit", ".webm");
- } else {
- muxerFormat = MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4;
- tmpFile = File.createTempFile("tmp10bit", ".mp4");
- }
- mMuxer = new MediaMuxer(tmpFile.getAbsolutePath(), muxerFormat);
- configureCodec(format, true, true, true);
- mCodec.start();
- doWork(4);
- queueEOS();
- waitForAllOutputs();
- if (mTrackID != -1) {
- mMuxer.stop();
- mTrackID = -1;
- }
- if (mMuxer != null) {
- mMuxer.release();
- mMuxer = null;
- }
- assertTrue(log + "unexpected error", !mAsyncHandle.hasSeenError());
- assertTrue(log + "no input sent", 0 != mInputCount);
- assertTrue(log + "output received", 0 != mOutputCount);
-
- MediaFormat fmt = mCodec.getOutputFormat();
- mCodec.stop();
- mCodec.release();
-
- // verify if the out fmt contains HDR Static metadata as expected
- validateHDRStaticMetaData(fmt, hdrStaticInfo);
-
- // verify if the muxed file contains HDR metadata as expected
- MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
- String decoder = codecList.findDecoderForFormat(format);
- assertNotNull("Device advertises support for encoding " + format.toString() +
- " but not decoding it", decoder);
- CodecDecoderTestBase cdtb =
- new CodecDecoderTestBase(decoder, mMime, tmpFile.getAbsolutePath());
- String parent = tmpFile.getParent();
- if (parent != null) parent += File.separator;
- else parent = "";
- cdtb.validateHDRStaticMetaData(parent, tmpFile.getName(), hdrStaticInfo, false);
- if (mTestDynamicMetadata) {
- cdtb.validateHDRDynamicMetaData(parent, tmpFile.getName(), false);
- }
-
- // if HDR static metadata can also be signalled via elementary stream then verify if
- // the elementary stream contains HDR static data as expected
- if (mCheckESList.contains(mMime)) {
- cdtb.validateHDRStaticMetaData(parent, tmpFile.getName(), hdrStaticInfo, true);
-
- // since HDR static metadata is signalled via elementary stream then verify if
- // the elementary stream contains HDR static data as expected
- if (mTestDynamicMetadata) {
- cdtb.validateHDRDynamicMetaData(parent, tmpFile.getName(), true);
- }
- }
-
- tmpFile.delete();
+ @CddTest(requirements = {"5.12/C-6-4"})
+ public void testHDRInfo() throws IOException, InterruptedException {
+ validateHDRInfo(mHDRStaticInfo, mHDRDynamicInfo);
}
}
diff --git a/tests/ondevicepersonalization/TEST_MAPPING b/tests/ondevicepersonalization/TEST_MAPPING
new file mode 100644
index 0000000..8d96b25
--- /dev/null
+++ b/tests/ondevicepersonalization/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "presubmit": [
+ {
+ "name": "CtsOnDevicePersonalizationTestCases"
+ }
+ ]
+}
diff --git a/tests/ondevicepersonalization/src/android/ondevicepersonalization/cts/OnDevicePersonalizationServiceTest.java b/tests/ondevicepersonalization/src/android/ondevicepersonalization/cts/OnDevicePersonalizationServiceTest.java
index 16db9a9..a0467a2 100644
--- a/tests/ondevicepersonalization/src/android/ondevicepersonalization/cts/OnDevicePersonalizationServiceTest.java
+++ b/tests/ondevicepersonalization/src/android/ondevicepersonalization/cts/OnDevicePersonalizationServiceTest.java
@@ -39,7 +39,7 @@
@Before
public void setup() throws Exception {
mContext = ApplicationProvider.getApplicationContext();
- mService = new OnDevicePersonalizationManager(mContext);
+ mService = mContext.getSystemService(OnDevicePersonalizationManager.class);
}
@Test
diff --git a/tests/tests/bluetooth/AndroidTest.xml b/tests/tests/bluetooth/AndroidTest.xml
index 9a3075b..9818962 100644
--- a/tests/tests/bluetooth/AndroidTest.xml
+++ b/tests/tests/bluetooth/AndroidTest.xml
@@ -33,6 +33,6 @@
<!-- Only run Cts Tests in MTS if the Bluetooth Mainline module is installed. -->
<object type="module_controller"
class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
- <option name="mainline-module-package-name" value="com.google.android.bluetooth" />
+ <option name="mainline-module-package-name" value="com.android.btservices" />
</object>
</configuration>
diff --git a/tests/tests/media/codec/src/android/media/codec/cts/ExtractDecodeEditEncodeMuxTest.java b/tests/tests/media/codec/src/android/media/codec/cts/ExtractDecodeEditEncodeMuxTest.java
index b2fbc6c..8acbbdd 100644
--- a/tests/tests/media/codec/src/android/media/codec/cts/ExtractDecodeEditEncodeMuxTest.java
+++ b/tests/tests/media/codec/src/android/media/codec/cts/ExtractDecodeEditEncodeMuxTest.java
@@ -42,6 +42,8 @@
import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaCodecInfo.CodecProfileLevel;
+import com.android.compatibility.common.util.CddTest;
+
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -130,38 +132,43 @@
super(MediaStubActivity.class);
}
+ @CddTest(requirements = {"5.2", "5.3"})
public void testExtractDecodeEditEncodeMuxQCIF() throws Throwable {
if(!setSize(176, 144)) return;
setSource("video_480x360_mp4_h264_500kbps_30fps_aac_stereo_128kbps_44100hz.mp4");
setCopyVideo();
- setVideoMimeType(MediaFormat.MIMETYPE_VIDEO_AVC);
+ setOutputVideoMimeType(MediaFormat.MIMETYPE_VIDEO_AVC);
TestWrapper.runTest(this);
}
+ @CddTest(requirements = {"5.2", "5.3"})
public void testExtractDecodeEditEncodeMuxQVGA() throws Throwable {
if(!setSize(320, 240)) return;
setSource("video_480x360_mp4_h264_500kbps_30fps_aac_stereo_128kbps_44100hz.mp4");
setCopyVideo();
- setVideoMimeType(MediaFormat.MIMETYPE_VIDEO_AVC);
+ setOutputVideoMimeType(MediaFormat.MIMETYPE_VIDEO_AVC);
TestWrapper.runTest(this);
}
+ @CddTest(requirements = {"5.2", "5.3"})
public void testExtractDecodeEditEncodeMux720p() throws Throwable {
if(!setSize(1280, 720)) return;
setSource("video_480x360_mp4_h264_500kbps_30fps_aac_stereo_128kbps_44100hz.mp4");
setCopyVideo();
- setVideoMimeType(MediaFormat.MIMETYPE_VIDEO_AVC);
+ setOutputVideoMimeType(MediaFormat.MIMETYPE_VIDEO_AVC);
TestWrapper.runTest(this);
}
+ @CddTest(requirements = {"5.2", "5.3"})
public void testExtractDecodeEditEncodeMux2160pHevc() throws Throwable {
if(!setSize(3840, 2160)) return;
setSource("video_480x360_mp4_h264_500kbps_30fps_aac_stereo_128kbps_44100hz.mp4");
setCopyVideo();
- setVideoMimeType(MediaFormat.MIMETYPE_VIDEO_HEVC);
+ setOutputVideoMimeType(MediaFormat.MIMETYPE_VIDEO_HEVC);
TestWrapper.runTest(this);
}
+ @CddTest(requirements = {"5.1.1", "5.1.2"})
public void testExtractDecodeEditEncodeMuxAudio() throws Throwable {
if(!setSize(1280, 720)) return;
setSource("video_480x360_mp4_h264_500kbps_30fps_aac_stereo_128kbps_44100hz.mp4");
@@ -170,11 +177,13 @@
TestWrapper.runTest(this);
}
+ @CddTest(requirements = {"5.1.1", "5.1.2", "5.2", "5.3"})
public void testExtractDecodeEditEncodeMuxAudioVideo() throws Throwable {
if(!setSize(1280, 720)) return;
setSource("video_480x360_mp4_h264_500kbps_30fps_aac_stereo_128kbps_44100hz.mp4");
setCopyAudio();
setCopyVideo();
+ setOutputVideoMimeType(MediaFormat.MIMETYPE_VIDEO_AVC);
setVerifyAudioFormat();
TestWrapper.runTest(this);
}
@@ -289,7 +298,7 @@
mOutputFile = sb.toString();
}
- private void setVideoMimeType(String mimeType) {
+ private void setOutputVideoMimeType(String mimeType) {
mOutputVideoMimeType = mimeType;
}
@@ -305,44 +314,52 @@
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
- // We avoid the device-specific limitations on width and height by using values
- // that are multiples of 16, which all tested devices seem to be able to handle.
- MediaFormat outputVideoFormat =
- MediaFormat.createVideoFormat(mOutputVideoMimeType, mWidth, mHeight);
+ String videoEncoderName = null;
+ MediaFormat outputVideoFormat = null;
+ if (mCopyVideo) {
+ // We avoid the device-specific limitations on width and height by using values
+ // that are multiples of 16, which all tested devices seem to be able to handle.
+ outputVideoFormat =
+ MediaFormat.createVideoFormat(mOutputVideoMimeType, mWidth, mHeight);
- // Set some properties. Failing to specify some of these can cause the MediaCodec
- // configure() call to throw an unhelpful exception.
- outputVideoFormat.setInteger(
- MediaFormat.KEY_COLOR_FORMAT, OUTPUT_VIDEO_COLOR_FORMAT);
- outputVideoFormat.setInteger(MediaFormat.KEY_BIT_RATE, OUTPUT_VIDEO_BIT_RATE);
- outputVideoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, OUTPUT_VIDEO_FRAME_RATE);
- outputVideoFormat.setInteger(
- MediaFormat.KEY_I_FRAME_INTERVAL, OUTPUT_VIDEO_IFRAME_INTERVAL);
- if (VERBOSE) Log.d(TAG, "video format: " + outputVideoFormat);
+ // Set some properties. Failing to specify some of these can cause the MediaCodec
+ // configure() call to throw an unhelpful exception.
+ outputVideoFormat.setInteger(
+ MediaFormat.KEY_COLOR_FORMAT, OUTPUT_VIDEO_COLOR_FORMAT);
+ outputVideoFormat.setInteger(MediaFormat.KEY_BIT_RATE, OUTPUT_VIDEO_BIT_RATE);
+ outputVideoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, OUTPUT_VIDEO_FRAME_RATE);
+ outputVideoFormat.setInteger(
+ MediaFormat.KEY_I_FRAME_INTERVAL, OUTPUT_VIDEO_IFRAME_INTERVAL);
+ if (VERBOSE) Log.d(TAG, "video format: " + outputVideoFormat);
- String videoEncoderName = mcl.findEncoderForFormat(outputVideoFormat);
- if (videoEncoderName == null) {
- // Don't fail CTS if they don't have an AVC codec (not here, anyway).
- Log.e(TAG, "Unable to find an appropriate codec for " + outputVideoFormat);
- return;
+ videoEncoderName = mcl.findEncoderForFormat(outputVideoFormat);
+ if (videoEncoderName == null) {
+ // Don't fail CTS if they don't have an AVC codec (not here, anyway).
+ Log.e(TAG, "Unable to find an appropriate codec for " + outputVideoFormat);
+ return;
+ }
+ if (VERBOSE) Log.d(TAG, "video found codec: " + videoEncoderName);
}
- if (VERBOSE) Log.d(TAG, "video found codec: " + videoEncoderName);
- MediaFormat outputAudioFormat =
- MediaFormat.createAudioFormat(
- OUTPUT_AUDIO_MIME_TYPE, OUTPUT_AUDIO_SAMPLE_RATE_HZ,
- OUTPUT_AUDIO_CHANNEL_COUNT);
- outputAudioFormat.setInteger(MediaFormat.KEY_BIT_RATE, OUTPUT_AUDIO_BIT_RATE);
- // TODO: Bug workaround --- uncomment once fixed.
- // outputAudioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, OUTPUT_AUDIO_AAC_PROFILE);
+ String audioEncoderName = null;
+ MediaFormat outputAudioFormat = null;
+ if (mCopyAudio) {
+ outputAudioFormat =
+ MediaFormat.createAudioFormat(
+ OUTPUT_AUDIO_MIME_TYPE, OUTPUT_AUDIO_SAMPLE_RATE_HZ,
+ OUTPUT_AUDIO_CHANNEL_COUNT);
+ outputAudioFormat.setInteger(MediaFormat.KEY_BIT_RATE, OUTPUT_AUDIO_BIT_RATE);
+ // TODO: Bug workaround --- uncomment once fixed.
+ // outputAudioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, OUTPUT_AUDIO_AAC_PROFILE);
- String audioEncoderName = mcl.findEncoderForFormat(outputAudioFormat);
- if (audioEncoderName == null) {
- // Don't fail CTS if they don't have an AAC codec (not here, anyway).
- Log.e(TAG, "Unable to find an appropriate codec for " + outputAudioFormat);
- return;
+ audioEncoderName = mcl.findEncoderForFormat(outputAudioFormat);
+ if (audioEncoderName == null) {
+ // Don't fail CTS if they don't have an AAC codec (not here, anyway).
+ Log.e(TAG, "Unable to find an appropriate codec for " + outputAudioFormat);
+ return;
+ }
+ if (VERBOSE) Log.d(TAG, "audio found codec: " + audioEncoderName);
}
- if (VERBOSE) Log.d(TAG, "audio found codec: " + audioEncoderName);
MediaExtractor videoExtractor = null;
MediaExtractor audioExtractor = null;
@@ -863,29 +880,25 @@
ByteBuffer decoderInputBuffer = audioDecoderInputBuffers[decoderInputBufferIndex];
int size = audioExtractor.readSampleData(decoderInputBuffer, 0);
long presentationTime = audioExtractor.getSampleTime();
+ int flags = audioExtractor.getSampleFlags();
if (VERBOSE) {
Log.d(TAG, "audio extractor: returned buffer of size " + size);
Log.d(TAG, "audio extractor: returned buffer for time " + presentationTime);
}
+ audioExtractorDone = !audioExtractor.advance();
+ if (audioExtractorDone) {
+ if (VERBOSE) Log.d(TAG, "audio extractor: EOS");
+ flags = flags | MediaCodec.BUFFER_FLAG_END_OF_STREAM;
+ }
if (size >= 0) {
audioDecoder.queueInputBuffer(
decoderInputBufferIndex,
0,
size,
presentationTime,
- audioExtractor.getSampleFlags());
+ flags);
+ audioExtractedFrameCount++;
}
- audioExtractorDone = !audioExtractor.advance();
- if (audioExtractorDone) {
- if (VERBOSE) Log.d(TAG, "audio extractor: EOS");
- audioDecoder.queueInputBuffer(
- decoderInputBufferIndex,
- 0,
- 0,
- 0,
- MediaCodec.BUFFER_FLAG_END_OF_STREAM);
- }
- audioExtractedFrameCount++;
// We extracted a frame, let's try something else next.
break;
}
diff --git a/tests/tests/media/common/src/android/media/cts/TestUtils.java b/tests/tests/media/common/src/android/media/cts/TestUtils.java
index f98b3ab..20bf143 100644
--- a/tests/tests/media/common/src/android/media/cts/TestUtils.java
+++ b/tests/tests/media/common/src/android/media/cts/TestUtils.java
@@ -190,6 +190,7 @@
if (name.startsWith("c2.android.")) {
return true;
}
+ Log.d(TAG, "Test mode MTS does not test codec " + name);
return false;
}
diff --git a/tests/tests/media/decoder/src/android/media/decoder/cts/NativeDecoderTest.java b/tests/tests/media/decoder/src/android/media/decoder/cts/NativeDecoderTest.java
index f5e3e0d..9b98fed 100644
--- a/tests/tests/media/decoder/src/android/media/decoder/cts/NativeDecoderTest.java
+++ b/tests/tests/media/decoder/src/android/media/decoder/cts/NativeDecoderTest.java
@@ -129,81 +129,6 @@
return new AssetFileDescriptor(parcelFD, 0, parcelFD.getStatSize());
}
- private static int[] getSampleSizes(String path, String[] keys, String[] values)
- throws IOException {
- MediaExtractor ex = new MediaExtractor();
- if (keys == null || values == null) {
- ex.setDataSource(path);
- } else {
- Map<String, String> headers = null;
- int numheaders = Math.min(keys.length, values.length);
- for (int i = 0; i < numheaders; i++) {
- if (headers == null) {
- headers = new HashMap<>();
- }
- String key = keys[i];
- String value = values[i];
- headers.put(key, value);
- }
- ex.setDataSource(path, headers);
- }
-
- return getSampleSizes(ex);
- }
-
- private static int[] getSampleSizes(FileDescriptor fd, long offset, long size)
- throws IOException {
- MediaExtractor ex = new MediaExtractor();
- ex.setDataSource(fd, offset, size);
- return getSampleSizes(ex);
- }
-
- private static int[] getSampleSizes(MediaExtractor ex) {
- ArrayList<Integer> foo = new ArrayList<Integer>();
- ByteBuffer buf = ByteBuffer.allocate(1024*1024);
- int numtracks = ex.getTrackCount();
- assertTrue("no tracks", numtracks > 0);
- foo.add(numtracks);
- for (int i = 0; i < numtracks; i++) {
- MediaFormat format = ex.getTrackFormat(i);
- String mime = format.getString(MediaFormat.KEY_MIME);
- if (mime.startsWith("audio/")) {
- foo.add(0);
- foo.add(format.getInteger(MediaFormat.KEY_SAMPLE_RATE));
- foo.add(format.getInteger(MediaFormat.KEY_CHANNEL_COUNT));
- foo.add((int)format.getLong(MediaFormat.KEY_DURATION));
- } else if (mime.startsWith("video/")) {
- foo.add(1);
- foo.add(format.getInteger(MediaFormat.KEY_WIDTH));
- foo.add(format.getInteger(MediaFormat.KEY_HEIGHT));
- foo.add((int)format.getLong(MediaFormat.KEY_DURATION));
- } else {
- fail("unexpected mime type: " + mime);
- }
- ex.selectTrack(i);
- }
- while(true) {
- int n = ex.readSampleData(buf, 0);
- if (n < 0) {
- break;
- }
- foo.add(n);
- foo.add(ex.getSampleTrackIndex());
- foo.add(ex.getSampleFlags());
- foo.add((int)ex.getSampleTime()); // just the low bits should be OK
- byte[] foobar = new byte[n];
- buf.get(foobar, 0, n);
- foo.add(adler32(foobar));
- ex.advance();
- }
-
- int [] ret = new int[foo.size()];
- for (int i = 0; i < ret.length; i++) {
- ret[i] = foo.get(i);
- }
- return ret;
- }
-
@Test
public void testDataSource() throws Exception {
int testsRun = testDecoder(
diff --git a/tests/tests/media/encoder/src/android/media/encoder/cts/VideoEncoderTest.java b/tests/tests/media/encoder/src/android/media/encoder/cts/VideoEncoderTest.java
index 4afc7aa..74aa214 100644
--- a/tests/tests/media/encoder/src/android/media/encoder/cts/VideoEncoderTest.java
+++ b/tests/tests/media/encoder/src/android/media/encoder/cts/VideoEncoderTest.java
@@ -43,6 +43,7 @@
import android.media.cts.OutputSurface;
import android.media.cts.Preconditions;
import android.media.cts.TestArgs;
+import android.media.cts.TestUtils;
import android.net.Uri;
import android.platform.test.annotations.AppModeFull;
import android.util.Log;
@@ -1301,6 +1302,10 @@
if (TestArgs.shouldSkipCodec(encoder)) {
continue;
}
+ if (!TestUtils.isTestableCodecInCurrentMode(encoder)) {
+ Log.d(TAG, "Skipping tests for codec: " + encoder);
+ continue;
+ }
CodecCapabilities caps = getCodecCapabities(encoder, mediaType, true);
assertNotNull(caps);
EncoderSize encoderSize = new EncoderSize(encoder, mediaType, caps);
diff --git a/tests/tests/media/misc/AndroidTest.xml b/tests/tests/media/misc/AndroidTest.xml
index 4a2ffaf..1facb8d 100644
--- a/tests/tests/media/misc/AndroidTest.xml
+++ b/tests/tests/media/misc/AndroidTest.xml
@@ -40,7 +40,7 @@
</target_preparer>
<target_preparer class="com.android.compatibility.common.tradefed.targetprep.MediaPreparer">
<option name="push-all" value="true" />
- <option name="media-folder-name" value="CtsMediaMiscTestCases-1.0" />
+ <option name="media-folder-name" value="CtsMediaMiscTestCases-2.0" />
<option name="dynamic-config-module" value="CtsMediaMiscTestCases" />
</target_preparer>
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
diff --git a/tests/tests/media/misc/DynamicConfig.xml b/tests/tests/media/misc/DynamicConfig.xml
index 09e5fba..51e05bb 100644
--- a/tests/tests/media/misc/DynamicConfig.xml
+++ b/tests/tests/media/misc/DynamicConfig.xml
@@ -15,6 +15,6 @@
<dynamicConfig>
<entry key="media_files_url">
- <value>https://storage.googleapis.com/android_media/cts/tests/tests/media/misc/CtsMediaMiscTestCases-1.0.zip</value>
+ <value>https://storage.googleapis.com/android_media/cts/tests/tests/media/misc/CtsMediaMiscTestCases-2.0.zip</value>
</entry>
</dynamicConfig>
diff --git a/tests/tests/media/misc/copy_media.sh b/tests/tests/media/misc/copy_media.sh
index 96f5a49..27ba1e7 100755
--- a/tests/tests/media/misc/copy_media.sh
+++ b/tests/tests/media/misc/copy_media.sh
@@ -17,4 +17,4 @@
[ -z "$MEDIA_ROOT_DIR" ] && MEDIA_ROOT_DIR=$(dirname $0)/..
source $MEDIA_ROOT_DIR/common/copy_media_utils.sh
get_adb_options "$@"
-copy_media "misc" "CtsMediaMiscTestCases-1.0"
+copy_media "misc" "CtsMediaMiscTestCases-2.0"
diff --git a/tests/tests/media/misc/src/android/media/misc/cts/WorkDir.java b/tests/tests/media/misc/src/android/media/misc/cts/WorkDir.java
index 0c39229..dbb6035 100644
--- a/tests/tests/media/misc/src/android/media/misc/cts/WorkDir.java
+++ b/tests/tests/media/misc/src/android/media/misc/cts/WorkDir.java
@@ -20,6 +20,6 @@
class WorkDir extends WorkDirBase {
public static final String getMediaDirString() {
- return getMediaDirString("CtsMediaMiscTestCases-1.0");
+ return getMediaDirString("CtsMediaMiscTestCases-2.0");
}
}
diff --git a/tests/tests/media/player/src/android/media/player/cts/MediaPlayerFlakyNetworkTest.java b/tests/tests/media/player/src/android/media/player/cts/MediaPlayerFlakyNetworkTest.java
index 81c0d89..86c6912 100644
--- a/tests/tests/media/player/src/android/media/player/cts/MediaPlayerFlakyNetworkTest.java
+++ b/tests/tests/media/player/src/android/media/player/cts/MediaPlayerFlakyNetworkTest.java
@@ -75,6 +75,7 @@
private CtsTestServer mServer;
@Before
+ @Override
public void setUp() throws Throwable {
super.setUp();
}
diff --git a/tests/tests/mediastress/src/android/mediastress/cts/MediaRecorderStressTest.java b/tests/tests/mediastress/src/android/mediastress/cts/MediaRecorderStressTest.java
index 0505d8b..5c1ec89 100644
--- a/tests/tests/mediastress/src/android/mediastress/cts/MediaRecorderStressTest.java
+++ b/tests/tests/mediastress/src/android/mediastress/cts/MediaRecorderStressTest.java
@@ -282,6 +282,10 @@
mRecorder.reset();
mRecorder.release();
output.write(", " + i);
+ if (mRemoveVideo) {
+ removeRecodedVideo(filename);
+ }
+
}
output.write("\n\n");
@@ -371,6 +375,9 @@
mRecorder.release();
Log.v(TAG, "release video recorder");
output.write(", " + i);
+ if (mRemoveVideo) {
+ removeRecodedVideo(filename);
+ }
}
output.write("\n\n");
diff --git a/tests/tests/os/Android.bp b/tests/tests/os/Android.bp
index 8bc8222..c50c6a7 100644
--- a/tests/tests/os/Android.bp
+++ b/tests/tests/os/Android.bp
@@ -40,6 +40,7 @@
"hamcrest-library",
"modules-utils-build_system",
"platformprotosnano",
+ "safety-center-internal-data",
],
jni_uses_platform_apis: true,
jni_libs: [
diff --git a/tests/tests/os/src/android/os/cts/AppHibernationUtils.kt b/tests/tests/os/src/android/os/cts/AppHibernationUtils.kt
index b16ad15..f49e065 100644
--- a/tests/tests/os/src/android/os/cts/AppHibernationUtils.kt
+++ b/tests/tests/os/src/android/os/cts/AppHibernationUtils.kt
@@ -50,17 +50,18 @@
import com.android.compatibility.common.util.click
import com.android.compatibility.common.util.depthFirstSearch
import com.android.compatibility.common.util.textAsString
+import java.io.InputStream
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
import org.hamcrest.Matcher
import org.hamcrest.Matchers
import org.junit.Assert
import org.junit.Assert.assertThat
import org.junit.Assert.assertTrue
-import java.io.InputStream
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
private const val BROADCAST_TIMEOUT_MS = 60000L
+const val PROPERTY_SAFETY_CENTER_ENABLED = "safety_center_is_enabled"
const val HIBERNATION_BOOT_RECEIVER_CLASS_NAME =
"com.android.permissioncontroller.hibernation.HibernationOnBootReceiver"
const val ACTION_SET_UP_HIBERNATION =
@@ -139,6 +140,14 @@
}
}
+fun runPermissionEventCleanupJob(context: Context) {
+ eventually {
+ runShellCommandOrThrow("cmd jobscheduler run -u " +
+ "${Process.myUserHandle().identifier} -f " +
+ "${context.packageManager.permissionControllerPackageName} 3")
+ }
+}
+
inline fun withApp(
apk: String,
packageName: String,
@@ -196,6 +205,12 @@
threshold.toString(), action)
}
+inline fun <T> withSafetyCenterEnabled(action: () -> T): T {
+ return withDeviceConfig(
+ DeviceConfig.NAMESPACE_PRIVACY, PROPERTY_SAFETY_CENTER_ENABLED,
+ true.toString(), action)
+}
+
fun awaitAppState(pkg: String, stateMatcher: Matcher<Int>) {
val context: Context = InstrumentationRegistry.getTargetContext()
eventually {
@@ -218,6 +233,7 @@
fun goHome() {
runShellCommandOrThrow("input keyevent KEYCODE_HOME")
+ waitForIdle()
}
/**
diff --git a/tests/tests/os/src/android/os/cts/AutoRevokeTest.kt b/tests/tests/os/src/android/os/cts/AutoRevokeTest.kt
index 9667a55..fdfda50 100644
--- a/tests/tests/os/src/android/os/cts/AutoRevokeTest.kt
+++ b/tests/tests/os/src/android/os/cts/AutoRevokeTest.kt
@@ -28,7 +28,11 @@
import android.content.res.Resources
import android.net.Uri
import android.os.Build
+import android.os.UserHandle
import android.platform.test.annotations.AppModeFull
+import android.provider.DeviceConfig
+import android.safetycenter.SafetyCenterIssue
+import android.safetycenter.SafetyCenterManager
import android.support.test.uiautomator.By
import android.support.test.uiautomator.BySelector
import android.support.test.uiautomator.UiObject2
@@ -38,6 +42,7 @@
import androidx.test.InstrumentationRegistry
import androidx.test.filters.SdkSuppress
import androidx.test.runner.AndroidJUnit4
+import com.android.compatibility.common.util.DeviceConfigStateChangerRule
import com.android.compatibility.common.util.DisableAnimationRule
import com.android.compatibility.common.util.FreezeRotationRule
import com.android.compatibility.common.util.MatcherUtils.hasTextThat
@@ -52,6 +57,13 @@
import com.android.compatibility.common.util.depthFirstSearch
import com.android.compatibility.common.util.uiDump
import com.android.modules.utils.build.SdkLevel
+import com.android.safetycenter.internaldata.SafetyCenterIds
+import com.android.safetycenter.internaldata.SafetyCenterIssueId
+import com.android.safetycenter.internaldata.SafetyCenterIssueKey
+import java.lang.reflect.Modifier
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+import java.util.regex.Pattern
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.containsStringIgnoringCase
import org.hamcrest.CoreMatchers.equalTo
@@ -69,10 +81,6 @@
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import java.lang.reflect.Modifier
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.atomic.AtomicReference
-import java.util.regex.Pattern
private const val READ_CALENDAR = "android.permission.READ_CALENDAR"
private const val BLUETOOTH_CONNECT = "android.permission.BLUETOOTH_CONNECT"
@@ -82,7 +90,6 @@
*/
@RunWith(AndroidJUnit4::class)
class AutoRevokeTest {
-
private val context: Context = InstrumentationRegistry.getTargetContext()
private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
@@ -94,8 +101,16 @@
private lateinit var preMinVersionApkPath: String
private lateinit var preMinVersionAppPackageName: String
+ @Rule
+ @JvmField
+ val storeExactTimeRule = DeviceConfigStateChangerRule(context,
+ DeviceConfig.NAMESPACE_PERMISSIONS, STORE_EXACT_TIME_KEY, "true")
+
companion object {
const val LOG_TAG = "AutoRevokeTest"
+ private const val STORE_EXACT_TIME_KEY = "permission_changes_store_exact_time"
+ private const val UNUSED_APPS_SOURCE_ID = "AndroidPermissionAutoRevoke"
+ private const val UNUSED_APPS_ISSUE_ID = "unused_apps_issue"
@JvmStatic
@BeforeClass
@@ -257,6 +272,61 @@
@AppModeFull(reason = "Uses separate apps for testing")
@Test
+ fun testAppWithPermissionsChangedRecently_doesNotGetPermissionRevoked() {
+ val unusedThreshold = 15_000L
+ withUnusedThresholdMs(unusedThreshold) {
+ withDummyApp {
+ // Setup
+ // Ensure app is considered unused and then change permission
+ Thread.sleep(unusedThreshold)
+ goToPermissions()
+ click("Calendar")
+ click("Allow")
+ goBack()
+ goBack()
+ goBack()
+
+ // Run
+ runAppHibernationJob(context, LOG_TAG)
+
+ // Verify that permission is not revoked because the permission was changed
+ // within the unused threshold even though the app itself is unused
+ assertPermission(PERMISSION_GRANTED)
+ }
+ }
+ }
+
+ @AppModeFull(reason = "Uses separate apps for testing")
+ @Test
+ fun testPermissionEventCleanupService_scrubsEvents() {
+ val unusedThreshold = 15_000L
+ withUnusedThresholdMs(unusedThreshold) {
+ withDummyApp {
+ // Setup
+ // Ensure app is considered unused
+ Thread.sleep(unusedThreshold)
+ goToPermissions()
+ click("Calendar")
+ click("Allow")
+ goBack()
+ goBack()
+ goBack()
+ // Run with threshold where events would be cleaned up
+ withUnusedThresholdMs(0) {
+ runPermissionEventCleanupJob(context)
+ Thread.sleep(3000L)
+ }
+
+ runAppHibernationJob(context, LOG_TAG)
+
+ // Verify that permission is revoked because there are no recent permission changes
+ assertPermission(PERMISSION_DENIED)
+ }
+ }
+ }
+
+ @AppModeFull(reason = "Uses separate apps for testing")
+ @Test
fun testPreMinAutoRevokeVersionUnusedApp_doesntGetPermissionRevoked() {
withUnusedThresholdMs(3L) {
withDummyApp(preMinVersionApkPath, preMinVersionAppPackageName) {
@@ -376,6 +446,88 @@
}
}
+ @AppModeFull(reason = "Uses separate apps for testing")
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
+ @Test
+ fun testAutoRevoke_showsUpInSafetyCenter() {
+ withSafetyCenterEnabled {
+ withUnusedThresholdMs(3L) {
+ withDummyApp {
+ startAppAndAcceptPermission()
+
+ killDummyApp()
+
+ // Run
+ runAppHibernationJob(context, LOG_TAG)
+
+ // Verify
+ val safetyCenterManager =
+ context.getSystemService(SafetyCenterManager::class.java)!!
+ eventually {
+ val issues = ArrayList<SafetyCenterIssue>()
+ runWithShellPermissionIdentity {
+ val safetyCenterData = safetyCenterManager!!.safetyCenterData
+ issues.addAll(safetyCenterData.issues)
+ }
+ val issueId = SafetyCenterIds.encodeToString(
+ SafetyCenterIssueId.newBuilder()
+ .setSafetyCenterIssueKey(SafetyCenterIssueKey.newBuilder()
+ .setSafetySourceId(UNUSED_APPS_SOURCE_ID)
+ .setSafetySourceIssueId(UNUSED_APPS_ISSUE_ID)
+ .setUserId(UserHandle.myUserId())
+ .build())
+ .setIssueTypeId(UNUSED_APPS_ISSUE_ID)
+ .build())
+ assertTrue(issues.any { it.id == issueId })
+ }
+ }
+ }
+ }
+ }
+
+ @AppModeFull(reason = "Uses separate apps for testing")
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
+ @Test
+ fun testAutoRevoke_goToUnusedAppsPage_removesSafetyCenterIssue() {
+ withSafetyCenterEnabled {
+ withUnusedThresholdMs(3L) {
+ withDummyApp {
+ startAppAndAcceptPermission()
+
+ killDummyApp()
+
+ // Run
+ runAppHibernationJob(context, LOG_TAG)
+
+ // Go to unused apps page
+ openUnusedAppsNotification()
+ waitFindObject(By.text(supportedAppPackageName))
+
+ // Verify
+ val safetyCenterManager =
+ context.getSystemService(SafetyCenterManager::class.java)!!
+ eventually {
+ val issues = ArrayList<SafetyCenterIssue>()
+ runWithShellPermissionIdentity {
+ val safetyCenterData = safetyCenterManager!!.safetyCenterData
+ issues.addAll(safetyCenterData.issues)
+ }
+ val issueId = SafetyCenterIds.encodeToString(
+ SafetyCenterIssueId.newBuilder()
+ .setSafetyCenterIssueKey(SafetyCenterIssueKey.newBuilder()
+ .setSafetySourceId(UNUSED_APPS_SOURCE_ID)
+ .setSafetySourceIssueId(UNUSED_APPS_ISSUE_ID)
+ .setUserId(UserHandle.myUserId())
+ .build())
+ .setIssueTypeId(UNUSED_APPS_ISSUE_ID)
+ .build())
+ assertFalse(issues.any { it.id == issueId })
+ }
+ }
+ }
+ }
+ }
+
private fun isAutomotiveDevice(): Boolean {
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
}
@@ -440,6 +592,7 @@
waitFindObject(By.res("com.android.permissioncontroller:id/permission_allow_button"))
.click()
}
+ waitForIdle()
}
private fun clickUninstallIcon() {
diff --git a/tests/tests/permission/Android.bp b/tests/tests/permission/Android.bp
index eae7276..5c861b5 100644
--- a/tests/tests/permission/Android.bp
+++ b/tests/tests/permission/Android.bp
@@ -47,6 +47,8 @@
// which provides assertThrows
"testng",
"bluetooth-test-util-lib",
+ "CtsAccessibilityCommon",
+ "safety-center-internal-data",
],
jni_libs: [
"libctspermission_jni",
diff --git a/tests/tests/permission/AndroidManifest.xml b/tests/tests/permission/AndroidManifest.xml
index dc19893..8f3d39b 100644
--- a/tests/tests/permission/AndroidManifest.xml
+++ b/tests/tests/permission/AndroidManifest.xml
@@ -71,6 +71,15 @@
<action android:name="android.service.notification.NotificationListenerService"/>
</intent-filter>
</service>
+ <service android:name=".AccessibilityTestService"
+ android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.accessibilityservice.AccessibilityService"/>
+ </intent-filter>
+ <meta-data android:name="android.accessibilityservice"
+ android:resource="@xml/test_accessibilityservice"/>
+ </service>
</application>
<!--
diff --git a/tests/tests/permission/AndroidTest.xml b/tests/tests/permission/AndroidTest.xml
index 58ee52f..56a72c9 100644
--- a/tests/tests/permission/AndroidTest.xml
+++ b/tests/tests/permission/AndroidTest.xml
@@ -91,6 +91,7 @@
<option name="push" value="CtsStorageEscalationApp28.apk->/data/local/tmp/cts/permissions/CtsStorageEscalationApp28.apk" />
<option name="push" value="CtsStorageEscalationApp29Full.apk->/data/local/tmp/cts/permissions/CtsStorageEscalationApp29Full.apk" />
<option name="push" value="CtsStorageEscalationApp29Scoped.apk->/data/local/tmp/cts/permissions/CtsStorageEscalationApp29Scoped.apk" />
+ <option name="push" value="CtsAppThatHasNotificationListener.apk->/data/local/tmp/cts/permissions/CtsAppThatHasNotificationListener.apk" />
</target_preparer>
<!-- Remove additional apps if installed -->
@@ -102,6 +103,7 @@
<option name="teardown-command" value="pm uninstall android.permission.cts.revokepermissionwhenremoved.userapp" />
<option name="teardown-command" value="pm uninstall android.permission.cts.revokepermissionwhenremoved.runtimepermissiondefinerapp" />
<option name="teardown-command" value="pm uninstall android.permission.cts.revokepermissionwhenremoved.runtimepermissionuserapp" />
+ <option name="teardown-command" value="pm uninstall android.permission.cts.appthathasnotificationlistener" />
</target_preparer>
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
diff --git a/tests/tests/permission/AppThatHasNotificationListener/Android.bp b/tests/tests/permission/AppThatHasNotificationListener/Android.bp
new file mode 100644
index 0000000..419ab5d
--- /dev/null
+++ b/tests/tests/permission/AppThatHasNotificationListener/Android.bp
@@ -0,0 +1,39 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+ name: "CtsAppThatHasNotificationListener",
+ defaults: [
+ "cts_defaults",
+ "mts-target-sdk-version-current",
+ ],
+ sdk_version: "current",
+ min_sdk_version: "30",
+ // Tag this module as a cts test artifact
+ test_suites: [
+ "cts",
+ "general-tests",
+ "sts",
+ "mts-permission",
+ ],
+ srcs: [
+ "src/**/*.java",
+ ],
+}
diff --git a/tests/tests/permission/AppThatHasNotificationListener/AndroidManifest.xml b/tests/tests/permission/AppThatHasNotificationListener/AndroidManifest.xml
new file mode 100644
index 0000000..03d23df
--- /dev/null
+++ b/tests/tests/permission/AppThatHasNotificationListener/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.permission.cts.appthathasnotificationlistener"
+ android:versionCode="1">
+
+ <application android:label="CtsNotificationListener">
+ <service
+ android:name=".CtsNotificationListenerService"
+ android:label="CtsNotificationListener"
+ android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.service.notification.NotificationListenerService"/>
+ </intent-filter>
+ </service>
+ </application>
+</manifest>
diff --git a/tests/tests/permission/AppThatHasNotificationListener/src/android/permission/cts/appthathasnotificationlistener/CtsNotificationListenerService.java b/tests/tests/permission/AppThatHasNotificationListener/src/android/permission/cts/appthathasnotificationlistener/CtsNotificationListenerService.java
new file mode 100644
index 0000000..2bd423e
--- /dev/null
+++ b/tests/tests/permission/AppThatHasNotificationListener/src/android/permission/cts/appthathasnotificationlistener/CtsNotificationListenerService.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.permission.cts.appthathasnotificationlistener;
+
+import android.service.notification.NotificationListenerService;
+
+public class CtsNotificationListenerService extends NotificationListenerService {}
diff --git a/tests/tests/permission/permissionTestUtilLib/src/android/permission/cts/TestUtils.java b/tests/tests/permission/permissionTestUtilLib/src/android/permission/cts/TestUtils.java
index dfcc38e..8dd3e96 100644
--- a/tests/tests/permission/permissionTestUtilLib/src/android/permission/cts/TestUtils.java
+++ b/tests/tests/permission/permissionTestUtilLib/src/android/permission/cts/TestUtils.java
@@ -16,9 +16,16 @@
package android.permission.cts;
+import android.app.UiAutomation;
+import android.os.Process;
import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.SystemUtil;
+
+import org.junit.Assert;
/** Common test utilities */
public class TestUtils {
@@ -123,4 +130,42 @@
}
}
}
+
+ /**
+ * Run the job and then wait for completion
+ */
+ public static void runJobAndWaitUntilCompleted(
+ String packageName,
+ int jobId, long timeout) {
+ runJobAndWaitUntilCompleted(packageName, jobId, timeout,
+ InstrumentationRegistry.getInstrumentation().getUiAutomation());
+ }
+
+ /**
+ * Run the job and then wait for completion
+ */
+ public static void runJobAndWaitUntilCompleted(
+ String packageName,
+ int jobId,
+ long timeout,
+ UiAutomation automation) {
+ String runJobCmd = "cmd jobscheduler run -u " + Process.myUserHandle().getIdentifier()
+ + " -f " + packageName + " " + jobId;
+ String statusCmd = "cmd jobscheduler get-job-state -u "
+ + Process.myUserHandle().getIdentifier() + " " + packageName + " " + jobId;
+
+ SystemUtil.runWithShellPermissionIdentity(automation, () -> {
+ SystemUtil.runShellCommand(automation, runJobCmd);
+ Thread.sleep(500);
+ try {
+ eventually(() -> Assert.assertEquals(
+ "The job is probably still running",
+ "waiting",
+ SystemUtil.runShellCommand(automation, statusCmd).trim()),
+ timeout);
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
}
diff --git a/tests/tests/permission/res/xml/test_accessibilityservice.xml b/tests/tests/permission/res/xml/test_accessibilityservice.xml
new file mode 100644
index 0000000..fa87e2e
--- /dev/null
+++ b/tests/tests/permission/res/xml/test_accessibilityservice.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
+ android:accessibilityEventTypes="typeAllMask"
+ android:accessibilityFeedbackType="feedbackGeneric"
+ android:canRetrieveWindowContent="true"
+ android:accessibilityFlags="flagDefault"
+ android:notificationTimeout="0" />
diff --git a/tests/tests/permission/src/android/permission/cts/AccessibilityPrivacySourceTest.kt b/tests/tests/permission/src/android/permission/cts/AccessibilityPrivacySourceTest.kt
new file mode 100644
index 0000000..513e1df
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/AccessibilityPrivacySourceTest.kt
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.permission.cts
+
+import android.accessibility.cts.common.InstrumentedAccessibilityService
+import android.accessibility.cts.common.InstrumentedAccessibilityServiceTestRule
+import android.app.Instrumentation
+import android.app.UiAutomation
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.Process
+import android.permission.cts.NotificationListenerUtils.assertEmptyNotification
+import android.permission.cts.NotificationListenerUtils.assertNotificationExist
+import android.permission.cts.NotificationListenerUtils.cancelNotification
+import android.permission.cts.NotificationListenerUtils.cancelNotifications
+import android.permission.cts.NotificationListenerUtils.getNotification
+import android.permission.cts.SafetyCenterUtils.assertSafetyCenterIssueDoesNotExist
+import android.permission.cts.SafetyCenterUtils.assertSafetyCenterIssueExist
+import android.permission.cts.SafetyCenterUtils.assertSafetyCenterStarted
+import android.permission.cts.SafetyCenterUtils.deleteDeviceConfigPrivacyProperty
+import android.permission.cts.SafetyCenterUtils.deviceSupportsSafetyCenter
+import android.permission.cts.SafetyCenterUtils.setDeviceConfigPrivacyProperty
+import android.platform.test.annotations.AppModeFull
+import android.provider.DeviceConfig
+import android.safetycenter.SafetyCenterManager
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.runner.AndroidJUnit4
+import com.android.compatibility.common.util.DeviceConfigStateChangerRule
+import com.android.compatibility.common.util.ProtoUtils
+import com.android.compatibility.common.util.SystemUtil.runShellCommand
+import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
+import com.android.server.job.nano.JobSchedulerServiceDumpProto
+import org.junit.After
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.Before
+import org.junit.ClassRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@AppModeFull(
+ reason = "Cannot set system settings as instant app. Also we never show an accessibility " +
+ "notification for instant apps."
+)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
+class AccessibilityPrivacySourceTest {
+
+ private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val context: Context = instrumentation.targetContext
+ private val mAccessibilityServiceRule =
+ InstrumentedAccessibilityServiceTestRule(AccessibilityTestService::class.java, false)
+ private val permissionControllerPackage = context.packageManager.permissionControllerPackageName
+ private val accessibilityTestService =
+ ComponentName(context, AccessibilityTestService::class.java).flattenToString()
+ private val safetyCenterIssueId = "accessibility_$accessibilityTestService"
+ private val safetyCenterManager = context.getSystemService(SafetyCenterManager::class.java)
+
+ @get:Rule
+ val deviceConfigSafetyCenterEnabled =
+ DeviceConfigStateChangerRule(
+ context, DeviceConfig.NAMESPACE_PRIVACY, SAFETY_CENTER_ENABLED, true.toString())
+
+ @get:Rule
+ val deviceConfigA11ySourceEnabled =
+ DeviceConfigStateChangerRule(
+ context, DeviceConfig.NAMESPACE_PRIVACY, ACCESSIBILITY_SOURCE_ENABLED, true.toString())
+
+ @get:Rule
+ val deviceConfigA11yListenerDisabled =
+ DeviceConfigStateChangerRule(
+ context,
+ DeviceConfig.NAMESPACE_PRIVACY,
+ ACCESSIBILITY_LISTENER_ENABLED,
+ false.toString())
+
+ @Before
+ fun setup() {
+ Assume.assumeTrue(deviceSupportsSafetyCenter(context))
+ InstrumentedAccessibilityService.disableAllServices()
+ runShellCommand("input keyevent KEYCODE_WAKEUP")
+ resetPermissionController()
+ cancelNotifications(permissionControllerPackage)
+ }
+
+ @After
+ fun cleanup() {
+ cancelNotifications(permissionControllerPackage)
+ runWithShellPermissionIdentity { safetyCenterManager?.clearAllSafetySourceDataForTests() }
+ }
+
+ @Test
+ fun testJobSendsNotification() {
+ mAccessibilityServiceRule.enableService()
+ runJobAndWaitUntilCompleted()
+ assertNotificationExist(permissionControllerPackage, ACCESSIBILITY_NOTIFICATION_ID)
+ }
+
+ @Test
+ fun testJobSendsNotificationOnEnable() {
+ mAccessibilityServiceRule.enableService()
+ runJobAndWaitUntilCompleted()
+ assertNotificationExist(permissionControllerPackage, ACCESSIBILITY_NOTIFICATION_ID)
+
+ setDeviceConfigPrivacyProperty(ACCESSIBILITY_LISTENER_ENABLED, true.toString())
+ cancelNotification(permissionControllerPackage, ACCESSIBILITY_NOTIFICATION_ID)
+ InstrumentedAccessibilityService.disableAllServices()
+ setDeviceConfigPrivacyProperty(ACCESSIBILITY_LISTENER_ENABLED, false.toString())
+ setDeviceConfigPrivacyProperty(ACCESSIBILITY_JOB_INTERVAL_MILLIS, "0")
+
+ // enable service again and verify a notification
+ try {
+ mAccessibilityServiceRule.enableService()
+ runJobAndWaitUntilCompleted()
+ assertNotificationExist(permissionControllerPackage, ACCESSIBILITY_NOTIFICATION_ID)
+ } finally {
+ deleteDeviceConfigPrivacyProperty(ACCESSIBILITY_JOB_INTERVAL_MILLIS)
+ }
+ }
+
+ @Test
+ fun testJobSendsIssuesToSafetyCenter() {
+ mAccessibilityServiceRule.enableService()
+ runJobAndWaitUntilCompleted()
+ assertSafetyCenterIssueExist(
+ SC_ACCESSIBILITY_SOURCE_ID, safetyCenterIssueId, SC_ACCESSIBILITY_ISSUE_TYPE_ID)
+ }
+
+ @Test
+ fun testJobDoesNotSendNotificationInSecondRunForSameService() {
+ mAccessibilityServiceRule.enableService()
+ runJobAndWaitUntilCompleted()
+ assertNotificationExist(permissionControllerPackage, ACCESSIBILITY_NOTIFICATION_ID)
+
+ cancelNotification(permissionControllerPackage, ACCESSIBILITY_NOTIFICATION_ID)
+
+ runJobAndWaitUntilCompleted()
+ assertEmptyNotification(permissionControllerPackage, ACCESSIBILITY_NOTIFICATION_ID)
+ }
+
+ @Test
+ fun testAccessibilityListenerSendsIssueToSafetyCenter() {
+ setDeviceConfigPrivacyProperty(ACCESSIBILITY_LISTENER_ENABLED, true.toString())
+ val automation = getAutomation()
+ mAccessibilityServiceRule.enableService()
+ TestUtils.eventually(
+ {
+ assertSafetyCenterIssueExist(
+ SC_ACCESSIBILITY_SOURCE_ID,
+ safetyCenterIssueId,
+ SC_ACCESSIBILITY_ISSUE_TYPE_ID,
+ automation)
+ },
+ TIMEOUT_MILLIS)
+ automation.destroy()
+ }
+
+ @Test
+ fun testJobWithDisabledServiceDoesNotSendNotification() {
+ runJobAndWaitUntilCompleted()
+ assertEmptyNotification(permissionControllerPackage, ACCESSIBILITY_NOTIFICATION_ID)
+ }
+
+ @Test
+ fun testJobWithDisabledServiceDoesNotSendIssueToSafetyCenter() {
+ runJobAndWaitUntilCompleted()
+ assertSafetyCenterIssueDoesNotExist(
+ SC_ACCESSIBILITY_SOURCE_ID, safetyCenterIssueId, SC_ACCESSIBILITY_ISSUE_TYPE_ID)
+ }
+
+ @Test
+ fun testJobWithAccessibilityFeatureDisabledDoesNotSendNotification() {
+ setDeviceConfigPrivacyProperty(ACCESSIBILITY_SOURCE_ENABLED, false.toString())
+ mAccessibilityServiceRule.enableService()
+ runJobAndWaitUntilCompleted()
+ assertEmptyNotification(permissionControllerPackage, ACCESSIBILITY_NOTIFICATION_ID)
+ }
+
+ @Test
+ fun testJobWithAccessibilityFeatureDisabledDoesNotSendIssueToSafetyCenter() {
+ setDeviceConfigPrivacyProperty(ACCESSIBILITY_SOURCE_ENABLED, false.toString())
+ mAccessibilityServiceRule.enableService()
+ runJobAndWaitUntilCompleted()
+ assertSafetyCenterIssueDoesNotExist(
+ SC_ACCESSIBILITY_SOURCE_ID, safetyCenterIssueId, SC_ACCESSIBILITY_ISSUE_TYPE_ID)
+ }
+
+ @Test
+ fun testJobWithSafetyCenterDisabledDoesNotSendNotification() {
+ setDeviceConfigPrivacyProperty(SAFETY_CENTER_ENABLED, false.toString())
+ mAccessibilityServiceRule.enableService()
+ runJobAndWaitUntilCompleted()
+ assertEmptyNotification(permissionControllerPackage, ACCESSIBILITY_NOTIFICATION_ID)
+ }
+
+ @Test
+ fun testJobWithSafetyCenterDisabledDoesNotSendIssueToSafetyCenter() {
+ setDeviceConfigPrivacyProperty(SAFETY_CENTER_ENABLED, false.toString())
+ mAccessibilityServiceRule.enableService()
+ runJobAndWaitUntilCompleted()
+ assertSafetyCenterIssueDoesNotExist(
+ SC_ACCESSIBILITY_SOURCE_ID, safetyCenterIssueId, SC_ACCESSIBILITY_ISSUE_TYPE_ID)
+ }
+
+ @Test
+ fun testNotificationClickOpenSafetyCenter() {
+ mAccessibilityServiceRule.enableService()
+ runJobAndWaitUntilCompleted()
+ val statusBarNotification =
+ getNotification(permissionControllerPackage, ACCESSIBILITY_NOTIFICATION_ID)
+ Assert.assertNotNull(statusBarNotification)
+ val contentIntent = statusBarNotification!!.notification.contentIntent
+ contentIntent.send()
+ assertSafetyCenterStarted()
+ }
+
+ private fun getAutomation(): UiAutomation {
+ return instrumentation.getUiAutomation(
+ UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES)
+ }
+
+ private fun runJobAndWaitUntilCompleted() {
+ TestUtils.runJobAndWaitUntilCompleted(
+ permissionControllerPackage, ACCESSIBILITY_JOB_ID, TIMEOUT_MILLIS, getAutomation())
+ }
+
+ /** Reset the permission controllers state. */
+ @Throws(Throwable::class)
+ private fun resetPermissionController() {
+ PermissionUtils.clearAppState(permissionControllerPackage)
+ val currentUserId = Process.myUserHandle().identifier
+
+ // Wait until jobs are cleared
+ TestUtils.eventually(
+ {
+ val dump = getJobSchedulerDump()
+ for (job in dump!!.registeredJobs) {
+ if (job.dump.sourceUserId == currentUserId &&
+ job.dump.sourcePackageName == permissionControllerPackage) {
+ Assert.assertFalse(
+ job.dump.jobInfo.service.className.contains("AccessibilityJobService"))
+ }
+ }
+ },
+ TIMEOUT_MILLIS)
+
+ runShellCommand(
+ "cmd jobscheduler reset-execution-quota -u " +
+ "${Process.myUserHandle().identifier} $permissionControllerPackage")
+
+ // Setup up permission controller again (simulate a reboot)
+ val permissionControllerSetupIntent =
+ Intent(ACTION_SET_UP_ACCESSIBILITY_CHECK).apply {
+ setPackage(permissionControllerPackage)
+ setFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+ }
+
+ // Query for the setup broadcast receiver
+ val resolveInfos =
+ context.packageManager.queryBroadcastReceivers(permissionControllerSetupIntent, 0)
+
+ if (resolveInfos.size > 0) {
+ context.sendBroadcast(permissionControllerSetupIntent)
+ } else {
+ context.sendBroadcast(
+ Intent().apply {
+ setClassName(permissionControllerPackage, AccessibilityOnBootReceiver)
+ setFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+ setPackage(permissionControllerPackage)
+ })
+ }
+
+ // Wait until jobs are set up
+ TestUtils.eventually(
+ {
+ val dump = getJobSchedulerDump()
+ for (job in dump!!.registeredJobs) {
+ if (job.dump.sourceUserId == currentUserId &&
+ job.dump.sourcePackageName == permissionControllerPackage &&
+ job.dump.jobInfo.service.className.contains("AccessibilityJobService")) {
+ return@eventually
+ }
+ }
+ Assert.fail("accessibility job not found")
+ },
+ TIMEOUT_MILLIS)
+ }
+
+ @Throws(Exception::class)
+ private fun getJobSchedulerDump(): JobSchedulerServiceDumpProto? {
+ return ProtoUtils.getProto(
+ getAutomation(),
+ JobSchedulerServiceDumpProto::class.java,
+ ProtoUtils.DUMPSYS_JOB_SCHEDULER)
+ }
+
+ companion object {
+ private const val SC_ACCESSIBILITY_SOURCE_ID = "AndroidAccessibility"
+ private const val ACCESSIBILITY_SOURCE_ENABLED = "sc_accessibility_source_enabled"
+ private const val SAFETY_CENTER_ENABLED = "safety_center_is_enabled"
+ private const val ACCESSIBILITY_LISTENER_ENABLED = "sc_accessibility_listener_enabled"
+ private const val ACCESSIBILITY_JOB_INTERVAL_MILLIS = "sc_accessibility_job_interval_millis"
+
+ private const val ACCESSIBILITY_JOB_ID = 6
+ private const val ACCESSIBILITY_NOTIFICATION_ID = 4
+ private const val TIMEOUT_MILLIS: Long = 10000
+
+ private const val SC_ACCESSIBILITY_ISSUE_TYPE_ID = "accessibility_privacy_issue"
+
+ private const val AccessibilityOnBootReceiver =
+ "com.android.permissioncontroller.privacysources.AccessibilityOnBootReceiver"
+ private const val ACTION_SET_UP_ACCESSIBILITY_CHECK =
+ "com.android.permissioncontroller.action.SET_UP_ACCESSIBILITY_CHECK"
+
+ @get:ClassRule
+ @JvmStatic
+ val ctsNotificationListenerHelper =
+ CtsNotificationListenerHelperRule(
+ InstrumentationRegistry.getInstrumentation().targetContext)
+ }
+}
diff --git a/tests/tests/permission/src/android/permission/cts/AccessibilityTestService.kt b/tests/tests/permission/src/android/permission/cts/AccessibilityTestService.kt
new file mode 100644
index 0000000..9f5e3f1
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/AccessibilityTestService.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.permission.cts
+
+import android.accessibility.cts.common.InstrumentedAccessibilityService
+
+/**
+ * Test Accessibility Service
+ */
+class AccessibilityTestService : InstrumentedAccessibilityService()
diff --git a/tests/tests/permission/src/android/permission/cts/BaseNotificationListenerCheckTest.java b/tests/tests/permission/src/android/permission/cts/BaseNotificationListenerCheckTest.java
new file mode 100644
index 0000000..f88b7ec
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/BaseNotificationListenerCheckTest.java
@@ -0,0 +1,449 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.permission.cts;
+
+import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+import static android.content.Intent.FLAG_RECEIVER_FOREGROUND;
+import static android.os.Process.myUserHandle;
+import static android.permission.cts.PermissionUtils.clearAppState;
+import static android.permission.cts.TestUtils.eventually;
+
+import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+import static com.android.server.job.nano.JobPackageHistoryProto.START_PERIODIC_JOB;
+import static com.android.server.job.nano.JobPackageHistoryProto.STOP_PERIODIC_JOB;
+
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import static java.lang.Math.max;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.app.NotificationManager;
+import android.app.UiAutomation;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Build;
+import android.platform.test.annotations.AppModeFull;
+import android.provider.DeviceConfig;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.StatusBarNotification;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.DeviceConfigStateChangerRule;
+import com.android.compatibility.common.util.ProtoUtils;
+import com.android.server.job.nano.JobPackageHistoryProto;
+import com.android.server.job.nano.JobSchedulerServiceDumpProto;
+import com.android.server.job.nano.JobSchedulerServiceDumpProto.RegisteredJob;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+/**
+ * Base test class used for {@code NotificationListenerCheckTest} and
+ * {@code NotificationListenerCheckWithSafetyCenterUnsupportedTest}
+ */
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "Cannot set system settings as instant app. Also we never show a notification"
+ + " listener check notification for instant apps.")
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
+public class BaseNotificationListenerCheckTest {
+ private static final String LOG_TAG = BaseNotificationListenerCheckTest.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ protected static final String TEST_APP_PKG =
+ "android.permission.cts.appthathasnotificationlistener";
+ private static final String TEST_APP_NOTIFICATION_SERVICE =
+ TEST_APP_PKG + ".CtsNotificationListenerService";
+ protected static final String TEST_APP_NOTIFICATION_LISTENER_APK =
+ "/data/local/tmp/cts/permissions/CtsAppThatHasNotificationListener.apk";
+
+ private static final int NOTIFICATION_LISTENER_CHECK_JOB_ID = 4;
+
+ /**
+ * Device config property for whether notification listener check is enabled on the device
+ */
+ private static final String PROPERTY_NOTIFICATION_LISTENER_CHECK_ENABLED =
+ "notification_listener_check_enabled";
+
+ /**
+ * Device config property for time period in milliseconds after which current enabled
+ * notification
+ * listeners are queried
+ */
+ private static final String PROPERTY_NOTIFICATION_LISTENER_CHECK_INTERVAL_MILLIS =
+ "notification_listener_check_interval_millis";
+
+ protected static final Long OVERRIDE_NOTIFICATION_LISTENER_CHECK_INTERVAL_MILLIS =
+ SECONDS.toMillis(1);
+
+ private static final String PROPERTY_JOB_SCHEDULER_MAX_JOB_PER_RATE_LIMIT_WINDOW =
+ "qc_max_job_count_per_rate_limiting_window";
+
+ private static final String PROPERTY_JOB_SCHEDULER_RATE_LIMIT_WINDOW_MILLIS =
+ "qc_rate_limiting_window_ms";
+
+ private static final String ACTION_SET_UP_NOTIFICATION_LISTENER_CHECK =
+ "com.android.permissioncontroller.action.SET_UP_NOTIFICATION_LISTENER_CHECK";
+ private static final String NotificationListenerOnBootReceiver =
+ "com.android.permissioncontroller.privacysources.SetupPeriodicNotificationListenerCheck";
+
+ /**
+ * ID for notification shown by
+ * {@link com.android.permissioncontroller.privacysources.NotificationListenerCheck}.
+ */
+ public static final int NOTIFICATION_LISTENER_CHECK_NOTIFICATION_ID = 3;
+
+ protected static final long UNEXPECTED_TIMEOUT_MILLIS = 10000;
+ protected static final long ENSURE_NOTIFICATION_NOT_SHOWN_EXPECTED_TIMEOUT_MILLIS = 5000;
+
+ private static final Context sContext = InstrumentationRegistry.getTargetContext();
+ private static final PackageManager sPackageManager = sContext.getPackageManager();
+ private static final UiAutomation sUiAutomation = InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation();
+
+ private static final String PERMISSION_CONTROLLER_PKG = sContext.getPackageManager()
+ .getPermissionControllerPackageName();
+
+ private static List<ComponentName> sPreviouslyEnabledNotificationListeners;
+
+ // Override SafetyCenter enabled flag
+ @Rule
+ public DeviceConfigStateChangerRule sPrivacyDeviceConfigSafetyCenterEnabled =
+ new DeviceConfigStateChangerRule(sContext,
+ DeviceConfig.NAMESPACE_PRIVACY,
+ SafetyCenterUtils.PROPERTY_SAFETY_CENTER_ENABLED,
+ Boolean.toString(true));
+
+ // Override NlsCheck enabled flag
+ @Rule
+ public DeviceConfigStateChangerRule sPrivacyDeviceConfigNlsCheckEnabled =
+ new DeviceConfigStateChangerRule(sContext,
+ DeviceConfig.NAMESPACE_PRIVACY,
+ PROPERTY_NOTIFICATION_LISTENER_CHECK_ENABLED,
+ Boolean.toString(true));
+
+ // Override general notification interval from once every day to once ever 1 second
+ @Rule
+ public DeviceConfigStateChangerRule sPrivacyDeviceConfigNlsCheckIntervalMillis =
+ new DeviceConfigStateChangerRule(sContext,
+ DeviceConfig.NAMESPACE_PRIVACY,
+ PROPERTY_NOTIFICATION_LISTENER_CHECK_INTERVAL_MILLIS,
+ Long.toString(OVERRIDE_NOTIFICATION_LISTENER_CHECK_INTERVAL_MILLIS));
+
+ // Disable job scheduler throttling by allowing 300000 jobs per 30 sec
+ @Rule
+ public DeviceConfigStateChangerRule sJobSchedulerDeviceConfig1 =
+ new DeviceConfigStateChangerRule(sContext,
+ DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+ PROPERTY_JOB_SCHEDULER_MAX_JOB_PER_RATE_LIMIT_WINDOW,
+ Integer.toString(3000000));
+
+ // Disable job scheduler throttling by allowing 300000 jobs per 30 sec
+ @Rule
+ public DeviceConfigStateChangerRule sJobSchedulerDeviceConfig2 =
+ new DeviceConfigStateChangerRule(sContext,
+ DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+ PROPERTY_JOB_SCHEDULER_RATE_LIMIT_WINDOW_MILLIS,
+ Integer.toString(30000));
+
+ @Rule
+ public CtsNotificationListenerHelperRule ctsNotificationListenerHelper =
+ new CtsNotificationListenerHelperRule(sContext);
+
+ @BeforeClass
+ public static void beforeClassSetup() throws Exception {
+ // Disallow any OEM enabled NLS
+ disallowPreexistingNotificationListeners();
+ }
+
+ @AfterClass
+ public static void afterClassTearDown() throws Throwable {
+ // Reallow any previously OEM allowed NLS
+ reallowPreexistingNotificationListeners();
+ }
+
+ private static void setDeviceConfigPrivacyProperty(String propertyName, String value) {
+ runWithShellPermissionIdentity(() -> {
+ boolean valueWasSet = DeviceConfig.setProperty(
+ DeviceConfig.NAMESPACE_PRIVACY,
+ /* name = */ propertyName,
+ /* value = */ value,
+ /* makeDefault = */ false);
+ if (!valueWasSet) {
+ throw new IllegalStateException("Could not set " + propertyName + " to " + value);
+ }
+ }, WRITE_DEVICE_CONFIG);
+ }
+
+ /**
+ * Enable or disable notification listener check
+ */
+ protected static void setNotificationListenerCheckEnabled(boolean enabled) {
+ setDeviceConfigPrivacyProperty(
+ PROPERTY_NOTIFICATION_LISTENER_CHECK_ENABLED,
+ /* value = */ String.valueOf(enabled));
+ }
+
+ /**
+ * Allow or disallow a {@link NotificationListenerService} component for the current user
+ *
+ * @param listenerComponent {@link NotificationListenerService} component to allow or disallow
+ */
+ private static void setNotificationListenerServiceAllowed(ComponentName listenerComponent,
+ boolean allowed) {
+ String command = " cmd notification " + (allowed ? "allow_listener " : "disallow_listener ")
+ + listenerComponent.flattenToString();
+ runShellCommand(command);
+ }
+
+ private static void disallowPreexistingNotificationListeners() {
+ runWithShellPermissionIdentity(() -> {
+ NotificationManager notificationManager =
+ sContext.getSystemService(NotificationManager.class);
+ sPreviouslyEnabledNotificationListeners =
+ notificationManager.getEnabledNotificationListeners();
+ });
+ if (DEBUG) {
+ Log.d(LOG_TAG, "Found " + sPreviouslyEnabledNotificationListeners.size()
+ + " previously allowed notification listeners. Disabling before test run.");
+ }
+ for (ComponentName listener : sPreviouslyEnabledNotificationListeners) {
+ setNotificationListenerServiceAllowed(listener, false);
+ }
+ }
+
+ private static void reallowPreexistingNotificationListeners() {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "Re-allowing " + sPreviouslyEnabledNotificationListeners.size()
+ + " previously allowed notification listeners found before test run.");
+ }
+ for (ComponentName listener : sPreviouslyEnabledNotificationListeners) {
+ setNotificationListenerServiceAllowed(listener, true);
+ }
+ }
+
+ protected void allowTestAppNotificationListenerService() {
+ setNotificationListenerServiceAllowed(
+ new ComponentName(TEST_APP_PKG, TEST_APP_NOTIFICATION_SERVICE), true);
+ }
+
+ protected void disallowTestAppNotificationListenerService() {
+ setNotificationListenerServiceAllowed(
+ new ComponentName(TEST_APP_PKG, TEST_APP_NOTIFICATION_SERVICE), false);
+ }
+
+ /**
+ * Get the state of the job scheduler
+ */
+ private static JobSchedulerServiceDumpProto getJobSchedulerDump() throws Exception {
+ return ProtoUtils.getProto(sUiAutomation, JobSchedulerServiceDumpProto.class,
+ ProtoUtils.DUMPSYS_JOB_SCHEDULER);
+ }
+
+ /**
+ * Get the last time the NOTIFICATION_LISTENER_CHECK_JOB_ID job was started/stopped for
+ * permission
+ * controller.
+ *
+ * @param event the job event (start/stop)
+ * @return the last time the event happened.
+ */
+ private static long getLastJobTime(int event) throws Exception {
+ int permControllerUid = sPackageManager.getPackageUid(PERMISSION_CONTROLLER_PKG, 0);
+
+ long lastTime = -1;
+
+ for (JobPackageHistoryProto.HistoryEvent historyEvent :
+ getJobSchedulerDump().history.historyEvent) {
+ if (historyEvent.uid == permControllerUid
+ && historyEvent.jobId == NOTIFICATION_LISTENER_CHECK_JOB_ID
+ && historyEvent.event == event) {
+ lastTime = max(lastTime,
+ System.currentTimeMillis() - historyEvent.timeSinceEventMs);
+ }
+ }
+
+ return lastTime;
+ }
+
+ /**
+ * Force a run of the notification listener check.
+ */
+ protected static void runNotificationListenerCheck() throws Throwable {
+ // Sleep a little to make sure we don't have overlap in timing
+ Thread.sleep(1000);
+
+ long beforeJob = System.currentTimeMillis();
+
+ // Sleep a little to avoid raciness in time keeping
+ Thread.sleep(1000);
+
+ runShellCommand("cmd jobscheduler run -u " + myUserHandle().getIdentifier() + " -f "
+ + PERMISSION_CONTROLLER_PKG + " " + NOTIFICATION_LISTENER_CHECK_JOB_ID);
+
+ eventually(() -> {
+ long startTime = getLastJobTime(START_PERIODIC_JOB);
+ assertTrue(startTime + " !> " + beforeJob, startTime > beforeJob);
+ }, UNEXPECTED_TIMEOUT_MILLIS);
+
+ // We can't simply require startTime <= endTime because the time being reported isn't
+ // accurate, and sometimes the end time may come before the start time by around 100 ms.
+ eventually(() -> {
+ long stopTime = getLastJobTime(STOP_PERIODIC_JOB);
+ assertTrue(stopTime + " !> " + beforeJob, stopTime > beforeJob);
+ }, UNEXPECTED_TIMEOUT_MILLIS);
+ }
+
+ /**
+ * Skip tests for if Safety Center not supported
+ */
+ protected void assumeDeviceSupportsSafetyCenter() {
+ assumeTrue(SafetyCenterUtils.deviceSupportsSafetyCenter(sContext));
+ }
+
+ /**
+ * Skip tests for if Safety Center IS supported
+ */
+ protected void assumeDeviceDoesNotSupportSafetyCenter() {
+ assumeFalse(SafetyCenterUtils.deviceSupportsSafetyCenter(sContext));
+ }
+
+ protected void wakeUpAndDismissKeyguard() {
+ runShellCommand("input keyevent KEYCODE_WAKEUP");
+ runShellCommand("wm dismiss-keyguard");
+ }
+
+ /**
+ * Reset the permission controllers state before each test
+ */
+ protected void resetPermissionControllerBeforeEachTest() throws Throwable {
+ resetPermissionController();
+
+ // ensure no posted notification listener notifications exits
+ eventually(() -> assertNull(getNotification(false)), UNEXPECTED_TIMEOUT_MILLIS);
+
+ // Reset job scheduler stats (to allow more jobs to be run)
+ runShellCommand(
+ "cmd jobscheduler reset-execution-quota -u " + myUserHandle().getIdentifier() + " "
+ + PERMISSION_CONTROLLER_PKG);
+ }
+
+ /**
+ * Reset the permission controllers state.
+ */
+ private static void resetPermissionController() throws Throwable {
+ clearAppState(PERMISSION_CONTROLLER_PKG);
+ int currentUserId = myUserHandle().getIdentifier();
+
+ // Wait until jobs are cleared
+ eventually(() -> {
+ JobSchedulerServiceDumpProto dump = getJobSchedulerDump();
+
+ for (RegisteredJob job : dump.registeredJobs) {
+ if (job.dump.sourceUserId == currentUserId) {
+ assertNotEquals(job.dump.sourcePackageName, PERMISSION_CONTROLLER_PKG);
+ }
+ }
+ }, UNEXPECTED_TIMEOUT_MILLIS);
+
+ // Setup up permission controller again (simulate a reboot)
+ Intent permissionControllerSetupIntent = new Intent(
+ ACTION_SET_UP_NOTIFICATION_LISTENER_CHECK).setPackage(
+ PERMISSION_CONTROLLER_PKG).setFlags(FLAG_RECEIVER_FOREGROUND);
+
+ // Query for the setup broadcast receiver
+ List<ResolveInfo> resolveInfos = sContext.getPackageManager().queryBroadcastReceivers(
+ permissionControllerSetupIntent, 0);
+
+ if (resolveInfos.size() > 0) {
+ sContext.sendBroadcast(permissionControllerSetupIntent);
+ } else {
+ sContext.sendBroadcast(new Intent()
+ .setClassName(PERMISSION_CONTROLLER_PKG, NotificationListenerOnBootReceiver)
+ .setFlags(FLAG_RECEIVER_FOREGROUND)
+ .setPackage(PERMISSION_CONTROLLER_PKG));
+ }
+
+ // Wait until jobs are set up
+ eventually(() -> {
+ JobSchedulerServiceDumpProto dump = getJobSchedulerDump();
+
+ for (RegisteredJob job : dump.registeredJobs) {
+ if (job.dump.sourceUserId == currentUserId
+ && job.dump.sourcePackageName.equals(PERMISSION_CONTROLLER_PKG)
+ && job.dump.jobInfo.service.className.contains(
+ "NotificationListenerCheck")) {
+ return;
+ }
+ }
+
+ fail("Permission controller jobs not found");
+ }, UNEXPECTED_TIMEOUT_MILLIS);
+ }
+
+ /**
+ * Preshow/dismiss cts NotificationListener notification as it negatively affects test results
+ * (can result in unexpected test pass/failures)
+ */
+ protected void triggerAndDismissCtsNotificationListenerNotification() throws Throwable {
+ // CtsNotificationListenerService isn't enabled at this point, but NotificationListener
+ // should be. Mark as notified by showing and dismissing
+ runNotificationListenerCheck();
+
+ // Ensure notification shows and dismiss
+ eventually(() -> assertNotNull(getNotification(true)),
+ UNEXPECTED_TIMEOUT_MILLIS);
+ }
+
+ /**
+ * Get a notification listener notification that is currently visible.
+ *
+ * @param cancelNotification if `true` the notification is canceled inside this method
+ * @return The notification or `null` if there is none
+ */
+ protected StatusBarNotification getNotification(boolean cancelNotification) throws Throwable {
+ return NotificationUtils.getNotificationForPackageAndId(
+ PERMISSION_CONTROLLER_PKG,
+ NOTIFICATION_LISTENER_CHECK_NOTIFICATION_ID,
+ cancelNotification);
+ }
+
+ /**
+ * Clear any notifications related to NotificationListenerCheck to ensure clean test setup
+ */
+ protected void clearNotifications() throws Throwable {
+ // Clear notification if present
+ NotificationUtils.clearNotificationsForPackage(PERMISSION_CONTROLLER_PKG);
+ }
+}
diff --git a/tests/tests/permission/src/android/permission/cts/CtsNotificationListenerHelperRule.kt b/tests/tests/permission/src/android/permission/cts/CtsNotificationListenerHelperRule.kt
new file mode 100644
index 0000000..b7fa960
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/CtsNotificationListenerHelperRule.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.permission.cts
+
+import android.content.ComponentName
+import android.content.Context
+import com.android.compatibility.common.util.SystemUtil
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Rule that enables and disables the CTS NotificationListenerService
+ */
+class CtsNotificationListenerHelperRule(context: Context) : TestRule {
+
+ private val notificationListenerComponentName = ComponentName(
+ context,
+ NotificationListener::class.java
+ )
+
+ override fun apply(base: Statement, description: Description): Statement {
+ return object : Statement() {
+ override fun evaluate() {
+ try {
+ // Allow NLS used to verify notifications sent
+ SystemUtil.runShellCommand(ALLOW_NLS_COMMAND +
+ notificationListenerComponentName.flattenToString())
+
+ base.evaluate()
+ } finally {
+ // Disallow NLS used to verify notifications sent
+ SystemUtil.runShellCommand(DISALLOW_NLS_COMMAND +
+ notificationListenerComponentName.flattenToString())
+ }
+ }
+ }
+ }
+
+ companion object {
+ private const val ALLOW_NLS_COMMAND = "cmd notification allow_listener "
+ private const val DISALLOW_NLS_COMMAND = "cmd notification disallow_listener "
+ }
+}
\ No newline at end of file
diff --git a/tests/tests/permission/src/android/permission/cts/LocationAccessCheckTest.java b/tests/tests/permission/src/android/permission/cts/LocationAccessCheckTest.java
index e13f58b..ceb4ede 100644
--- a/tests/tests/permission/src/android/permission/cts/LocationAccessCheckTest.java
+++ b/tests/tests/permission/src/android/permission/cts/LocationAccessCheckTest.java
@@ -18,6 +18,7 @@
import static android.Manifest.permission.ACCESS_BACKGROUND_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
import static android.app.AppOpsManager.OPSTR_FINE_LOCATION;
import static android.app.AppOpsManager.OP_FLAGS_ALL_TRUSTED;
import static android.content.Context.BIND_AUTO_CREATE;
@@ -47,6 +48,7 @@
import android.app.ActivityManager;
import android.app.AppOpsManager;
+import android.app.PendingIntent;
import android.app.UiAutomation;
import android.content.ComponentName;
import android.content.ContentResolver;
@@ -59,6 +61,7 @@
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
+import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Looper;
@@ -75,10 +78,10 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.DeviceConfigStateChangerRule;
-import com.android.compatibility.common.util.DeviceConfigStateHelper;
import com.android.compatibility.common.util.ProtoUtils;
import com.android.compatibility.common.util.mainline.MainlineModule;
import com.android.compatibility.common.util.mainline.ModuleDetector;
@@ -120,13 +123,20 @@
private static final int LOCATION_ACCESS_CHECK_JOB_ID = 0;
private static final int LOCATION_ACCESS_CHECK_NOTIFICATION_ID = 0;
- /** Whether to show location access check notifications. */
+ /**
+ * Whether to show location access check notifications.
+ */
private static final String PROPERTY_LOCATION_ACCESS_CHECK_ENABLED =
"location_access_check_enabled";
private static final String PROPERTY_LOCATION_ACCESS_CHECK_DELAY_MILLIS =
"location_access_check_delay_millis";
private static final String PROPERTY_LOCATION_ACCESS_PERIODIC_INTERVAL_MILLIS =
"location_access_check_periodic_interval_millis";
+ private static final String PROPERTY_BG_LOCATION_CHECK_ENABLED = "bg_location_check_is_enabled";
+ private static final String PROPERTY_JOB_SCHEDULER_MAX_JOB_PER_RATE_LIMIT_WINDOW =
+ "qc_max_job_count_per_rate_limiting_window";
+ private static final String PROPERTY_JOB_SCHEDULER_RATE_LIMIT_WINDOW_MILLIS =
+ "qc_rate_limiting_window_ms";
private static final long UNEXPECTED_TIMEOUT_MILLIS = 10000;
private static final long EXPECTED_TIMEOUT_MILLIS = 15000;
@@ -147,6 +157,45 @@
"com.android.permissioncontroller.permission.service"
+ ".LocationAccessCheck$SetupPeriodicBackgroundLocationAccessCheck";
+
+ /**
+ * The result of {@link #assumeCanGetFineLocation()}, so we don't have to run it over and over
+ * again.
+ */
+ private static Boolean sCanAccessFineLocation = null;
+
+ private static ServiceConnection sConnection;
+ private static IAccessLocationOnCommand sLocationAccessor;
+
+ private static void assumeNotPlayManaged() throws Exception {
+ assumeFalse(ModuleDetector.moduleIsPlayManaged(
+ sContext.getPackageManager(), MainlineModule.PERMISSION_CONTROLLER));
+ }
+
+ // Override location access check flag
+ @Rule
+ public DeviceConfigStateChangerRule mPrivacyDeviceConfig =
+ new DeviceConfigStateChangerRule(sContext,
+ DeviceConfig.NAMESPACE_PRIVACY,
+ PROPERTY_LOCATION_ACCESS_CHECK_ENABLED,
+ Boolean.toString(true));
+
+ // Override SafetyCenter enabled flag
+ @Rule
+ public DeviceConfigStateChangerRule sPrivacyDeviceConfigSafetyCenterEnabled =
+ new DeviceConfigStateChangerRule(sContext,
+ DeviceConfig.NAMESPACE_PRIVACY,
+ SafetyCenterUtils.PROPERTY_SAFETY_CENTER_ENABLED,
+ Boolean.toString(true));
+
+ // Override BG location enabled flag
+ @Rule
+ public DeviceConfigStateChangerRule sPrivacyDeviceConfigBgLocationCheckEnabled =
+ new DeviceConfigStateChangerRule(sContext,
+ DeviceConfig.NAMESPACE_PRIVACY,
+ PROPERTY_BG_LOCATION_CHECK_ENABLED,
+ Boolean.toString(true));
+
// Override general notification interval
@Rule
public DeviceConfigStateChangerRule sPrivacyDeviceConfigBgCheckIntervalMillis =
@@ -163,23 +212,46 @@
PROPERTY_LOCATION_ACCESS_CHECK_DELAY_MILLIS,
"50");
+ // Disable job scheduler throttling by allowing 300000 jobs per 30 sec
+ @Rule
+ public DeviceConfigStateChangerRule sJobSchedulerDeviceConfig1 =
+ new DeviceConfigStateChangerRule(sContext,
+ DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+ PROPERTY_JOB_SCHEDULER_MAX_JOB_PER_RATE_LIMIT_WINDOW,
+ Integer.toString(3000000));
+
+ // Disable job scheduler throttling by allowing 300000 jobs per 30 sec
+ @Rule
+ public DeviceConfigStateChangerRule sJobSchedulerDeviceConfig2 =
+ new DeviceConfigStateChangerRule(sContext,
+ DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+ PROPERTY_JOB_SCHEDULER_RATE_LIMIT_WINDOW_MILLIS,
+ Integer.toString(30000));
+
/**
- * The result of {@link #assumeCanGetFineLocation()}, so we don't have to run it over and over
- * again.
+ * Change settings so that permission controller can show location access notifications more
+ * often.
*/
- private static Boolean sCanAccessFineLocation = null;
+ @BeforeClass
+ public static void reduceDelays() {
+ runWithShellPermissionIdentity(() -> {
+ ContentResolver cr = sContext.getContentResolver();
+ // New settings will be applied in when permission controller is reset
+ Settings.Secure.putLong(cr, LOCATION_ACCESS_CHECK_INTERVAL_MILLIS, 100);
+ Settings.Secure.putLong(cr, LOCATION_ACCESS_CHECK_DELAY_MILLIS, 50);
+ });
+ }
- private static ServiceConnection sConnection;
- private static IAccessLocationOnCommand sLocationAccessor;
-
- private DeviceConfigStateHelper mPrivacyDeviceConfig =
- new DeviceConfigStateHelper(DeviceConfig.NAMESPACE_PRIVACY);
- private static DeviceConfigStateHelper sJobSchedulerDeviceConfig =
- new DeviceConfigStateHelper(DeviceConfig.NAMESPACE_JOB_SCHEDULER);
-
- private static void assumeNotPlayManaged() throws Exception {
- assumeFalse(ModuleDetector.moduleIsPlayManaged(
- sContext.getPackageManager(), MainlineModule.PERMISSION_CONTROLLER));
+ /**
+ * Reset settings so that permission controller runs normally.
+ */
+ @AfterClass
+ public static void resetDelays() throws Throwable {
+ runWithShellPermissionIdentity(() -> {
+ ContentResolver cr = sContext.getContentResolver();
+ Settings.Secure.resetToDefaults(cr, LOCATION_ACCESS_CHECK_INTERVAL_MILLIS);
+ Settings.Secure.resetToDefaults(cr, LOCATION_ACCESS_CHECK_DELAY_MILLIS);
+ });
}
/**
@@ -297,7 +369,6 @@
* controller.
*
* @param event the job event (start/stop)
- *
* @return the last time the event happened.
*/
private static long getLastJobTime(int event) throws Exception {
@@ -424,25 +495,6 @@
NotificationListener.class).flattenToString()));
}
- /**
- * Change settings so that permission controller can show location access notifications more
- * often.
- */
- @BeforeClass
- public static void reduceDelays() {
- runWithShellPermissionIdentity(() -> {
- ContentResolver cr = sContext.getContentResolver();
-
- // New settings will be applied in when permission controller is reset
- Settings.Secure.putLong(cr, LOCATION_ACCESS_CHECK_INTERVAL_MILLIS, 100);
- Settings.Secure.putLong(cr, LOCATION_ACCESS_CHECK_DELAY_MILLIS, 50);
-
- // Disable job scheduler throttling by allowing 300000 jobs per 30 sec
- sJobSchedulerDeviceConfig.set("qc_max_job_count_per_rate_limiting_window", "3000000");
- sJobSchedulerDeviceConfig.set("qc_rate_limiting_window_ms", "30000");
- });
- }
-
@BeforeClass
public static void installBackgroundAccessApp() throws Exception {
installBackgroundAccessApp(false);
@@ -473,6 +525,21 @@
sLocationAccessor = null;
}
+ private void setDeviceConfigProperty(
+ @NonNull String propertyName,
+ @NonNull String value) {
+ runWithShellPermissionIdentity(() -> {
+ boolean valueWasSet = DeviceConfig.setProperty(
+ DeviceConfig.NAMESPACE_PRIVACY,
+ propertyName,
+ value,
+ false);
+ if (!valueWasSet) {
+ throw new IllegalStateException("Could not set " + propertyName + " to " + value);
+ }
+ }, WRITE_DEVICE_CONFIG);
+ }
+
private static void installForegroundAccessApp() throws Exception {
unbindService();
@@ -543,8 +610,8 @@
* Enable location access check
*/
public void enableLocationAccessCheck() throws Throwable {
- mPrivacyDeviceConfig.set(PROPERTY_LOCATION_ACCESS_CHECK_ENABLED, "true");
-
+ setDeviceConfigProperty(PROPERTY_LOCATION_ACCESS_CHECK_ENABLED,
+ "true");
// Run a location access check to update enabled state inside permission controller
runLocationCheck();
}
@@ -553,8 +620,8 @@
* Disable location access check
*/
private void disableLocationAccessCheck() throws Throwable {
- mPrivacyDeviceConfig.set(PROPERTY_LOCATION_ACCESS_CHECK_ENABLED, "false");
-
+ setDeviceConfigProperty(PROPERTY_LOCATION_ACCESS_CHECK_ENABLED,
+ "false");
// Run a location access check to update enabled state inside permission controller
runLocationCheck();
}
@@ -669,27 +736,10 @@
}
/**
- * Reset settings so that permission controller runs normally.
- */
- @AfterClass
- public static void resetDelays() throws Throwable {
- runWithShellPermissionIdentity(() -> {
- ContentResolver cr = sContext.getContentResolver();
-
- Settings.Secure.resetToDefaults(cr, LOCATION_ACCESS_CHECK_INTERVAL_MILLIS);
- Settings.Secure.resetToDefaults(cr, LOCATION_ACCESS_CHECK_DELAY_MILLIS);
-
- sJobSchedulerDeviceConfig.restoreOriginalValues();
- });
- }
-
- /**
* Reset location access check
*/
@After
public void resetPrivacyConfig() throws Throwable {
- mPrivacyDeviceConfig.restoreOriginalValues();
-
// Run a location access check to update enabled state inside permission controller
runLocationCheck();
}
@@ -703,7 +753,6 @@
public void notificationIsShown() throws Throwable {
accessLocation();
runLocationCheck();
-
eventually(() -> assertNotNull(getNotification(true)), EXPECTED_TIMEOUT_MILLIS);
}
@@ -882,4 +931,23 @@
runLocationCheck();
assertNull(getNotification(false));
}
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
+ public void notificationOnClickOpensSafetyCenter() throws Throwable {
+ accessLocation();
+ runLocationCheck();
+
+ StatusBarNotification currentNotification = eventually(() -> {
+ StatusBarNotification notification = getNotification(false);
+ assertNotNull(notification);
+ return notification;
+ }, EXPECTED_TIMEOUT_MILLIS);
+
+ // Verify content intent
+ PendingIntent contentIntent = currentNotification.getNotification().contentIntent;
+ contentIntent.send();
+
+ SafetyCenterUtils.assertSafetyCenterStarted();
+ }
}
diff --git a/tests/tests/permission/src/android/permission/cts/NotificationListenerCheckTest.java b/tests/tests/permission/src/android/permission/cts/NotificationListenerCheckTest.java
new file mode 100644
index 0000000..88006b9
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/NotificationListenerCheckTest.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.permission.cts;
+
+import static android.permission.cts.PermissionUtils.clearAppState;
+import static android.permission.cts.PermissionUtils.install;
+import static android.permission.cts.PermissionUtils.uninstallApp;
+import static android.permission.cts.TestUtils.ensure;
+import static android.permission.cts.TestUtils.eventually;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import android.app.PendingIntent;
+import android.os.Build;
+import android.platform.test.annotations.AppModeFull;
+import android.service.notification.StatusBarNotification;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests the {@code NotificationListenerCheck} in permission controller.
+ */
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "Cannot set system settings as instant app. Also we never show a notification"
+ + " listener check notification for instant apps.")
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
+public class NotificationListenerCheckTest extends BaseNotificationListenerCheckTest {
+
+ @Before
+ public void setup() throws Throwable {
+ // Skip tests if safety center not allowed
+ assumeDeviceSupportsSafetyCenter();
+
+ wakeUpAndDismissKeyguard();
+ resetPermissionControllerBeforeEachTest();
+
+ // Cts NLS is required to verify sent Notifications, however, we don't want it to show up in
+ // testing
+ triggerAndDismissCtsNotificationListenerNotification();
+
+ clearNotifications();
+
+ // Install and allow the app with NLS for testing
+ install(TEST_APP_NOTIFICATION_LISTENER_APK);
+ allowTestAppNotificationListenerService();
+ }
+
+ @After
+ public void tearDown() throws Throwable {
+ // Disallow and uninstall the app with NLS for testing
+ disallowTestAppNotificationListenerService();
+ uninstallApp(TEST_APP_PKG);
+
+ clearNotifications();
+ }
+
+ @Test
+ public void noNotificationIfFeatureDisabled() throws Throwable {
+ setNotificationListenerCheckEnabled(false);
+
+ runNotificationListenerCheck();
+
+ ensure(() -> assertNull("Expected no notifications", getNotification(false)),
+ ENSURE_NOTIFICATION_NOT_SHOWN_EXPECTED_TIMEOUT_MILLIS);
+ }
+
+ @Test
+ public void noNotificationIfSafetyCenterDisabled() throws Throwable {
+ SafetyCenterUtils.setSafetyCenterEnabled(false);
+
+ runNotificationListenerCheck();
+
+ ensure(() -> assertNull("Expected no notifications", getNotification(false)),
+ ENSURE_NOTIFICATION_NOT_SHOWN_EXPECTED_TIMEOUT_MILLIS);
+ }
+
+ @Test
+ public void notificationIsShown() throws Throwable {
+ runNotificationListenerCheck();
+
+ eventually(() -> assertNotNull("Expected notification, none found", getNotification(false)),
+ UNEXPECTED_TIMEOUT_MILLIS);
+ }
+
+ @Test
+ public void notificationIsShownOnlyOnce() throws Throwable {
+ runNotificationListenerCheck();
+ eventually(() -> assertNotNull(getNotification(true)), UNEXPECTED_TIMEOUT_MILLIS);
+
+ runNotificationListenerCheck();
+
+ ensure(() -> assertNull("Expected no notifications", getNotification(false)),
+ ENSURE_NOTIFICATION_NOT_SHOWN_EXPECTED_TIMEOUT_MILLIS);
+ }
+
+ @Test
+ public void notificationIsShownAgainAfterClear() throws Throwable {
+ runNotificationListenerCheck();
+
+ eventually(() -> assertNotNull(getNotification(true)), UNEXPECTED_TIMEOUT_MILLIS);
+
+ clearAppState(TEST_APP_PKG);
+
+ // Wait until package is cleared and permission controller has cleared the state
+ Thread.sleep(2000);
+
+ allowTestAppNotificationListenerService();
+ runNotificationListenerCheck();
+
+ eventually(() -> assertNotNull(getNotification(true)), UNEXPECTED_TIMEOUT_MILLIS);
+ }
+
+ @Test
+ public void notificationIsShownAgainAfterUninstallAndReinstall() throws Throwable {
+ runNotificationListenerCheck();
+
+ eventually(() -> assertNotNull(getNotification(true)), UNEXPECTED_TIMEOUT_MILLIS);
+
+ uninstallApp(TEST_APP_PKG);
+
+ // Wait until package permission controller has cleared the state
+ Thread.sleep(2000);
+
+ install(TEST_APP_NOTIFICATION_LISTENER_APK);
+
+ allowTestAppNotificationListenerService();
+ runNotificationListenerCheck();
+
+ eventually(() -> assertNotNull(getNotification(true)), UNEXPECTED_TIMEOUT_MILLIS);
+ }
+
+ @Test
+ public void notificationIsShownAgainAfterDisableAndReenableAppNotificationListener()
+ throws Throwable {
+ runNotificationListenerCheck();
+
+ eventually(() -> assertNotNull(getNotification(true)), UNEXPECTED_TIMEOUT_MILLIS);
+
+ // Disallow NLS, and run NLS check job. This should clear NLS off notified list
+ disallowTestAppNotificationListenerService();
+ runNotificationListenerCheck();
+
+ // Re-allow NLS, and run NLS check job. This work now that it's cleared NLS off notified
+ // list
+ allowTestAppNotificationListenerService();
+ runNotificationListenerCheck();
+
+ eventually(() -> assertNotNull(getNotification(true)), UNEXPECTED_TIMEOUT_MILLIS);
+ }
+
+ @Test
+ public void removeNotificationOnUninstall() throws Throwable {
+ runNotificationListenerCheck();
+
+ eventually(() -> assertNotNull(getNotification(false)), UNEXPECTED_TIMEOUT_MILLIS);
+
+ uninstallApp(TEST_APP_PKG);
+
+ // Wait until package permission controller has cleared the state
+ Thread.sleep(2000);
+
+ eventually(() -> assertNull(getNotification(false)), UNEXPECTED_TIMEOUT_MILLIS);
+ }
+
+ @Test
+ public void notificationIsNotShownAfterDisableAppNotificationListener() throws Throwable {
+ disallowTestAppNotificationListenerService();
+
+ runNotificationListenerCheck();
+
+ // We don't expect a notification, but try to trigger one anyway
+ ensure(() -> assertNull("Expected no notifications", getNotification(false)),
+ ENSURE_NOTIFICATION_NOT_SHOWN_EXPECTED_TIMEOUT_MILLIS);
+ }
+
+ @Test
+ public void notificationOnClick_opensSafetyCenter() throws Throwable {
+ runNotificationListenerCheck();
+
+ StatusBarNotification currentNotification = eventually(
+ () -> {
+ StatusBarNotification notification = getNotification(false);
+ assertNotNull(notification);
+ return notification;
+ }, UNEXPECTED_TIMEOUT_MILLIS);
+
+ // Verify content intent
+ PendingIntent contentIntent = currentNotification.getNotification().contentIntent;
+ contentIntent.send();
+
+ SafetyCenterUtils.assertSafetyCenterStarted();
+ }
+}
diff --git a/tests/tests/permission/src/android/permission/cts/NotificationListenerCheckWithSafetyCenterUnsupportedTest.java b/tests/tests/permission/src/android/permission/cts/NotificationListenerCheckWithSafetyCenterUnsupportedTest.java
new file mode 100644
index 0000000..a346de6
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/NotificationListenerCheckWithSafetyCenterUnsupportedTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.permission.cts;
+
+import static android.permission.cts.PermissionUtils.install;
+import static android.permission.cts.PermissionUtils.uninstallApp;
+import static android.permission.cts.TestUtils.ensure;
+
+import static org.junit.Assert.assertNull;
+
+import android.os.Build;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests the {@code NotificationListenerCheck} in permission controller.
+ */
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "Cannot set system settings as instant app. Also we never show a notification"
+ + " listener check notification for instant apps.")
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
+public class NotificationListenerCheckWithSafetyCenterUnsupportedTest
+ extends BaseNotificationListenerCheckTest {
+
+ @Before
+ public void setup() throws Throwable {
+ // Skip tests if safety center is supported
+ assumeDeviceDoesNotSupportSafetyCenter();
+
+ wakeUpAndDismissKeyguard();
+ resetPermissionControllerBeforeEachTest();
+
+ clearNotifications();
+
+ // Install and allow the app with NLS for testing
+ install(TEST_APP_NOTIFICATION_LISTENER_APK);
+ allowTestAppNotificationListenerService();
+ }
+
+ @After
+ public void tearDown() throws Throwable {
+ // Disallow and uninstall the app with NLS for testing
+ disallowTestAppNotificationListenerService();
+ uninstallApp(TEST_APP_PKG);
+
+ clearNotifications();
+ }
+
+ @Test
+ public void noNotifications_featureEnabled_safetyCenterEnabled() throws Throwable {
+ runNotificationListenerCheck();
+
+ ensure(() -> assertNull("Expected no notifications", getNotification(false)),
+ ENSURE_NOTIFICATION_NOT_SHOWN_EXPECTED_TIMEOUT_MILLIS);
+ }
+
+ @Test
+ public void noNotifications_featureDisabled_safetyCenterEnabled() throws Throwable {
+ setNotificationListenerCheckEnabled(false);
+
+ runNotificationListenerCheck();
+
+ ensure(() -> assertNull("Expected no notifications", getNotification(false)),
+ ENSURE_NOTIFICATION_NOT_SHOWN_EXPECTED_TIMEOUT_MILLIS);
+ }
+
+ @Test
+ public void noNotifications_featureEnabled_safetyCenterDisabled() throws Throwable {
+ SafetyCenterUtils.setSafetyCenterEnabled(false);
+
+ runNotificationListenerCheck();
+
+ ensure(() -> assertNull("Expected no notifications", getNotification(false)),
+ ENSURE_NOTIFICATION_NOT_SHOWN_EXPECTED_TIMEOUT_MILLIS);
+ }
+
+ @Test
+ public void noNotifications_featureDisabled_safetyCenterDisabled() throws Throwable {
+ setNotificationListenerCheckEnabled(false);
+ SafetyCenterUtils.setSafetyCenterEnabled(false);
+
+ runNotificationListenerCheck();
+
+ ensure(() -> assertNull("Expected no notifications", getNotification(false)),
+ ENSURE_NOTIFICATION_NOT_SHOWN_EXPECTED_TIMEOUT_MILLIS);
+ }
+}
diff --git a/tests/tests/permission/src/android/permission/cts/NotificationListenerUtils.kt b/tests/tests/permission/src/android/permission/cts/NotificationListenerUtils.kt
new file mode 100644
index 0000000..0ee8a3d
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/NotificationListenerUtils.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.permission.cts
+
+import android.service.notification.StatusBarNotification
+import org.junit.Assert
+import android.permission.cts.TestUtils.ensure
+import android.permission.cts.TestUtils.eventually
+
+object NotificationListenerUtils {
+
+ private const val NOTIFICATION_CANCELLATION_TIMEOUT_MILLIS = 5000L
+ private const val NOTIFICATION_WAIT_MILLIS = 2000L
+ private val notificationService = NotificationListener.getInstance()
+
+ @JvmStatic
+ fun assertEmptyNotification(packageName: String, notificationId: Int) {
+ ensure({
+ Assert.assertNull(
+ "Expected no notification",
+ getNotification(packageName, notificationId))
+ }, NOTIFICATION_WAIT_MILLIS)
+ }
+
+ @JvmStatic
+ fun assertNotificationExist(packageName: String, notificationId: Int) {
+ eventually({
+ Assert.assertNotNull(
+ "Expected notification, none found",
+ getNotification(packageName, notificationId))
+ }, NOTIFICATION_WAIT_MILLIS)
+ }
+
+ @JvmStatic
+ fun cancelNotification(packageName: String, notificationId: Int) {
+ val notification = getNotification(packageName, notificationId)
+ if (notification != null) {
+ notificationService.cancelNotification(notification.key)
+ eventually({
+ Assert.assertTrue(getNotification(packageName, notificationId) == null)
+ }, NOTIFICATION_CANCELLATION_TIMEOUT_MILLIS)
+ }
+ }
+
+ @JvmStatic
+ fun cancelNotifications(packageName: String) {
+ val notifications = getNotifications(packageName)
+ if (notifications.isNotEmpty()) {
+ notifications.forEach { notification ->
+ notificationService.cancelNotification(notification.key)
+ }
+ eventually({
+ Assert.assertTrue(getNotifications(packageName).isEmpty())
+ }, NOTIFICATION_CANCELLATION_TIMEOUT_MILLIS)
+ }
+ }
+
+ @JvmStatic
+ fun getNotification(packageName: String, notificationId: Int): StatusBarNotification? {
+ return getNotifications(packageName).firstOrNull {
+ it.id == notificationId
+ }
+ }
+
+ @JvmStatic
+ fun getNotifications(packageName: String): List<StatusBarNotification> {
+ val notifications: MutableList<StatusBarNotification> = ArrayList()
+ for (notification in notificationService.activeNotifications) {
+ if (notification.packageName == packageName) {
+ notifications.add(notification)
+ }
+ }
+ return notifications
+ }
+}
\ No newline at end of file
diff --git a/tests/tests/permission/src/android/permission/cts/NotificationUtils.kt b/tests/tests/permission/src/android/permission/cts/NotificationUtils.kt
new file mode 100644
index 0000000..7c50837
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/NotificationUtils.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.permission.cts
+
+import android.service.notification.StatusBarNotification
+import org.junit.Assert
+import java.util.concurrent.TimeUnit
+
+/**
+ * Utility methods to interact with NotificationManager through the CTS NotificationListenerService
+ * to get or clear notifications.
+ */
+object NotificationUtils {
+
+ private val NOTIFICATION_CANCELLATION_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(5)
+
+ /**
+ * Get a notification listener notification that is currently visible.
+ *
+ * @param cancelNotification if `true` the notification is canceled inside this method
+ * @return The notification or `null` if there is none
+ */
+ @JvmStatic
+ @Throws(Throwable::class)
+ fun getNotificationForPackageAndId(
+ pkg: String,
+ id: Int,
+ cancelNotification: Boolean
+ ): StatusBarNotification? {
+ val notificationService = NotificationListener.getInstance()
+ val notifications: List<StatusBarNotification> = getNotificationsForPackage(pkg)
+ if (notifications.isEmpty()) {
+ return null
+ }
+ for (notification in notifications) {
+ if (notification.id == id) {
+ if (cancelNotification) {
+ clearNotification(notification)
+ }
+ return notification
+ }
+ }
+ return null
+ }
+
+ /**
+ * Clears all currently visible notifications for a specified package.
+ */
+ @JvmStatic
+ @Throws(Throwable::class)
+ fun clearNotificationsForPackage(pkg: String) {
+ val notifications: List<StatusBarNotification> = getNotificationsForPackage(pkg)
+ if (notifications.isEmpty()) {
+ return
+ }
+
+ clearNotifications(notifications)
+ }
+
+ /** Clears the specified notification and ensures (asserts) it was removed */
+ @JvmStatic
+ @Throws(Throwable::class)
+ fun clearNotification(notification: StatusBarNotification) {
+ val notificationService = NotificationListener.getInstance()
+ notificationService.cancelNotification(notification.key)
+
+ // Wait for notification to get canceled
+ TestUtils.eventually({
+ Assert.assertFalse(
+ listOf(*notificationService.activeNotifications)
+ .contains(notification)
+ )
+ }, NOTIFICATION_CANCELLATION_TIMEOUT_MILLIS)
+ }
+
+ private fun clearNotifications(notifications: List<StatusBarNotification>) {
+ val notificationService = NotificationListener.getInstance()
+ notifications.forEach { notificationService.cancelNotification(it.key) }
+
+ // Wait for notification to get canceled
+ TestUtils.eventually({
+ val activeNotifications: List<StatusBarNotification> =
+ listOf(*notificationService.activeNotifications)
+ Assert.assertFalse(
+ activeNotifications.any { notifications.contains(it) }
+ )
+ }, NOTIFICATION_CANCELLATION_TIMEOUT_MILLIS)
+ }
+
+ /**
+ * Get all notifications associated with a given package that are currently visible.
+ * @param pkg Package for which to filter the notifications by
+ * @return [List] of [StatusBarNotification]
+ */
+ @Throws(Exception::class)
+ private fun getNotificationsForPackage(pkg: String): List<StatusBarNotification> {
+ val notificationService = NotificationListener.getInstance()
+ val notifications: MutableList<StatusBarNotification> = ArrayList()
+ for (notification in notificationService.activeNotifications) {
+ if (notification.packageName == pkg) {
+ notifications.add(notification)
+ }
+ }
+ return notifications
+ }
+}
\ No newline at end of file
diff --git a/tests/tests/permission/src/android/permission/cts/SafetyCenterUtils.kt b/tests/tests/permission/src/android/permission/cts/SafetyCenterUtils.kt
new file mode 100644
index 0000000..7514079
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/SafetyCenterUtils.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.permission.cts
+
+import android.app.Instrumentation
+import android.app.UiAutomation
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import android.os.Build
+import android.os.UserHandle
+import android.provider.DeviceConfig
+import android.safetycenter.SafetyCenterIssue
+import android.safetycenter.SafetyCenterManager
+import android.support.test.uiautomator.By
+import androidx.annotation.RequiresApi
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
+import com.android.compatibility.common.util.UiAutomatorUtils.waitFindObject
+import com.android.safetycenter.internaldata.SafetyCenterIds
+import com.android.safetycenter.internaldata.SafetyCenterIssueId
+import com.android.safetycenter.internaldata.SafetyCenterIssueKey
+import org.junit.Assert
+
+object SafetyCenterUtils {
+ /** Name of the flag that determines whether SafetyCenter is enabled. */
+ const val PROPERTY_SAFETY_CENTER_ENABLED = "safety_center_is_enabled"
+
+ private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+
+ /** Returns whether the device supports Safety Center. */
+ @JvmStatic
+ fun deviceSupportsSafetyCenter(context: Context): Boolean {
+ return context.resources.getBoolean(
+ Resources.getSystem().getIdentifier("config_enableSafetyCenter", "bool", "android"))
+ }
+
+ /** Enabled or disable Safety Center */
+ @JvmStatic
+ fun setSafetyCenterEnabled(enabled: Boolean) {
+ setDeviceConfigPrivacyProperty(PROPERTY_SAFETY_CENTER_ENABLED, enabled.toString())
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ @JvmStatic
+ fun startSafetyCenterActivity(context: Context) {
+ context.startActivity(
+ Intent(Intent.ACTION_SAFETY_CENTER)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK))
+ }
+
+ @JvmStatic
+ fun assertSafetyCenterStarted() {
+ // CollapsingToolbar title can't be found by text, so using description instead.
+ waitFindObject(By.desc("Security & privacy"))
+ }
+
+ @JvmStatic
+ fun setDeviceConfigPrivacyProperty(
+ propertyName: String,
+ value: String,
+ uiAutomation: UiAutomation = instrumentation.uiAutomation
+ ) {
+ runWithShellPermissionIdentity(uiAutomation) {
+ val valueWasSet =
+ DeviceConfig.setProperty(
+ DeviceConfig.NAMESPACE_PRIVACY,
+ /* name = */ propertyName,
+ /* value = */ value,
+ /* makeDefault = */ false)
+ check(valueWasSet) { "Could not set $propertyName to $value" }
+ }
+ }
+
+ @JvmStatic
+ fun deleteDeviceConfigPrivacyProperty(
+ propertyName: String,
+ uiAutomation: UiAutomation = instrumentation.uiAutomation
+ ) {
+ runWithShellPermissionIdentity(uiAutomation) {
+ DeviceConfig.deleteProperty(DeviceConfig.NAMESPACE_PRIVACY, propertyName)
+ }
+ }
+
+ @JvmStatic
+ private fun getSafetyCenterIssues(
+ automation: UiAutomation = instrumentation.uiAutomation
+ ): List<SafetyCenterIssue> {
+ val safetyCenterManager =
+ instrumentation.targetContext.getSystemService(SafetyCenterManager::class.java)
+ val issues = ArrayList<SafetyCenterIssue>()
+ runWithShellPermissionIdentity(automation) {
+ val safetyCenterData = safetyCenterManager!!.safetyCenterData
+ issues.addAll(safetyCenterData.issues)
+ }
+ return issues
+ }
+
+ @JvmStatic
+ fun assertSafetyCenterIssueExist(
+ sourceId: String,
+ issueId: String,
+ issueTypeId: String,
+ automation: UiAutomation = instrumentation.uiAutomation
+ ) {
+ val safetyCenterIssueId = safetyCenterIssueId(sourceId, issueId, issueTypeId)
+ Assert.assertTrue(
+ "Expect issues in safety center",
+ getSafetyCenterIssues(automation).any { safetyCenterIssueId == it.id })
+ }
+
+ @JvmStatic
+ fun assertSafetyCenterIssueDoesNotExist(
+ sourceId: String,
+ issueId: String,
+ issueTypeId: String,
+ automation: UiAutomation = instrumentation.uiAutomation
+ ) {
+ val safetyCenterIssueId = safetyCenterIssueId(sourceId, issueId, issueTypeId)
+ Assert.assertTrue(
+ "Expect no issue in safety center",
+ getSafetyCenterIssues(automation).none { safetyCenterIssueId == it.id })
+ }
+
+ private fun safetyCenterIssueId(sourceId: String, sourceIssueId: String, issueTypeId: String) =
+ SafetyCenterIds.encodeToString(
+ SafetyCenterIssueId.newBuilder()
+ .setSafetyCenterIssueKey(
+ SafetyCenterIssueKey.newBuilder()
+ .setSafetySourceId(sourceId)
+ .setSafetySourceIssueId(sourceIssueId)
+ .setUserId(UserHandle.myUserId())
+ .build())
+ .setIssueTypeId(issueTypeId)
+ .build())
+}
diff --git a/tests/tests/permission3/src/android/permission3/cts/PermissionAttributionTest.kt b/tests/tests/permission3/src/android/permission3/cts/PermissionAttributionTest.kt
index 31e5449..367ae31 100644
--- a/tests/tests/permission3/src/android/permission3/cts/PermissionAttributionTest.kt
+++ b/tests/tests/permission3/src/android/permission3/cts/PermissionAttributionTest.kt
@@ -22,13 +22,11 @@
import android.content.Intent
import android.location.LocationManager
import android.os.Build
-import android.provider.DeviceConfig
import android.support.test.uiautomator.By
import androidx.test.filters.SdkSuppress
import com.android.compatibility.common.util.AppOpsUtils.setOpMode
import com.android.compatibility.common.util.CtsDownstreamingTest
import com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity
-import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
@@ -47,7 +45,6 @@
private val micLabel = packageManager.getPermissionGroupInfo(
android.Manifest.permission_group.MICROPHONE, 0).loadLabel(packageManager).toString()
val locationManager = context.getSystemService(LocationManager::class.java)!!
- private var wasEnabled = false
@Before
fun installAppLocationProviderAndAllowMockLocation() {
@@ -63,15 +60,11 @@
setOpMode(
context.packageName, AppOpsManager.OPSTR_MOCK_LOCATION, AppOpsManager.MODE_ALLOWED
)
- wasEnabled = setSubattributionEnabledStateIfNeeded(true)
}
@After
fun teardown() {
locationManager.removeTestProvider(APP_PACKAGE_NAME)
- if (!wasEnabled) {
- setSubattributionEnabledStateIfNeeded(false)
- }
}
@Test
@@ -115,24 +108,10 @@
assertEquals(Activity.RESULT_OK, result.resultCode)
}
- private fun setSubattributionEnabledStateIfNeeded(shouldBeEnabled: Boolean): Boolean {
- var currentlyEnabled = false
- runWithShellPermissionIdentity {
- currentlyEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
- FLAG_SUBATTRIBUTION, false)
- if (currentlyEnabled != shouldBeEnabled) {
- DeviceConfig.setProperty(DeviceConfig.NAMESPACE_PRIVACY, FLAG_SUBATTRIBUTION,
- shouldBeEnabled.toString(), false)
- }
- }
- return currentlyEnabled
- }
-
companion object {
const val APP_APK_PATH = "$APK_DIRECTORY/CtsAccessMicrophoneAppLocationProvider.apk"
const val APP_PACKAGE_NAME = "android.permission3.cts.accessmicrophoneapplocationprovider"
const val APP_LABEL = "LocationProviderWithMicApp"
const val ATTRIBUTION_LABEL = "Attribution Label"
- const val FLAG_SUBATTRIBUTION = "permissions_hub_subattribution_enabled"
}
}
diff --git a/tests/tests/permission3/src/android/permission3/cts/PermissionHistoryTest.kt b/tests/tests/permission3/src/android/permission3/cts/PermissionHistoryTest.kt
index 5355fdc..9e8b3a0 100644
--- a/tests/tests/permission3/src/android/permission3/cts/PermissionHistoryTest.kt
+++ b/tests/tests/permission3/src/android/permission3/cts/PermissionHistoryTest.kt
@@ -28,8 +28,8 @@
import org.junit.After
import org.junit.Assume.assumeFalse
import org.junit.Before
-import org.junit.Ignore
import org.junit.Test
+import java.util.regex.Pattern
private const val APP_LABEL_1 = "CtsMicAccess"
private const val APP_LABEL_2 = "CtsMicAccess2"
@@ -42,14 +42,14 @@
private const val SHOW_7_DAYS = "Show 7 days"
private const val SHOW_24_HOURS = "Show 24 hours"
private const val MORE_OPTIONS = "More options"
-private const val TIMELINE_7_DAYS_DESCRIPTION = "in the past 7 days"
-private const val DASHBOARD_7_DAYS_DESCRIPTION = "7 days"
+private const val DASHBOARD_7_DAYS_DESCRIPTION_REGEX = "^.*7.*days$"
private const val PRIV_DASH_7_DAY_ENABLED = "privacy_dashboard_7_day_toggle"
+private const val REFRESH = "Refresh"
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
class PermissionHistoryTest : BasePermissionHubTest() {
private val micLabel = packageManager.getPermissionGroupInfo(
- Manifest.permission_group.MICROPHONE, 0).loadLabel(packageManager).toString()
+ Manifest.permission_group.MICROPHONE, 0).loadLabel(packageManager).toString()
private var was7DayToggleEnabled = false
// Permission history is not available on TV devices.
@@ -100,15 +100,27 @@
waitFindObject(By.textContains(APP_LABEL_1))
openPermissionDashboard()
- waitFindObject(By.res("android:id/title").textContains("Microphone")).click()
- waitFindObject(By.textContains(micLabel))
- waitFindObject(By.textContains(APP_LABEL_1))
+
+ SystemUtil.eventually {
+ try {
+ waitFindObject(By.hasChild(By.textContains("Microphone"))
+ .hasChild(By.textStartsWith("Used by")))
+ .click()
+ waitFindObject(By.textContains(micLabel))
+ waitFindObject(By.textContains(APP_LABEL_1))
+ } catch (e: Exception) {
+ // Sometimes the dashboard was in the state from previous failed tests.
+ // Clicking the refresh button to get the most recent access.
+ waitFindObject(By.descContains(REFRESH)).click()
+ throw e
+ }
+ }
+
pressBack()
pressBack()
}
@Test
- @Ignore
fun testToggleSystemApps() {
// I had some hard time mocking a system app.
// Hence here I am only testing if the toggle is there.
@@ -121,10 +133,18 @@
// Auto doesn't show the "Show system" action when it is disabled. If a system app ends up
// being installed for this test, then the Auto logic should be tested too.
if (!isAutomotive) {
- val menuView = waitFindObject(By.descContains(MORE_OPTIONS))
- menuView.click()
-
- waitFindObject(By.text(SHOW_SYSTEM))
+ SystemUtil.eventually {
+ try {
+ val menuView = waitFindObject(By.descContains(MORE_OPTIONS))
+ menuView.click()
+ waitFindObject(By.text(SHOW_SYSTEM))
+ } catch (e: Exception) {
+ // Sometimes the dashboard was in the state from previous failed tests.
+ // Clicking the refresh button to get the most recent access.
+ waitFindObject(By.descContains(REFRESH)).click()
+ throw e
+ }
+ }
}
pressBack()
@@ -152,14 +172,24 @@
waitFindObject(By.text(SHOW_7_DAYS)).click()
}
- waitFindObject(By.res("android:id/title").textContains("Microphone"))
- waitFindObject(By.textContains(DASHBOARD_7_DAYS_DESCRIPTION))
+ SystemUtil.eventually {
+ try {
+ waitFindObject(By.hasChild(By.textContains("Microphone"))
+ .hasChild(By.textStartsWith("Used by")))
+ } catch (e: Exception) {
+ // Sometimes the dashboard was in the state from previous failed tests.
+ // Clicking the refresh button to get the most recent access.
+ waitFindObject(By.descContains(REFRESH)).click()
+ throw e
+ }
+ }
+
+ waitFindObject(By.text(Pattern.compile(DASHBOARD_7_DAYS_DESCRIPTION_REGEX, Pattern.DOTALL)))
pressBack()
}
@Test
- @Ignore
fun testToggleFrom24HoursTo7DaysInTimeline() {
// Auto doesn't support the 7 day view
assumeFalse(isAutomotive)
@@ -182,13 +212,12 @@
waitFindObject(By.descContains(micLabel))
waitFindObject(By.textContains(APP_LABEL_1))
- waitFindObject(By.textContains(TIMELINE_7_DAYS_DESCRIPTION))
+ waitFindObject(By.text(Pattern.compile(DASHBOARD_7_DAYS_DESCRIPTION_REGEX, Pattern.DOTALL)))
pressBack()
}
@Test
- @Ignore
fun testMicrophoneTimelineWithOneApp() {
openMicrophoneApp(INTENT_ACTION_1)
waitFindObject(By.textContains(APP_LABEL_1))
@@ -204,7 +233,6 @@
}
@Test
- @Ignore
fun testCameraTimelineWithMultipleApps() {
openMicrophoneApp(INTENT_ACTION_1)
waitFindObject(By.textContains(APP_LABEL_1))
diff --git a/tests/tests/permission4/src/android/permission4/cts/CameraMicIndicatorsPermissionTest.kt b/tests/tests/permission4/src/android/permission4/cts/CameraMicIndicatorsPermissionTest.kt
index fe9037a..d5bd49b 100644
--- a/tests/tests/permission4/src/android/permission4/cts/CameraMicIndicatorsPermissionTest.kt
+++ b/tests/tests/permission4/src/android/permission4/cts/CameraMicIndicatorsPermissionTest.kt
@@ -26,13 +26,19 @@
import android.hardware.camera2.CameraManager
import android.os.Build
import android.os.Process
+import android.os.SystemClock
import android.permission.PermissionManager
import android.provider.DeviceConfig
import android.provider.Settings
+import android.safetycenter.SafetyCenterManager
import android.server.wm.WindowManagerStateHelper
import android.support.test.uiautomator.By
+import android.support.test.uiautomator.BySelector
+import android.support.test.uiautomator.StaleObjectException
import android.support.test.uiautomator.UiDevice
+import android.support.test.uiautomator.UiObject2
import android.support.test.uiautomator.UiSelector
+import androidx.annotation.RequiresApi
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import com.android.compatibility.common.util.DisableAnimationRule
@@ -40,6 +46,7 @@
import com.android.compatibility.common.util.SystemUtil.eventually
import com.android.compatibility.common.util.SystemUtil.runShellCommand
import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
+import com.android.compatibility.common.util.UiAutomatorUtils
import org.junit.After
import org.junit.Assert
import org.junit.Assert.assertEquals
@@ -50,7 +57,6 @@
import org.junit.Assume.assumeFalse
import org.junit.Assume.assumeTrue
import org.junit.Before
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -72,6 +78,8 @@
private const val UNEXPECTED_TIMEOUT_MILLIS = 1000
private const val TIMEOUT_MILLIS: Long = 20000
private const val TV_MIC_INDICATOR_WINDOW_TITLE = "MicrophoneCaptureIndicator"
+private const val MIC_LABEL_NAME = "microphone_toggle_label_qs"
+private const val CAMERA_LABEL_NAME = "camera_toggle_label_qs"
class CameraMicIndicatorsPermissionTest {
private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
@@ -84,11 +92,9 @@
private val isTv = packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
private val isCar = packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
+ private val micLabel = getPermissionControllerString(MIC_LABEL_NAME)
+ private val cameraLabel = getPermissionControllerString(CAMERA_LABEL_NAME)
private var wasEnabled = false
- private val micLabel = packageManager.getPermissionGroupInfo(
- Manifest.permission_group.MICROPHONE, 0).loadLabel(packageManager).toString()
- private val cameraLabel = packageManager.getPermissionGroupInfo(
- Manifest.permission_group.CAMERA, 0).loadLabel(packageManager).toString()
private var isScreenOn = false
private var screenTimeoutBeforeTest: Long = 0L
@@ -102,7 +108,7 @@
private val safetyCenterEnabled = callWithShellPermissionIdentity {
DeviceConfig.getString(DeviceConfig.NAMESPACE_PRIVACY,
- SAFETY_CENTER_ENABLED, false.toString())
+ SAFETY_CENTER_ENABLED, false.toString())
}
@Before
@@ -115,7 +121,7 @@
context.contentResolver, Settings.System.SCREEN_OFF_TIMEOUT, 1800000L
)
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_PRIVACY,
- SAFETY_CENTER_ENABLED, false.toString(), false)
+ SAFETY_CENTER_ENABLED, false.toString(), false)
}
if (!isScreenOn) {
@@ -207,9 +213,7 @@
testCameraAndMicIndicator(useMic = false, useCamera = true, chainUsage = true)
}
- // Enable when safety center sends a broadcast on safety center flag value change
@Test
- @Ignore
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
fun testSafetyCenterCameraIndicator() {
assumeFalse(isTv)
@@ -217,28 +221,27 @@
val manager = context.getSystemService(CameraManager::class.java)!!
assumeTrue(manager.cameraIdList.isNotEmpty())
changeSafetyCenterFlag(true.toString())
+ assumeSafetyCenterEnabled()
testCameraAndMicIndicator(useMic = false, useCamera = true, safetyCenterEnabled = true)
}
- // Enable when safety center sends a broadcast on safety center flag value change
@Test
- @Ignore
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
fun testSafetyCenterMicIndicator() {
assumeFalse(isTv)
assumeFalse(isCar)
changeSafetyCenterFlag(true.toString())
+ assumeSafetyCenterEnabled()
testCameraAndMicIndicator(useMic = true, useCamera = false, safetyCenterEnabled = true)
}
- // Enable when safety center sends a broadcast on safety center flag value change
@Test
- @Ignore
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
fun testSafetyCenterHotwordIndicatorBehavior() {
assumeFalse(isTv)
assumeFalse(isCar)
changeSafetyCenterFlag(true.toString())
+ assumeSafetyCenterEnabled()
testCameraAndMicIndicator(
useMic = false,
useCamera = false,
@@ -247,14 +250,13 @@
)
}
- // Enable when safety center sends a broadcast on safety center flag value change
@Test
- @Ignore
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
fun testSafetyCenterChainUsageWithOtherUsage() {
assumeFalse(isTv)
assumeFalse(isCar)
changeSafetyCenterFlag(true.toString())
+ assumeSafetyCenterEnabled()
testCameraAndMicIndicator(
useMic = false,
useCamera = true,
@@ -405,15 +407,23 @@
return@eventually
}
if (useMic) {
- val iconView = uiDevice.findObject(UiSelector().descriptionContains(micLabel))
- assertTrue("View with description $micLabel not found", iconView.exists())
+ var iconView = if (safetyCenterEnabled) {
+ waitFindObjectOrNull(By.text(micLabel))
+ } else {
+ uiDevice.findObject(UiSelector().descriptionContains(micLabel))
+ }
+ assertNotNull("View with description $micLabel not found", iconView)
}
if (useCamera) {
- val iconView = uiDevice.findObject(UiSelector().descriptionContains(cameraLabel))
- assertTrue("View with text $APP_LABEL not found", iconView.exists())
+ var iconView = if (safetyCenterEnabled) {
+ waitFindObjectOrNull(By.text(cameraLabel))
+ } else {
+ uiDevice.findObject(UiSelector().descriptionContains(cameraLabel))
+ }
+ assertNotNull("View with text $APP_LABEL not found", iconView)
}
- val appView = uiDevice.findObject(UiSelector().textContains(APP_LABEL))
- assertTrue("View with text $APP_LABEL not found", appView.exists())
+ var appView = waitFindObjectOrNull(By.textContains(APP_LABEL))
+ assertNotNull("View with text $APP_LABEL not found", appView)
if (safetyCenterEnabled) {
assertTrue("Did not find safety center views",
uiDevice.findObjects(By.res(SAFETY_CENTER_ITEM_ID)).size > 0)
@@ -445,16 +455,21 @@
"Did not find shell package"
}
- val usageViews = if (safetyCenterEnabled) {
- uiDevice.findObjects(By.res(SAFETY_CENTER_ITEM_ID))
+ if (safetyCenterEnabled) {
+ var micView = waitFindObjectOrNull(By.text(micLabel))
+ assertNotNull("View with text $micLabel not found", micView)
+ var camView = waitFindObjectOrNull(By.text(cameraLabel))
+ assertNotNull("View with text $cameraLabel not found", camView)
+ var shellView = waitFindObjectOrNull(By.textContains(shellLabel))
+ assertNotNull("View with text $shellLabel not found", shellView)
} else {
- uiDevice.findObjects(By.res(PRIVACY_ITEM_ID))
+ val usageViews = uiDevice.findObjects(By.res(PRIVACY_ITEM_ID))
+ assertEquals("Expected two usage views", 2, usageViews.size)
+ val appViews = uiDevice.findObjects(By.textContains(APP_LABEL))
+ assertEquals("Expected two $APP_LABEL view", 2, appViews.size)
+ val shellView = uiDevice.findObjects(By.textContains(shellLabel))
+ assertEquals("Expected only one shell view", 1, shellView.size)
}
- assertEquals("Expected two usage views", 2, usageViews.size)
- val appViews = uiDevice.findObjects(By.textContains(APP_LABEL))
- assertEquals("Expected two $APP_LABEL view", 2, appViews.size)
- val shellView = uiDevice.findObjects(By.textContains(shellLabel))
- assertEquals("Expected only one shell view", 1, shellView.size)
}
private fun isChipPresent(): Boolean {
@@ -488,7 +503,52 @@
private fun changeSafetyCenterFlag(safetyCenterEnabled: String) {
runWithShellPermissionIdentity {
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_PRIVACY,
- SAFETY_CENTER_ENABLED, safetyCenterEnabled, false)
+ SAFETY_CENTER_ENABLED, safetyCenterEnabled, false)
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ private fun assumeSafetyCenterEnabled() {
+ val safetyCenterManager = context.getSystemService(SafetyCenterManager::class.java)!!
+ val isSafetyCenterEnabled: Boolean = runWithShellPermissionIdentity<Boolean> {
+ safetyCenterManager.isSafetyCenterEnabled
+ }
+ assumeTrue(isSafetyCenterEnabled)
+ }
+
+ protected fun waitFindObjectOrNull(selector: BySelector): UiObject2? {
+ waitForIdle()
+ return findObjectWithRetry({ t -> UiAutomatorUtils.waitFindObjectOrNull(selector, t) })
+ }
+
+ private fun findObjectWithRetry(
+ automatorMethod: (timeoutMillis: Long) -> UiObject2?,
+ timeoutMillis: Long = TIMEOUT_MILLIS
+ ): UiObject2? {
+ waitForIdle()
+ val startTime = SystemClock.elapsedRealtime()
+ return try {
+ automatorMethod(timeoutMillis)
+ } catch (e: StaleObjectException) {
+ val remainingTime = timeoutMillis - (SystemClock.elapsedRealtime() - startTime)
+ if (remainingTime <= 0) {
+ throw e
+ }
+ automatorMethod(remainingTime)
+ }
+ }
+
+ private fun getPermissionControllerString(resourceName: String): String {
+ val permissionControllerPkg = context.packageManager.permissionControllerPackageName
+ try {
+ val permissionControllerContext =
+ context.createPackageContext(permissionControllerPkg, 0)
+ val resourceId =
+ permissionControllerContext.resources.getIdentifier(
+ resourceName, "string", "com.android.permissioncontroller")
+ return permissionControllerContext.getString(resourceId)
+ } catch (e: PackageManager.NameNotFoundException) {
+ throw RuntimeException(e)
}
}
}
\ No newline at end of file
diff --git a/tests/tests/provider/OWNERS b/tests/tests/provider/OWNERS
index 1e318ea..7c9a6b2 100644
--- a/tests/tests/provider/OWNERS
+++ b/tests/tests/provider/OWNERS
@@ -1,7 +1,13 @@
-# Bug component: 655625
-
-include platform/frameworks/base:/core/java/android/os/storage/OWNERS
-
tgunn@google.com
nicksauer@google.com
nona@google.com
+
+# Storage team ownership
+
+# Bug component: 655625 = per-file *MediaStore*
+
+per-file *MediaStore* = file:platform/frameworks/base:/core/java/android/os/storage/OWNERS
+per-file Android.bp = file:platform/frameworks/base:/core/java/android/os/storage/OWNERS
+per-file AndroidManifest.xml = file:platform/frameworks/base:/core/java/android/os/storage/OWNERS
+per-file AndroidTest.xml = file:platform/frameworks/base:/core/java/android/os/storage/OWNERS
+per-file OWNERS = file:platform/frameworks/base:/core/java/android/os/storage/OWNERS
diff --git a/tests/tests/wifi/src/android/net/wifi/aware/cts/SingleDeviceTest.java b/tests/tests/wifi/src/android/net/wifi/aware/cts/SingleDeviceTest.java
index 26b5b7f..d063b40 100644
--- a/tests/tests/wifi/src/android/net/wifi/aware/cts/SingleDeviceTest.java
+++ b/tests/tests/wifi/src/android/net/wifi/aware/cts/SingleDeviceTest.java
@@ -629,7 +629,7 @@
* then the attach/destroy will not correspond to enable/disable and will not result in a new
* MAC address being generated.
*/
- public void testAttachDiscoveryAddressChanges() {
+ public void testAttachDiscoveryAddressChanges() throws InterruptedException {
if (!TestUtils.shouldTestWifiAware(getContext())) {
return;
}
@@ -638,6 +638,7 @@
Set<TestUtils.MacWrapper> macs = new HashSet<>();
for (int i = 0; i < numIterations; ++i) {
+ Thread.sleep(1000);
AttachCallbackTest attachCb = new AttachCallbackTest();
IdentityChangedListenerTest identityL = new IdentityChangedListenerTest();
mWifiAwareManager.attach(attachCb, identityL, mHandler);
diff --git a/tests/tests/wifi/src/android/net/wifi/cts/ConcurrencyTest.java b/tests/tests/wifi/src/android/net/wifi/cts/ConcurrencyTest.java
index f7322dd..fad09f4 100644
--- a/tests/tests/wifi/src/android/net/wifi/cts/ConcurrencyTest.java
+++ b/tests/tests/wifi/src/android/net/wifi/cts/ConcurrencyTest.java
@@ -50,6 +50,7 @@
import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.compatibility.common.util.ApiLevelUtil;
import com.android.compatibility.common.util.ShellIdentityUtils;
import com.android.compatibility.common.util.SystemUtil;
@@ -274,6 +275,31 @@
new LinkedList<Integer>(Arrays.asList(waitSingleSync)));
}
+ private NetworkInfo.DetailedState waitForNextNetworkState() {
+ assertTrue(waitForBroadcasts(MySync.NETWORK_INFO));
+ assertNotNull(mMySync.expectedNetworkInfo);
+ return mMySync.expectedNetworkInfo.getDetailedState();
+ }
+
+ private boolean waitForConnectedNetworkState() {
+ // The possible orders of network states are:
+ // * IDLE > CONNECTING > CONNECTED for lazy initialization
+ // * DISCONNECTED > CONNECTING > CONNECTED for previous group removal
+ // * CONNECTING > CONNECTED
+ NetworkInfo.DetailedState state = waitForNextNetworkState();
+ if (state == NetworkInfo.DetailedState.IDLE
+ || state == NetworkInfo.DetailedState.DISCONNECTED) {
+ state = waitForNextNetworkState();
+ }
+ if (ApiLevelUtil.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
+ if (state != NetworkInfo.DetailedState.CONNECTING) {
+ return false;
+ }
+ state = waitForNextNetworkState();
+ }
+ return state == NetworkInfo.DetailedState.CONNECTED;
+ }
+
private boolean waitForServiceResponse(MyResponse waitResponse) {
synchronized (waitResponse) {
long timeout = System.currentTimeMillis() + TIMEOUT_MSEC;
@@ -508,21 +534,7 @@
mWifiP2pManager.createGroup(mWifiP2pChannel, mActionListener);
assertTrue(waitForServiceResponse(mMyResponse));
assertTrue(mMyResponse.success);
-
- // The first network state might be IDLE due to
- // lazy initialization, but not CONNECTED.
- for (int i = 0; i < 2; i++) {
- assertTrue(waitForBroadcasts(MySync.NETWORK_INFO));
- assertNotNull(mMySync.expectedNetworkInfo);
- if (NetworkInfo.DetailedState.CONNECTED ==
- mMySync.expectedNetworkInfo.getDetailedState()) {
- break;
- }
- assertEquals(NetworkInfo.DetailedState.IDLE,
- mMySync.expectedNetworkInfo.getDetailedState());
- }
- assertEquals(NetworkInfo.DetailedState.CONNECTED,
- mMySync.expectedNetworkInfo.getDetailedState());
+ assertTrue(waitForConnectedNetworkState());
resetResponse(mMyResponse);
mWifiP2pManager.requestNetworkInfo(mWifiP2pChannel,
@@ -660,21 +672,7 @@
mWifiP2pManager.createGroup(mWifiP2pChannel, mActionListener);
assertTrue(waitForServiceResponse(mMyResponse));
assertTrue(mMyResponse.success);
-
- // The first network state might be IDLE due to
- // lazy initialization, but not CONNECTED.
- for (int i = 0; i < 2; i++) {
- assertTrue(waitForBroadcasts(MySync.NETWORK_INFO));
- assertNotNull(mMySync.expectedNetworkInfo);
- if (NetworkInfo.DetailedState.CONNECTED ==
- mMySync.expectedNetworkInfo.getDetailedState()) {
- break;
- }
- assertEquals(NetworkInfo.DetailedState.IDLE,
- mMySync.expectedNetworkInfo.getDetailedState());
- }
- assertEquals(NetworkInfo.DetailedState.CONNECTED,
- mMySync.expectedNetworkInfo.getDetailedState());
+ assertTrue(waitForConnectedNetworkState());
resetResponse(mMyResponse);
mWifiP2pManager.removeGroup(mWifiP2pChannel, mActionListener);
@@ -707,10 +705,7 @@
mWifiP2pManager.createGroup(mWifiP2pChannel, mActionListener);
assertTrue(waitForServiceResponse(mMyResponse));
assertTrue(mMyResponse.success);
- assertTrue(waitForBroadcasts(MySync.NETWORK_INFO));
- assertNotNull(mMySync.expectedNetworkInfo);
- assertEquals(NetworkInfo.DetailedState.CONNECTED,
- mMySync.expectedNetworkInfo.getDetailedState());
+ assertTrue(waitForConnectedNetworkState());
resetResponse(mMyResponse);
mWifiP2pManager.removeGroup(mWifiP2pChannel, mActionListener);
@@ -812,21 +807,7 @@
mWifiP2pManager.createGroup(mWifiP2pChannel, mActionListener);
assertTrue(waitForServiceResponse(mMyResponse));
assertTrue(mMyResponse.success);
-
- // The first network state might be IDLE due to
- // lazy initialization, but not CONNECTED.
- for (int i = 0; i < 2; i++) {
- assertTrue(waitForBroadcasts(MySync.NETWORK_INFO));
- assertNotNull(mMySync.expectedNetworkInfo);
- if (NetworkInfo.DetailedState.CONNECTED
- == mMySync.expectedNetworkInfo.getDetailedState()) {
- break;
- }
- assertEquals(NetworkInfo.DetailedState.IDLE,
- mMySync.expectedNetworkInfo.getDetailedState());
- }
- assertEquals(NetworkInfo.DetailedState.CONNECTED,
- mMySync.expectedNetworkInfo.getDetailedState());
+ assertTrue(waitForConnectedNetworkState());
resetResponse(mMyResponse);
MacAddress peerMacAddress = MacAddress.fromString(mTestWifiP2pPeerConfig.deviceAddress);
diff --git a/tests/tests/wifi/src/android/net/wifi/cts/WifiManagerTest.java b/tests/tests/wifi/src/android/net/wifi/cts/WifiManagerTest.java
index e3ff5f8..474d488 100644
--- a/tests/tests/wifi/src/android/net/wifi/cts/WifiManagerTest.java
+++ b/tests/tests/wifi/src/android/net/wifi/cts/WifiManagerTest.java
@@ -97,7 +97,6 @@
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.compatibility.common.util.ApiLevelUtil;
-import com.android.compatibility.common.util.ApiTest;
import com.android.compatibility.common.util.FeatureUtil;
import com.android.compatibility.common.util.PollingCheck;
import com.android.compatibility.common.util.PropertyUtil;
@@ -1881,6 +1880,7 @@
TestExecutor executor = new TestExecutor();
TestSoftApCallback lohsSoftApCallback = new TestSoftApCallback(mLock);
UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+ setWifiEnabled(false);
boolean wifiEnabled = mWifiManager.isWifiEnabled();
try {
uiAutomation.adoptShellPermissionIdentity();
@@ -2912,13 +2912,6 @@
verifySetGetSoftApConfig(softApConfigBuilder.build());
}
- // Test 11 AX control config.
- if (callback.getCurrentSoftApCapability()
- .areFeaturesSupported(SoftApCapability.SOFTAP_FEATURE_IEEE80211_AX)) {
- softApConfigBuilder.setIeee80211axEnabled(true);
- verifySetGetSoftApConfig(softApConfigBuilder.build());
- }
-
// Test 11 BE control config
if (ApiLevelUtil.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
if (callback.getCurrentSoftApCapability()
@@ -2929,6 +2922,12 @@
}
if (ApiLevelUtil.isAtLeast(Build.VERSION_CODES.S)) {
+ // Test 11 AX control config.
+ if (callback.getCurrentSoftApCapability()
+ .areFeaturesSupported(SoftApCapability.SOFTAP_FEATURE_IEEE80211_AX)) {
+ softApConfigBuilder.setIeee80211axEnabled(true);
+ verifySetGetSoftApConfig(softApConfigBuilder.build());
+ }
softApConfigBuilder.setBridgedModeOpportunisticShutdownEnabled(false);
verifySetGetSoftApConfig(softApConfigBuilder.build());
}
@@ -3912,6 +3911,14 @@
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
public void testActiveCountryCodeChangedCallback() throws Exception {
+ if (!hasLocationFeature()) {
+ // skip the test if location is not supported
+ return;
+ }
+ if (!isLocationEnabled()) {
+ fail("Please enable location for this test - since country code is not available"
+ + " when location is disabled!");
+ }
TestActiveCountryCodeChangedCallback testCountryCodeChangedCallback =
new TestActiveCountryCodeChangedCallback();
TestExecutor executor = new TestExecutor();
@@ -5469,7 +5476,6 @@
/**
* Tests {@link WifiConfiguration#setBssidAllowlist(List)}.
*/
- @ApiTest(apis = "android.net.wifi.WifiConfiguration#setBssidAllowlist")
public void testBssidAllowlist() throws Exception {
if (!WifiFeature.isWifiSupported(getContext())) {
// skip the test if WiFi is not supported
diff --git a/tests/uwb/src/android/uwb/cts/RangingSessionTest.java b/tests/uwb/src/android/uwb/cts/RangingSessionTest.java
deleted file mode 100644
index 6fcdc74..0000000
--- a/tests/uwb/src/android/uwb/cts/RangingSessionTest.java
+++ /dev/null
@@ -1,629 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.uwb.cts;
-
-import static android.uwb.RangingSession.Callback.REASON_BAD_PARAMETERS;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import android.os.PersistableBundle;
-import android.os.RemoteException;
-import android.uwb.IUwbAdapter;
-import android.uwb.RangingReport;
-import android.uwb.RangingSession;
-import android.uwb.SessionHandle;
-import android.uwb.UwbAddress;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-import java.util.concurrent.Executor;
-
-/**
- * Test of {@link RangingSession}.
- */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class RangingSessionTest {
- private static final Executor EXECUTOR = UwbTestUtils.getExecutor();
- private static final PersistableBundle PARAMS = new PersistableBundle();
- private static final UwbAddress UWB_ADDRESS = UwbAddress.fromBytes(new byte[] {0x00, 0x56});
- private static final @RangingSession.Callback.Reason int REASON =
- RangingSession.Callback.REASON_GENERIC_ERROR;
-
- @Test
- public void testOnRangingOpened_OnOpenSuccessCalled() {
- SessionHandle handle = new SessionHandle(123);
- RangingSession.Callback callback = mock(RangingSession.Callback.class);
- IUwbAdapter adapter = mock(IUwbAdapter.class);
- RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
- verifyOpenState(session, false);
-
- session.onRangingOpened();
- verifyOpenState(session, true);
-
- // Verify that the onOpenSuccess callback was invoked
- verify(callback, times(1)).onOpened(eq(session));
- verify(callback, times(0)).onClosed(anyInt(), any());
- }
-
- @Test
- public void testOnRangingOpened_OnServiceDiscoveredConnectedCalled() {
- SessionHandle handle = new SessionHandle(123);
- RangingSession.Callback callback = mock(RangingSession.Callback.class);
- IUwbAdapter adapter = mock(IUwbAdapter.class);
- RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
- verifyOpenState(session, false);
-
- session.onRangingOpened();
- verifyOpenState(session, true);
-
- // Verify that the onOpenSuccess callback was invoked
- verify(callback, times(1)).onOpened(eq(session));
- verify(callback, times(0)).onClosed(anyInt(), any());
-
- session.onServiceDiscovered(PARAMS);
- verify(callback, times(1)).onServiceDiscovered(eq(PARAMS));
-
- session.onServiceConnected(PARAMS);
- verify(callback, times(1)).onServiceConnected(eq(PARAMS));
- }
-
-
- @Test
- public void testOnRangingOpened_CannotOpenClosedSession() {
- SessionHandle handle = new SessionHandle(123);
- RangingSession.Callback callback = mock(RangingSession.Callback.class);
- IUwbAdapter adapter = mock(IUwbAdapter.class);
- RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
-
- session.onRangingOpened();
- verifyOpenState(session, true);
- verify(callback, times(1)).onOpened(eq(session));
- verify(callback, times(0)).onClosed(anyInt(), any());
-
- session.onRangingClosed(REASON, PARAMS);
- verifyOpenState(session, false);
- verify(callback, times(1)).onOpened(eq(session));
- verify(callback, times(1)).onClosed(anyInt(), any());
-
- // Now invoke the ranging started callback and ensure the session remains closed
- session.onRangingOpened();
- verifyOpenState(session, false);
- verify(callback, times(1)).onOpened(eq(session));
- verify(callback, times(1)).onClosed(anyInt(), any());
- }
-
- @Test
- public void testOnRangingClosed_OnClosedCalledWhenSessionNotOpen() {
- SessionHandle handle = new SessionHandle(123);
- RangingSession.Callback callback = mock(RangingSession.Callback.class);
- IUwbAdapter adapter = mock(IUwbAdapter.class);
- RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
- verifyOpenState(session, false);
-
- session.onRangingClosed(REASON, PARAMS);
- verifyOpenState(session, false);
-
- // Verify that the onOpenSuccess callback was invoked
- verify(callback, times(0)).onOpened(eq(session));
- verify(callback, times(1)).onClosed(anyInt(), any());
- }
-
- @Test
- public void testOnRangingClosed_OnClosedCalled() {
- SessionHandle handle = new SessionHandle(123);
- RangingSession.Callback callback = mock(RangingSession.Callback.class);
- IUwbAdapter adapter = mock(IUwbAdapter.class);
- RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
- session.onRangingStarted(PARAMS);
- session.onRangingClosed(REASON, PARAMS);
- verify(callback, times(1)).onClosed(anyInt(), any());
-
- verifyOpenState(session, false);
- session.onRangingClosed(REASON, PARAMS);
- verify(callback, times(2)).onClosed(anyInt(), any());
- }
-
- @Test
- public void testOnRangingResult_OnReportReceivedCalled() {
- SessionHandle handle = new SessionHandle(123);
- RangingSession.Callback callback = mock(RangingSession.Callback.class);
- IUwbAdapter adapter = mock(IUwbAdapter.class);
- RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
- verifyOpenState(session, false);
-
- session.onRangingStarted(PARAMS);
- verifyOpenState(session, true);
-
- RangingReport report = UwbTestUtils.getRangingReports(1);
- session.onRangingResult(report);
- verify(callback, times(1)).onReportReceived(eq(report));
- }
-
- @Test
- public void testStart_CannotStartIfAlreadyStarted() throws RemoteException {
- SessionHandle handle = new SessionHandle(123);
- RangingSession.Callback callback = mock(RangingSession.Callback.class);
- IUwbAdapter adapter = mock(IUwbAdapter.class);
- RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
- doAnswer(new StartAnswer(session)).when(adapter).startRanging(any(), any());
- session.onRangingOpened();
-
- session.start(PARAMS);
- verify(callback, times(1)).onStarted(any());
-
- // Calling start again should throw an illegal state
- verifyThrowIllegalState(() -> session.start(PARAMS));
- verify(callback, times(1)).onStarted(any());
- }
-
- @Test
- public void testStop_CannotStopIfAlreadyStopped() throws RemoteException {
- SessionHandle handle = new SessionHandle(123);
- RangingSession.Callback callback = mock(RangingSession.Callback.class);
- IUwbAdapter adapter = mock(IUwbAdapter.class);
- RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
- doAnswer(new StartAnswer(session)).when(adapter).startRanging(any(), any());
- doAnswer(new StopAnswer(session)).when(adapter).stopRanging(any());
- session.onRangingOpened();
- session.start(PARAMS);
-
- verifyNoThrowIllegalState(session::stop);
- verify(callback, times(1)).onStopped(anyInt(), any());
-
- // Calling stop again should throw an illegal state
- verifyThrowIllegalState(session::stop);
- verify(callback, times(1)).onStopped(anyInt(), any());
- }
-
- @Test
- public void testStop_CannotStopIfOpenFailed() throws RemoteException {
- SessionHandle handle = new SessionHandle(123);
- RangingSession.Callback callback = mock(RangingSession.Callback.class);
- IUwbAdapter adapter = mock(IUwbAdapter.class);
- RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
- doAnswer(new StartAnswer(session)).when(adapter).startRanging(any(), any());
- doAnswer(new StopAnswer(session)).when(adapter).stopRanging(any());
- session.onRangingOpened();
- session.start(PARAMS);
-
- verifyNoThrowIllegalState(() -> session.onRangingOpenFailed(REASON_BAD_PARAMETERS, PARAMS));
- verify(callback, times(1)).onOpenFailed(
- REASON_BAD_PARAMETERS, PARAMS);
-
- // Calling stop again should throw an illegal state
- verifyThrowIllegalState(session::stop);
- verify(callback, times(0)).onStopped(anyInt(), any());
- }
-
- @Test
- public void testCallbacks_OnlyWhenOpened() throws RemoteException {
- SessionHandle handle = new SessionHandle(123);
- RangingSession.Callback callback = mock(RangingSession.Callback.class);
- IUwbAdapter adapter = mock(IUwbAdapter.class);
- RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
- doAnswer(new OpenAnswer(session)).when(adapter).openRanging(
- any(), any(), any(), any(), any());
- doAnswer(new StartAnswer(session)).when(adapter).startRanging(any(), any());
- doAnswer(new ReconfigureAnswer(session)).when(adapter).reconfigureRanging(any(), any());
- doAnswer(new PauseAnswer(session)).when(adapter).pause(any(), any());
- doAnswer(new ResumeAnswer(session)).when(adapter).resume(any(), any());
- doAnswer(new ControleeAddAnswer(session)).when(adapter).addControlee(any(), any());
- doAnswer(new ControleeRemoveAnswer(session)).when(adapter).removeControlee(any(), any());
- doAnswer(new DataSendAnswer(session)).when(adapter).sendData(any(), any(), any(), any());
- doAnswer(new StopAnswer(session)).when(adapter).stopRanging(any());
- doAnswer(new CloseAnswer(session)).when(adapter).closeRanging(any());
-
- verifyThrowIllegalState(() -> session.reconfigure(PARAMS));
- verify(callback, times(0)).onReconfigured(any());
- verifyOpenState(session, false);
-
- session.onRangingOpened();
- verifyOpenState(session, true);
- verify(callback, times(1)).onOpened(any());
- verifyNoThrowIllegalState(() -> session.reconfigure(PARAMS));
- verify(callback, times(1)).onReconfigured(any());
- verifyThrowIllegalState(() -> session.pause(PARAMS));
- verify(callback, times(0)).onPaused(any());
- verifyThrowIllegalState(() -> session.resume(PARAMS));
- verify(callback, times(0)).onResumed(any());
- verifyNoThrowIllegalState(() -> session.addControlee(PARAMS));
- verify(callback, times(1)).onControleeAdded(any());
- verifyNoThrowIllegalState(() -> session.removeControlee(PARAMS));
- verify(callback, times(1)).onControleeRemoved(any());
- verifyThrowIllegalState(() -> session.sendData(
- UWB_ADDRESS, PARAMS, new byte[] {0x05, 0x1}));
- verify(callback, times(0)).onDataSent(any(), any());
-
- session.onRangingStartFailed(REASON_BAD_PARAMETERS, PARAMS);
- verifyOpenState(session, true);
- verify(callback, times(1)).onStartFailed(
- REASON_BAD_PARAMETERS, PARAMS);
-
- session.onRangingStarted(PARAMS);
- verifyOpenState(session, true);
- verifyNoThrowIllegalState(() -> session.reconfigure(PARAMS));
- verify(callback, times(2)).onReconfigured(any());
- verifyNoThrowIllegalState(() -> session.reconfigure(null));
- verify(callback, times(1)).onReconfigureFailed(
- eq(REASON_BAD_PARAMETERS), any());
- verifyNoThrowIllegalState(() -> session.pause(PARAMS));
- verify(callback, times(1)).onPaused(any());
- verifyNoThrowIllegalState(() -> session.pause(null));
- verify(callback, times(1)).onPauseFailed(
- eq(REASON_BAD_PARAMETERS), any());
- verifyNoThrowIllegalState(() -> session.resume(PARAMS));
- verify(callback, times(1)).onResumed(any());
- verifyNoThrowIllegalState(() -> session.resume(null));
- verify(callback, times(1)).onResumeFailed(
- eq(REASON_BAD_PARAMETERS), any());
- verifyNoThrowIllegalState(() -> session.addControlee(PARAMS));
- verify(callback, times(2)).onControleeAdded(any());
- verifyNoThrowIllegalState(() -> session.addControlee(null));
- verify(callback, times(1)).onControleeAddFailed(
- eq(REASON_BAD_PARAMETERS), any());
- verifyNoThrowIllegalState(() -> session.removeControlee(PARAMS));
- verify(callback, times(2)).onControleeRemoved(any());
- verifyNoThrowIllegalState(() -> session.removeControlee(null));
- verify(callback, times(1)).onControleeRemoveFailed(
- eq(REASON_BAD_PARAMETERS), any());
- verifyNoThrowIllegalState(() -> session.sendData(
- UWB_ADDRESS, PARAMS, new byte[] {0x05, 0x1}));
- verify(callback, times(1)).onDataSent(any(), any());
- verifyNoThrowIllegalState(() -> session.sendData(
- null, PARAMS, new byte[] {0x05, 0x1}));
- verify(callback, times(1)).onDataSendFailed(
- eq(null), eq(REASON_BAD_PARAMETERS), any());
-
- session.onDataReceived(UWB_ADDRESS, PARAMS, new byte[] {0x5, 0x7});
- verify(callback, times(1)).onDataReceived(
- UWB_ADDRESS, PARAMS, new byte[] {0x5, 0x7});
- session.onDataReceiveFailed(UWB_ADDRESS, REASON_BAD_PARAMETERS, PARAMS);
- verify(callback, times(1)).onDataReceiveFailed(
- UWB_ADDRESS, REASON_BAD_PARAMETERS, PARAMS);
-
- session.stop();
- verifyOpenState(session, true);
- verify(callback, times(1)).onStopped(REASON, PARAMS);
-
- verifyNoThrowIllegalState(() -> session.reconfigure(PARAMS));
- verify(callback, times(3)).onReconfigured(any());
- verifyThrowIllegalState(() -> session.pause(PARAMS));
- verify(callback, times(1)).onPaused(any());
- verifyThrowIllegalState(() -> session.resume(PARAMS));
- verify(callback, times(1)).onResumed(any());
- verifyNoThrowIllegalState(() -> session.addControlee(PARAMS));
- verify(callback, times(3)).onControleeAdded(any());
- verifyNoThrowIllegalState(() -> session.removeControlee(PARAMS));
- verify(callback, times(3)).onControleeRemoved(any());
- verifyThrowIllegalState(() -> session.sendData(
- UWB_ADDRESS, PARAMS, new byte[] {0x05, 0x1}));
- verify(callback, times(1)).onDataSent(any(), any());
-
- session.close();
- verifyOpenState(session, false);
- verify(callback, times(1)).onClosed(REASON, PARAMS);
-
- verifyThrowIllegalState(() -> session.reconfigure(PARAMS));
- verify(callback, times(3)).onReconfigured(any());
- verifyThrowIllegalState(() -> session.pause(PARAMS));
- verify(callback, times(1)).onPaused(any());
- verifyThrowIllegalState(() -> session.resume(PARAMS));
- verify(callback, times(1)).onResumed(any());
- verifyThrowIllegalState(() -> session.addControlee(PARAMS));
- verify(callback, times(3)).onControleeAdded(any());
- verifyThrowIllegalState(() -> session.removeControlee(PARAMS));
- verify(callback, times(3)).onControleeRemoved(any());
- verifyThrowIllegalState(() -> session.sendData(
- UWB_ADDRESS, PARAMS, new byte[] {0x05, 0x1}));
- verify(callback, times(1)).onDataSent(any(), any());
- }
-
- @Test
- public void testClose_NoCallbackUntilInvoked() throws RemoteException {
- SessionHandle handle = new SessionHandle(123);
- RangingSession.Callback callback = mock(RangingSession.Callback.class);
- IUwbAdapter adapter = mock(IUwbAdapter.class);
- RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
- session.onRangingOpened();
-
- // Calling close multiple times should invoke closeRanging until the session receives
- // the onClosed callback.
- int totalCallsBeforeOnRangingClosed = 3;
- for (int i = 1; i <= totalCallsBeforeOnRangingClosed; i++) {
- session.close();
- verifyOpenState(session, true);
- verify(adapter, times(i)).closeRanging(handle);
- verify(callback, times(0)).onClosed(anyInt(), any());
- }
-
- // After onClosed is invoked, then the adapter should no longer be called for each call to
- // the session's close.
- final int totalCallsAfterOnRangingClosed = 2;
- for (int i = 1; i <= totalCallsAfterOnRangingClosed; i++) {
- session.onRangingClosed(REASON, PARAMS);
- verifyOpenState(session, false);
- verify(adapter, times(totalCallsBeforeOnRangingClosed)).closeRanging(handle);
- verify(callback, times(i)).onClosed(anyInt(), any());
- }
- }
-
- @Test
- public void testClose_OnClosedCalled() throws RemoteException {
- SessionHandle handle = new SessionHandle(123);
- RangingSession.Callback callback = mock(RangingSession.Callback.class);
- IUwbAdapter adapter = mock(IUwbAdapter.class);
- RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
- doAnswer(new CloseAnswer(session)).when(adapter).closeRanging(any());
- session.onRangingOpened();
-
- session.close();
- verify(callback, times(1)).onClosed(anyInt(), any());
- }
-
- @Test
- public void testClose_CannotInteractFurther() throws RemoteException {
- SessionHandle handle = new SessionHandle(123);
- RangingSession.Callback callback = mock(RangingSession.Callback.class);
- IUwbAdapter adapter = mock(IUwbAdapter.class);
- RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
- doAnswer(new CloseAnswer(session)).when(adapter).closeRanging(any());
- session.close();
-
- verifyThrowIllegalState(() -> session.start(PARAMS));
- verifyThrowIllegalState(() -> session.reconfigure(PARAMS));
- verifyThrowIllegalState(() -> session.stop());
- verifyNoThrowIllegalState(() -> session.close());
- }
-
- @Test
- public void testOnRangingResult_OnReportReceivedCalledWhenOpen() {
- SessionHandle handle = new SessionHandle(123);
- RangingSession.Callback callback = mock(RangingSession.Callback.class);
- IUwbAdapter adapter = mock(IUwbAdapter.class);
- RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
-
- assertFalse(session.isOpen());
- session.onRangingStarted(PARAMS);
- assertTrue(session.isOpen());
-
- // Verify that the onReportReceived callback was invoked
- RangingReport report = UwbTestUtils.getRangingReports(1);
- session.onRangingResult(report);
- verify(callback, times(1)).onReportReceived(report);
- }
-
- @Test
- public void testOnRangingResult_OnReportReceivedNotCalledWhenNotOpen() {
- SessionHandle handle = new SessionHandle(123);
- RangingSession.Callback callback = mock(RangingSession.Callback.class);
- IUwbAdapter adapter = mock(IUwbAdapter.class);
- RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
-
- assertFalse(session.isOpen());
-
- // Verify that the onReportReceived callback was invoked
- RangingReport report = UwbTestUtils.getRangingReports(1);
- session.onRangingResult(report);
- verify(callback, times(0)).onReportReceived(report);
- }
-
- private void verifyOpenState(RangingSession session, boolean expected) {
- assertEquals(expected, session.isOpen());
- }
-
- private void verifyThrowIllegalState(Runnable runnable) {
- try {
- runnable.run();
- fail();
- } catch (IllegalStateException e) {
- // Pass
- }
- }
-
- private void verifyNoThrowIllegalState(Runnable runnable) {
- try {
- runnable.run();
- } catch (IllegalStateException e) {
- fail();
- }
- }
-
- abstract class AdapterAnswer implements Answer {
- protected RangingSession mSession;
-
- protected AdapterAnswer(RangingSession session) {
- mSession = session;
- }
- }
-
- class OpenAnswer extends AdapterAnswer {
- OpenAnswer(RangingSession session) {
- super(session);
- }
-
- @Override
- public Object answer(InvocationOnMock invocation) {
- PersistableBundle argParams = invocation.getArgument(1);
- if (argParams != null) {
- mSession.onRangingOpened();
- } else {
- mSession.onRangingOpenFailed(REASON_BAD_PARAMETERS, PARAMS);
- }
- return null;
- }
- }
-
- class StartAnswer extends AdapterAnswer {
- StartAnswer(RangingSession session) {
- super(session);
- }
-
- @Override
- public Object answer(InvocationOnMock invocation) {
- PersistableBundle argParams = invocation.getArgument(1);
- if (argParams != null) {
- mSession.onRangingStarted(PARAMS);
- } else {
- mSession.onRangingStartFailed(REASON_BAD_PARAMETERS, PARAMS);
- }
- return null;
- }
- }
-
- class ReconfigureAnswer extends AdapterAnswer {
- ReconfigureAnswer(RangingSession session) {
- super(session);
- }
-
- @Override
- public Object answer(InvocationOnMock invocation) {
- PersistableBundle argParams = invocation.getArgument(1);
- if (argParams != null) {
- mSession.onRangingReconfigured(PARAMS);
- } else {
- mSession.onRangingReconfigureFailed(REASON_BAD_PARAMETERS, PARAMS);
- }
- return null;
- }
- }
-
- class PauseAnswer extends AdapterAnswer {
- PauseAnswer(RangingSession session) {
- super(session);
- }
-
- @Override
- public Object answer(InvocationOnMock invocation) {
- PersistableBundle argParams = invocation.getArgument(1);
- if (argParams != null) {
- mSession.onRangingPaused(PARAMS);
- } else {
- mSession.onRangingPauseFailed(REASON_BAD_PARAMETERS, PARAMS);
- }
- return null;
- }
- }
-
- class ResumeAnswer extends AdapterAnswer {
- ResumeAnswer(RangingSession session) {
- super(session);
- }
-
- @Override
- public Object answer(InvocationOnMock invocation) {
- PersistableBundle argParams = invocation.getArgument(1);
- if (argParams != null) {
- mSession.onRangingResumed(PARAMS);
- } else {
- mSession.onRangingResumeFailed(REASON_BAD_PARAMETERS, PARAMS);
- }
- return null;
- }
- }
-
- class ControleeAddAnswer extends AdapterAnswer {
- ControleeAddAnswer(RangingSession session) {
- super(session);
- }
-
- @Override
- public Object answer(InvocationOnMock invocation) {
- PersistableBundle argParams = invocation.getArgument(1);
- if (argParams != null) {
- mSession.onControleeAdded(PARAMS);
- } else {
- mSession.onControleeAddFailed(REASON_BAD_PARAMETERS, PARAMS);
- }
- return null;
- }
- }
-
- class ControleeRemoveAnswer extends AdapterAnswer {
- ControleeRemoveAnswer(RangingSession session) {
- super(session);
- }
-
- @Override
- public Object answer(InvocationOnMock invocation) {
- PersistableBundle argParams = invocation.getArgument(1);
- if (argParams != null) {
- mSession.onControleeRemoved(PARAMS);
- } else {
- mSession.onControleeRemoveFailed(REASON_BAD_PARAMETERS, PARAMS);
- }
- return null;
- }
- }
-
- class DataSendAnswer extends AdapterAnswer {
- DataSendAnswer(RangingSession session) {
- super(session);
- }
-
- @Override
- public Object answer(InvocationOnMock invocation) {
- UwbAddress argParams = invocation.getArgument(1);
- if (argParams != null) {
- mSession.onDataSent(UWB_ADDRESS, PARAMS);
- } else {
- mSession.onDataSendFailed(null, REASON_BAD_PARAMETERS, PARAMS);
- }
- return null;
- }
- }
-
- class StopAnswer extends AdapterAnswer {
- StopAnswer(RangingSession session) {
- super(session);
- }
-
- @Override
- public Object answer(InvocationOnMock invocation) {
- mSession.onRangingStopped(REASON, PARAMS);
- return null;
- }
- }
-
- class CloseAnswer extends AdapterAnswer {
- CloseAnswer(RangingSession session) {
- super(session);
- }
-
- @Override
- public Object answer(InvocationOnMock invocation) {
- mSession.onRangingClosed(REASON, PARAMS);
- return null;
- }
- }
-}
diff --git a/tests/uwb/src/android/uwb/cts/SessionHandleTest.java b/tests/uwb/src/android/uwb/cts/SessionHandleTest.java
deleted file mode 100644
index d52a3e7..0000000
--- a/tests/uwb/src/android/uwb/cts/SessionHandleTest.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.uwb.cts;
-
-import static org.junit.Assert.assertEquals;
-
-import android.os.Parcel;
-import android.uwb.SessionHandle;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Test of {@link SessionHandle}.
- */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class SessionHandleTest {
-
- @Test
- public void testBasic() {
- int handleId = 12;
- SessionHandle handle = new SessionHandle(handleId);
- assertEquals(handle.getId(), handleId);
- }
-
- @Test
- public void testParcel() {
- Parcel parcel = Parcel.obtain();
- SessionHandle handle = new SessionHandle(10);
- handle.writeToParcel(parcel, 0);
- parcel.setDataPosition(0);
- SessionHandle fromParcel = SessionHandle.CREATOR.createFromParcel(parcel);
- assertEquals(handle, fromParcel);
- }
-}