fix: 引入Settings的Module
This commit is contained in:
31
car-qc-lib/Android.bp
Normal file
31
car-qc-lib/Android.bp
Normal file
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// 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 {
|
||||
default_applicable_licenses: ["Android-Apache-2.0"],
|
||||
}
|
||||
|
||||
android_library {
|
||||
name: "car-qc-lib",
|
||||
srcs: ["src/**/*.java"],
|
||||
optimize: {
|
||||
enabled: false,
|
||||
},
|
||||
static_libs: [
|
||||
"androidx.annotation_annotation",
|
||||
"car-ui-lib-no-overlayable",
|
||||
"car-resource-common",
|
||||
],
|
||||
}
|
||||
19
car-qc-lib/AndroidManifest.xml
Normal file
19
car-qc-lib/AndroidManifest.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.android.car.qc">
|
||||
</manifest>
|
||||
8
car-qc-lib/OWNERS
Normal file
8
car-qc-lib/OWNERS
Normal file
@@ -0,0 +1,8 @@
|
||||
# People who can approve changes for submission.
|
||||
|
||||
# Primary
|
||||
alexstetson@google.com
|
||||
|
||||
# Secondary (only if people in Primary are unreachable)
|
||||
babakbo@google.com
|
||||
igorr@google.com
|
||||
7
car-qc-lib/PREUPLOAD.cfg
Normal file
7
car-qc-lib/PREUPLOAD.cfg
Normal file
@@ -0,0 +1,7 @@
|
||||
[Hook Scripts]
|
||||
checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
|
||||
ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py -f ${PREUPLOAD_FILES}
|
||||
|
||||
[Builtin Hooks]
|
||||
commit_msg_changeid_field = true
|
||||
commit_msg_test_field = true
|
||||
50
car-qc-lib/build.gradle
Normal file
50
car-qc-lib/build.gradle
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Include this gradle file if you are building against this as a standalone gradle library project,
|
||||
* as opposed to building it as part of the git-tree. This is typically the file you want to include
|
||||
* if you create a new project in Android Studio. Besides, SetupDesign library is dependent with
|
||||
* SetupCompat library, you should also include setupcompat library in project.
|
||||
*
|
||||
* For example, you can include the following in your settings.gradle file:
|
||||
* include ':setupdesign'
|
||||
* project(':setupdesign').projectDir = new File(PATH_TO_THIS_DIRECTORY)
|
||||
* include ':setupcompat'
|
||||
* project(':setupcompat').projectDir = new File(PATH_TO_THIS_DIRECTORY)
|
||||
*
|
||||
* And then you can include the :setupdesign project as one of your dependencies
|
||||
* dependencies {
|
||||
* implementation project(path: ':setupdesign')
|
||||
* }
|
||||
*/
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 34
|
||||
namespace = "com.android.car.qc"
|
||||
defaultConfig {
|
||||
minSdkVersion 31
|
||||
targetSdkVersion 34
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard.flags'
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
manifest.srcFile 'AndroidManifest.xml'
|
||||
java.srcDirs = ['src']
|
||||
res.srcDirs = ['res']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation libs.androidx.annotation.annotation
|
||||
}
|
||||
27
car-qc-lib/res/color/qc_toggle_background_color.xml
Normal file
27
car-qc-lib/res/color/qc_toggle_background_color.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="false" android:state_enabled="false"
|
||||
android:alpha="?android:attr/disabledAlpha"
|
||||
android:color="@color/qc_toggle_off_background_color"/>
|
||||
<item android:state_checked="false"
|
||||
android:color="@color/qc_toggle_off_background_color"/>
|
||||
<item android:state_enabled="false"
|
||||
android:alpha="?android:attr/disabledAlpha"
|
||||
android:color="?android:attr/colorAccent"/>
|
||||
<item android:color="?android:attr/colorAccent"/>
|
||||
</selector>
|
||||
27
car-qc-lib/res/color/qc_toggle_icon_fill_color.xml
Normal file
27
car-qc-lib/res/color/qc_toggle_icon_fill_color.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="false" android:state_enabled="false"
|
||||
android:alpha="?android:attr/disabledAlpha"
|
||||
android:color="@android:color/white"/>
|
||||
<item android:state_checked="false"
|
||||
android:color="@android:color/white"/>
|
||||
<item android:state_enabled="false"
|
||||
android:alpha="?android:attr/disabledAlpha"
|
||||
android:color="@android:color/black"/>
|
||||
<item android:color="@android:color/black"/>
|
||||
</selector>
|
||||
21
car-qc-lib/res/drawable/qc_row_action_divider.xml
Normal file
21
car-qc-lib/res/drawable/qc_row_action_divider.xml
Normal file
@@ -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.
|
||||
-->
|
||||
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<size
|
||||
android:height="0dp"
|
||||
android:width="@dimen/qc_row_horizontal_margin"/>
|
||||
</shape>
|
||||
27
car-qc-lib/res/drawable/qc_seekbar_wrapper_background.xml
Normal file
27
car-qc-lib/res/drawable/qc_seekbar_wrapper_background.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Highlight the wrapper when it's focused but not selected. The wrapper is selected in
|
||||
direct manipulation mode. -->
|
||||
<item android:state_focused="true" android:state_selected="false">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/car_ui_rotary_focus_fill_color"/>
|
||||
<stroke android:width="@dimen/car_ui_rotary_focus_stroke_width"
|
||||
android:color="@color/car_ui_rotary_focus_stroke_color"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
28
car-qc-lib/res/drawable/qc_toggle_background.xml
Normal file
28
car-qc-lib/res/drawable/qc_toggle_background.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@android:id/background"
|
||||
android:width="@dimen/qc_toggle_background_size"
|
||||
android:height="@dimen/qc_toggle_background_size"
|
||||
android:start="@dimen/qc_toggle_background_padding"
|
||||
android:top="@dimen/qc_toggle_background_padding"
|
||||
android:drawable="@drawable/qc_toggle_button_background">
|
||||
</item>
|
||||
<item android:width="@dimen/qc_toggle_rotary_highlight_size"
|
||||
android:height="@dimen/qc_toggle_rotary_highlight_size"
|
||||
android:drawable="@drawable/qc_toggle_rotary_background"/>
|
||||
</layer-list>
|
||||
34
car-qc-lib/res/drawable/qc_toggle_button_background.xml
Normal file
34
car-qc-lib/res/drawable/qc_toggle_button_background.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<selector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item app:state_toggle_unavailable="true">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/qc_toggle_unavailable_background_color" />
|
||||
<stroke android:color="@color/qc_toggle_unavailable_color"
|
||||
android:width="@dimen/qc_toggle_unavailable_outline_width" />
|
||||
<corners android:radius="@dimen/qc_toggle_background_radius" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/qc_toggle_background_color" />
|
||||
<corners android:radius="@dimen/qc_toggle_background_radius" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
28
car-qc-lib/res/drawable/qc_toggle_rotary_background.xml
Normal file
28
car-qc-lib/res/drawable/qc_toggle_rotary_background.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:width="@dimen/qc_toggle_rotary_shadow_size"
|
||||
android:height="@dimen/qc_toggle_rotary_shadow_size"
|
||||
android:start="@dimen/qc_toggle_rotary_shadow_padding"
|
||||
android:top="@dimen/qc_toggle_rotary_shadow_padding"
|
||||
android:drawable="@drawable/qc_toggle_rotary_shadow">
|
||||
</item>
|
||||
<item android:width="@dimen/qc_toggle_rotary_highlight_size"
|
||||
android:height="@dimen/qc_toggle_rotary_highlight_size"
|
||||
android:drawable="@drawable/qc_toggle_rotary_highlight"/>
|
||||
</layer-list>
|
||||
34
car-qc-lib/res/drawable/qc_toggle_rotary_highlight.xml
Normal file
34
car-qc-lib/res/drawable/qc_toggle_rotary_highlight.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_focused="true" android:state_pressed="true">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/car_ui_rotary_focus_pressed_fill_secondary_color"/>
|
||||
<stroke android:width="@dimen/car_ui_rotary_focus_pressed_stroke_width"
|
||||
android:color="@color/car_ui_rotary_focus_stroke_color"/>
|
||||
<corners android:radius="@dimen/qc_toggle_rotary_highlight_radius" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:state_focused="true">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/car_ui_rotary_focus_fill_color"/>
|
||||
<stroke android:width="@dimen/car_ui_rotary_focus_stroke_width"
|
||||
android:color="@color/car_ui_rotary_focus_stroke_color"/>
|
||||
<corners android:radius="@dimen/qc_toggle_rotary_highlight_radius" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
25
car-qc-lib/res/drawable/qc_toggle_rotary_shadow.xml
Normal file
25
car-qc-lib/res/drawable/qc_toggle_rotary_shadow.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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.
|
||||
-->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_focused="true" android:state_pressed="false">
|
||||
<shape android:shape="rectangle">
|
||||
<stroke android:width="@dimen/qc_toggle_rotary_shadow_width"
|
||||
android:color="@color/qc_toggle_rotary_shadow_color"/>
|
||||
<corners android:radius="@dimen/qc_toggle_rotary_shadow_radius" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
33
car-qc-lib/res/drawable/qc_toggle_unavailable_background.xml
Normal file
33
car-qc-lib/res/drawable/qc_toggle_unavailable_background.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@android:id/background"
|
||||
android:width="@dimen/qc_toggle_background_size"
|
||||
android:height="@dimen/qc_toggle_background_size"
|
||||
android:start="@dimen/qc_toggle_background_padding"
|
||||
android:top="@dimen/qc_toggle_background_padding">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/qc_toggle_unavailable_background_color" />
|
||||
<stroke android:color="@color/qc_toggle_unavailable_color"
|
||||
android:width="@dimen/qc_toggle_unavailable_outline_width" />
|
||||
<corners android:radius="@dimen/qc_toggle_background_radius" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:width="@dimen/qc_toggle_rotary_highlight_size"
|
||||
android:height="@dimen/qc_toggle_rotary_highlight_size"
|
||||
android:drawable="@drawable/qc_toggle_rotary_background"/>
|
||||
</layer-list>
|
||||
21
car-qc-lib/res/layout/qc_action_switch.xml
Normal file
21
car-qc-lib/res/layout/qc_action_switch.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<com.android.car.ui.uxr.DrawableStateSwitch
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@android:id/switch_widget"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
25
car-qc-lib/res/layout/qc_action_toggle.xml
Normal file
25
car-qc-lib/res/layout/qc_action_toggle.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<com.android.car.ui.uxr.DrawableStateToggleButton
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/qc_toggle_button"
|
||||
android:background="@android:color/transparent"
|
||||
android:defaultFocusHighlightEnabled="false"
|
||||
android:minHeight="0dp"
|
||||
android:minWidth="0dp"
|
||||
android:layout_width="@dimen/qc_toggle_size"
|
||||
android:layout_height="@dimen/qc_toggle_size"/>
|
||||
143
car-qc-lib/res/layout/qc_row_view.xml
Normal file
143
car-qc-lib/res/layout/qc_row_view.xml
Normal file
@@ -0,0 +1,143 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<com.android.car.ui.uxr.DrawableStateConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginVertical="@dimen/qc_row_margin_vertical"
|
||||
android:clipToPadding="false"
|
||||
android:minHeight="@dimen/qc_row_min_height"
|
||||
android:paddingEnd="@dimen/qc_row_padding_end"
|
||||
android:paddingStart="@dimen/qc_row_padding_start">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/qc_row_start_items"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/qc_action_items_horizontal_margin"
|
||||
android:orientation="horizontal"
|
||||
android:divider="@drawable/qc_row_action_divider"
|
||||
android:showDividers="middle"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/qc_row_content"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"/>
|
||||
|
||||
<com.android.car.ui.uxr.DrawableStateConstraintLayout
|
||||
android:id="@+id/qc_row_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
app:layout_constraintStart_toEndOf="@+id/qc_row_start_items"
|
||||
app:layout_constraintEnd_toStartOf="@+id/qc_row_end_items"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintHeight_default="wrap"
|
||||
app:layout_constraintHeight_min="@dimen/qc_row_min_height">
|
||||
|
||||
<com.android.car.ui.uxr.DrawableStateImageView
|
||||
android:id="@+id/qc_icon"
|
||||
android:layout_width="@dimen/qc_row_icon_size"
|
||||
android:layout_height="@dimen/qc_row_icon_size"
|
||||
android:layout_marginEnd="@dimen/qc_row_icon_margin_end"
|
||||
android:scaleType="fitCenter"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/barrier1"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/barrier2"/>
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="end"
|
||||
app:barrierAllowsGoneWidgets="false"/>
|
||||
|
||||
<com.android.car.ui.uxr.DrawableStateTextView
|
||||
android:id="@+id/qc_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:singleLine="true"
|
||||
style="@style/TextAppearance.QC.Title"
|
||||
app:layout_constraintStart_toEndOf="@+id/barrier1"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/qc_summary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"/>
|
||||
|
||||
<com.android.car.ui.uxr.DrawableStateTextView
|
||||
android:id="@+id/qc_summary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
style="@style/TextAppearance.QC.Subtitle"
|
||||
app:layout_constraintStart_toEndOf="@+id/barrier1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/qc_title"
|
||||
app:layout_constraintBottom_toTopOf="@+id/barrier2"/>
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="top"
|
||||
app:constraint_referenced_ids="qc_seekbar_wrapper"/>
|
||||
|
||||
<androidx.preference.UnPressableLinearLayout
|
||||
android:id="@+id/qc_seekbar_wrapper"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@dimen/qc_seekbar_padding_top"
|
||||
android:focusable="true"
|
||||
android:background="@drawable/qc_seekbar_wrapper_background"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:layout_centerVertical="true"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toEndOf="@+id/barrier1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/barrier2"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
<com.android.car.qc.view.QCSeekBarView
|
||||
android:id="@+id/qc_seekbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Widget.QC.SeekBar"/>
|
||||
</androidx.preference.UnPressableLinearLayout>
|
||||
|
||||
</com.android.car.ui.uxr.DrawableStateConstraintLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/qc_row_end_items"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/qc_action_items_horizontal_margin"
|
||||
android:orientation="horizontal"
|
||||
android:divider="@drawable/qc_row_action_divider"
|
||||
android:showDividers="middle"
|
||||
app:layout_constraintStart_toEndOf="@+id/qc_row_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
</com.android.car.ui.uxr.DrawableStateConstraintLayout>
|
||||
41
car-qc-lib/res/layout/qc_tile_view.xml
Normal file
41
car-qc-lib/res/layout/qc_tile_view.xml
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<com.android.car.ui.uxr.DrawableStateLinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/qc_tile_wrapper"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:background="?android:attr/selectableItemBackground">
|
||||
<com.android.car.ui.uxr.DrawableStateToggleButton
|
||||
android:id="@+id/qc_tile_toggle_button"
|
||||
android:background="@android:color/transparent"
|
||||
android:layout_width="@dimen/qc_toggle_size"
|
||||
android:layout_height="@dimen/qc_toggle_size"
|
||||
android:layout_marginTop="@dimen/qc_toggle_margin"
|
||||
android:layout_marginBottom="@dimen/qc_toggle_margin"
|
||||
android:layout_marginStart="@dimen/qc_toggle_margin"
|
||||
android:layout_marginEnd="@dimen/qc_toggle_margin"
|
||||
android:clickable="false"
|
||||
android:focusable="false"/>
|
||||
<com.android.car.ui.uxr.DrawableStateTextView
|
||||
android:id="@android:id/summary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/TextAppearance.QC.Subtitle"/>
|
||||
</com.android.car.ui.uxr.DrawableStateLinearLayout>
|
||||
19
car-qc-lib/res/values/attrs.xml
Normal file
19
car-qc-lib/res/values/attrs.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<attr name="state_toggle_unavailable"/>
|
||||
</resources>
|
||||
31
car-qc-lib/res/values/colors.xml
Normal file
31
car-qc-lib/res/values/colors.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<color name="qc_start_icon_color">@android:color/white</color>
|
||||
<color name="qc_toggle_off_background_color">#626262</color>
|
||||
<color name="qc_toggle_unavailable_background_color">@android:color/transparent</color>
|
||||
<color name="qc_toggle_unavailable_color">#37FFFFFF</color>
|
||||
<color name="qc_toggle_rotary_shadow_color">#C7000000</color>
|
||||
<!-- The SeekBar thumb color. -->
|
||||
<color name="qc_seekbar_thumb">#FFFFFF</color>
|
||||
<!-- The SeekBar thumb color when disabled. Use for the dark theme. -->
|
||||
<color name="qc_seekbar_thumb_disabled_on_dark">#757575</color>
|
||||
<!-- The Switch thumb color. -->
|
||||
<color name="qc_switch_thumb_color">#FFFFFF</color>
|
||||
<!-- The Switch thumb color when disabled. Use for the dark theme. -->
|
||||
<color name="qc_switch_thumb_color_disabled_on_dark">#757575</color>
|
||||
</resources>
|
||||
44
car-qc-lib/res/values/dimens.xml
Normal file
44
car-qc-lib/res/values/dimens.xml
Normal file
@@ -0,0 +1,44 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<dimen name="qc_row_padding_start">32dp</dimen>
|
||||
<dimen name="qc_row_padding_end">32dp</dimen>
|
||||
<dimen name="qc_row_min_height">76dp</dimen>
|
||||
<dimen name="qc_row_margin_vertical">10dp</dimen>
|
||||
<dimen name="qc_row_icon_size">44dp</dimen>
|
||||
<dimen name="qc_row_icon_margin_end">32dp</dimen>
|
||||
<dimen name="qc_row_content_margin">16dp</dimen>
|
||||
|
||||
<dimen name="qc_action_items_horizontal_margin">32dp</dimen>
|
||||
<dimen name="qc_toggle_size">80dp</dimen>
|
||||
<dimen name="qc_toggle_background_size">72dp</dimen>
|
||||
<dimen name="qc_toggle_margin">12dp</dimen>
|
||||
<dimen name="qc_row_horizontal_margin">16dp</dimen>
|
||||
<dimen name="qc_toggle_background_radius">16dp</dimen>
|
||||
<dimen name="qc_toggle_background_padding">4dp</dimen>
|
||||
<dimen name="qc_toggle_foreground_icon_inset">18dp</dimen>
|
||||
<dimen name="qc_toggle_unavailable_outline_width">2dp</dimen>
|
||||
<dimen name="qc_toggle_rotary_highlight_size">80dp</dimen>
|
||||
<dimen name="qc_toggle_rotary_highlight_radius">20dp</dimen>
|
||||
<dimen name="qc_toggle_rotary_shadow_size">64dp</dimen>
|
||||
<dimen name="qc_toggle_rotary_shadow_width">4dp</dimen>
|
||||
<dimen name="qc_toggle_rotary_shadow_radius">16dp</dimen>
|
||||
<dimen name="qc_toggle_rotary_shadow_padding">8dp</dimen>
|
||||
|
||||
|
||||
<dimen name="qc_seekbar_padding_top">16dp</dimen>
|
||||
</resources>
|
||||
38
car-qc-lib/res/values/styles.xml
Normal file
38
car-qc-lib/res/values/styles.xml
Normal file
@@ -0,0 +1,38 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<style name="TextAppearance.QC" parent="android:TextAppearance.DeviceDefault">
|
||||
<item name="android:textColor">@color/car_on_surface</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.QC.Title" parent="android:TextAppearance.DeviceDefault.Large">
|
||||
<item name="android:textColor">@color/car_on_surface</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.QC.Subtitle" parent="android:TextAppearance.DeviceDefault.Small">
|
||||
<item name="android:textColor">@color/car_on_surface_variant</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.QC" parent="android:Widget.DeviceDefault"/>
|
||||
|
||||
<style name="Widget.QC.SeekBar">
|
||||
<item name="android:background">@null</item>
|
||||
<item name="android:clickable">false</item>
|
||||
<item name="android:focusable">false</item>
|
||||
<item name="android:splitTrack">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
266
car-qc-lib/src/com/android/car/qc/QCActionItem.java
Normal file
266
car-qc-lib/src/com/android/car/qc/QCActionItem.java
Normal file
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
* 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.car.qc;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
/**
|
||||
* Quick Control Action that are includes as either start or end actions in {@link QCRow}
|
||||
*/
|
||||
public class QCActionItem extends QCItem {
|
||||
private final boolean mIsChecked;
|
||||
private final boolean mIsAvailable;
|
||||
private final boolean mIsClickable;
|
||||
private Icon mIcon;
|
||||
private PendingIntent mAction;
|
||||
private PendingIntent mDisabledClickAction;
|
||||
private String mContentDescription;
|
||||
|
||||
public QCActionItem(@NonNull @QCItemType String type, boolean isChecked, boolean isEnabled,
|
||||
boolean isAvailable, boolean isClickable, boolean isClickableWhileDisabled,
|
||||
@Nullable Icon icon, @Nullable String contentDescription,
|
||||
@Nullable PendingIntent action, @Nullable PendingIntent disabledClickAction) {
|
||||
super(type, isEnabled, isClickableWhileDisabled);
|
||||
mIsChecked = isChecked;
|
||||
mIsAvailable = isAvailable;
|
||||
mIsClickable = isClickable;
|
||||
mIcon = icon;
|
||||
mContentDescription = contentDescription;
|
||||
mAction = action;
|
||||
mDisabledClickAction = disabledClickAction;
|
||||
}
|
||||
|
||||
public QCActionItem(@NonNull Parcel in) {
|
||||
super(in);
|
||||
mIsChecked = in.readBoolean();
|
||||
mIsAvailable = in.readBoolean();
|
||||
mIsClickable = in.readBoolean();
|
||||
boolean hasIcon = in.readBoolean();
|
||||
if (hasIcon) {
|
||||
mIcon = Icon.CREATOR.createFromParcel(in);
|
||||
}
|
||||
boolean hasContentDescription = in.readBoolean();
|
||||
if (hasContentDescription) {
|
||||
mContentDescription = in.readString();
|
||||
}
|
||||
boolean hasAction = in.readBoolean();
|
||||
if (hasAction) {
|
||||
mAction = PendingIntent.CREATOR.createFromParcel(in);
|
||||
}
|
||||
boolean hasDisabledClickAction = in.readBoolean();
|
||||
if (hasDisabledClickAction) {
|
||||
mDisabledClickAction = PendingIntent.CREATOR.createFromParcel(in);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
super.writeToParcel(dest, flags);
|
||||
dest.writeBoolean(mIsChecked);
|
||||
dest.writeBoolean(mIsAvailable);
|
||||
dest.writeBoolean(mIsClickable);
|
||||
boolean includeIcon = getType().equals(QC_TYPE_ACTION_TOGGLE) && mIcon != null;
|
||||
dest.writeBoolean(includeIcon);
|
||||
if (includeIcon) {
|
||||
mIcon.writeToParcel(dest, flags);
|
||||
}
|
||||
boolean hasContentDescription = mContentDescription != null;
|
||||
dest.writeBoolean(hasContentDescription);
|
||||
if (hasContentDescription) {
|
||||
dest.writeString(mContentDescription);
|
||||
}
|
||||
boolean hasAction = mAction != null;
|
||||
dest.writeBoolean(hasAction);
|
||||
if (hasAction) {
|
||||
mAction.writeToParcel(dest, flags);
|
||||
}
|
||||
boolean hasDisabledClickAction = mDisabledClickAction != null;
|
||||
dest.writeBoolean(hasDisabledClickAction);
|
||||
if (hasDisabledClickAction) {
|
||||
mDisabledClickAction.writeToParcel(dest, flags);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent getPrimaryAction() {
|
||||
return mAction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent getDisabledClickAction() {
|
||||
return mDisabledClickAction;
|
||||
}
|
||||
|
||||
public boolean isChecked() {
|
||||
return mIsChecked;
|
||||
}
|
||||
|
||||
public boolean isAvailable() {
|
||||
return mIsAvailable;
|
||||
}
|
||||
|
||||
public boolean isClickable() {
|
||||
return mIsClickable;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Icon getIcon() {
|
||||
return mIcon;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getContentDescription() {
|
||||
return mContentDescription;
|
||||
}
|
||||
|
||||
public static Creator<QCActionItem> CREATOR = new Creator<QCActionItem>() {
|
||||
@Override
|
||||
public QCActionItem createFromParcel(Parcel source) {
|
||||
return new QCActionItem(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public QCActionItem[] newArray(int size) {
|
||||
return new QCActionItem[size];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Builder for {@link QCActionItem}.
|
||||
*/
|
||||
public static class Builder {
|
||||
private final String mType;
|
||||
private boolean mIsChecked;
|
||||
private boolean mIsEnabled = true;
|
||||
private boolean mIsAvailable = true;
|
||||
private boolean mIsClickable = true;
|
||||
private boolean mIsClickableWhileDisabled = false;
|
||||
private Icon mIcon;
|
||||
private PendingIntent mAction;
|
||||
private PendingIntent mDisabledClickAction;
|
||||
private String mContentDescription;
|
||||
|
||||
public Builder(@NonNull @QCItemType String type) {
|
||||
if (!isValidType(type)) {
|
||||
throw new IllegalArgumentException("Invalid QCActionItem type provided" + type);
|
||||
}
|
||||
mType = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not the action item should be checked.
|
||||
*/
|
||||
public Builder setChecked(boolean checked) {
|
||||
mIsChecked = checked;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not the action item should be enabled.
|
||||
*/
|
||||
public Builder setEnabled(boolean enabled) {
|
||||
mIsEnabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not the action item is available.
|
||||
*/
|
||||
public Builder setAvailable(boolean available) {
|
||||
mIsAvailable = available;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the action is clickable. This differs from available in that the style will
|
||||
* remain as if it's enabled/available but click actions will not be processed.
|
||||
*/
|
||||
public Builder setClickable(boolean clickable) {
|
||||
mIsClickable = clickable;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not an action item should be clickable while disabled.
|
||||
*/
|
||||
public Builder setClickableWhileDisabled(boolean clickable) {
|
||||
mIsClickableWhileDisabled = clickable;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the icon for {@link QC_TYPE_ACTION_TOGGLE} actions
|
||||
*/
|
||||
public Builder setIcon(@Nullable Icon icon) {
|
||||
mIcon = icon;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the content description
|
||||
*/
|
||||
public Builder setContentDescription(@Nullable String contentDescription) {
|
||||
mContentDescription = contentDescription;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the string resource to use for content description
|
||||
*/
|
||||
public Builder setContentDescription(@NonNull Context context,
|
||||
@StringRes int contentDescriptionResId) {
|
||||
mContentDescription = context.getString(contentDescriptionResId);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the PendingIntent to be sent when the action item is clicked.
|
||||
*/
|
||||
public Builder setAction(@Nullable PendingIntent action) {
|
||||
mAction = action;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the PendingIntent to be sent when the action item is clicked while disabled.
|
||||
*/
|
||||
public Builder setDisabledClickAction(@Nullable PendingIntent action) {
|
||||
mDisabledClickAction = action;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the final {@link QCActionItem}.
|
||||
*/
|
||||
public QCActionItem build() {
|
||||
return new QCActionItem(mType, mIsChecked, mIsEnabled, mIsAvailable, mIsClickable,
|
||||
mIsClickableWhileDisabled, mIcon, mContentDescription, mAction,
|
||||
mDisabledClickAction);
|
||||
}
|
||||
|
||||
private boolean isValidType(String type) {
|
||||
return type.equals(QC_TYPE_ACTION_SWITCH) || type.equals(QC_TYPE_ACTION_TOGGLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
154
car-qc-lib/src/com/android/car/qc/QCItem.java
Normal file
154
car-qc-lib/src/com/android/car/qc/QCItem.java
Normal file
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* 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.car.qc;
|
||||
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringDef;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* Base class for all quick controls elements.
|
||||
*/
|
||||
public abstract class QCItem implements Parcelable {
|
||||
public static final String QC_TYPE_LIST = "QC_TYPE_LIST";
|
||||
public static final String QC_TYPE_ROW = "QC_TYPE_ROW";
|
||||
public static final String QC_TYPE_TILE = "QC_TYPE_TILE";
|
||||
public static final String QC_TYPE_SLIDER = "QC_TYPE_SLIDER";
|
||||
public static final String QC_TYPE_ACTION_SWITCH = "QC_TYPE_ACTION_SWITCH";
|
||||
public static final String QC_TYPE_ACTION_TOGGLE = "QC_TYPE_ACTION_TOGGLE";
|
||||
|
||||
public static final String QC_ACTION_TOGGLE_STATE = "QC_ACTION_TOGGLE_STATE";
|
||||
public static final String QC_ACTION_SLIDER_VALUE = "QC_ACTION_SLIDER_VALUE";
|
||||
|
||||
@StringDef(value = {
|
||||
QC_TYPE_LIST,
|
||||
QC_TYPE_ROW,
|
||||
QC_TYPE_TILE,
|
||||
QC_TYPE_SLIDER,
|
||||
QC_TYPE_ACTION_SWITCH,
|
||||
QC_TYPE_ACTION_TOGGLE,
|
||||
})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface QCItemType {
|
||||
}
|
||||
|
||||
private final String mType;
|
||||
private final boolean mIsEnabled;
|
||||
private final boolean mIsClickableWhileDisabled;
|
||||
private ActionHandler mActionHandler;
|
||||
private ActionHandler mDisabledClickActionHandler;
|
||||
|
||||
public QCItem(@NonNull @QCItemType String type) {
|
||||
this(type, /* isEnabled= */true, /* isClickableWhileDisabled= */ false);
|
||||
}
|
||||
|
||||
public QCItem(@NonNull @QCItemType String type, boolean isEnabled,
|
||||
boolean isClickableWhileDisabled) {
|
||||
mType = type;
|
||||
mIsEnabled = isEnabled;
|
||||
mIsClickableWhileDisabled = isClickableWhileDisabled;
|
||||
}
|
||||
|
||||
public QCItem(@NonNull Parcel in) {
|
||||
mType = in.readString();
|
||||
mIsEnabled = in.readBoolean();
|
||||
mIsClickableWhileDisabled = in.readBoolean();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@QCItemType
|
||||
public String getType() {
|
||||
return mType;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return mIsEnabled;
|
||||
}
|
||||
|
||||
public boolean isClickableWhileDisabled() {
|
||||
return mIsClickableWhileDisabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(mType);
|
||||
dest.writeBoolean(mIsEnabled);
|
||||
dest.writeBoolean(mIsClickableWhileDisabled);
|
||||
}
|
||||
|
||||
public void setActionHandler(@Nullable ActionHandler handler) {
|
||||
mActionHandler = handler;
|
||||
}
|
||||
|
||||
public void setDisabledClickActionHandler(@Nullable ActionHandler handler) {
|
||||
mDisabledClickActionHandler = handler;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ActionHandler getActionHandler() {
|
||||
return mActionHandler;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ActionHandler getDisabledClickActionHandler() {
|
||||
return mDisabledClickActionHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the PendingIntent that is sent when the item is clicked.
|
||||
*/
|
||||
@Nullable
|
||||
public abstract PendingIntent getPrimaryAction();
|
||||
|
||||
/**
|
||||
* Returns the PendingIntent that is sent when the item is clicked while disabled.
|
||||
*/
|
||||
@Nullable
|
||||
public abstract PendingIntent getDisabledClickAction();
|
||||
|
||||
/**
|
||||
* Action handler that can listen for an action to occur and notify listeners.
|
||||
*/
|
||||
public interface ActionHandler {
|
||||
/**
|
||||
* Callback when an action occurs.
|
||||
* @param item the QCItem that sent the action
|
||||
* @param context the context for the action
|
||||
* @param intent the intent that was sent with the action
|
||||
*/
|
||||
void onAction(@NonNull QCItem item, @NonNull Context context, @NonNull Intent intent);
|
||||
|
||||
default boolean isActivity() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
106
car-qc-lib/src/com/android/car/qc/QCList.java
Normal file
106
car-qc-lib/src/com/android/car/qc/QCList.java
Normal file
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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.car.qc;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Wrapping quick controls element that contains QCRow elements.
|
||||
*/
|
||||
public class QCList extends QCItem {
|
||||
private final List<QCRow> mRows;
|
||||
|
||||
public QCList(@NonNull List<QCRow> rows) {
|
||||
super(QC_TYPE_LIST);
|
||||
mRows = Collections.unmodifiableList(rows);
|
||||
}
|
||||
|
||||
public QCList(@NonNull Parcel in) {
|
||||
super(in);
|
||||
int rowCount = in.readInt();
|
||||
List<QCRow> rows = new ArrayList<>();
|
||||
for (int i = 0; i < rowCount; i++) {
|
||||
rows.add(QCRow.CREATOR.createFromParcel(in));
|
||||
}
|
||||
mRows = Collections.unmodifiableList(rows);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
super.writeToParcel(dest, flags);
|
||||
dest.writeInt(mRows.size());
|
||||
for (QCRow row : mRows) {
|
||||
row.writeToParcel(dest, flags);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent getPrimaryAction() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent getDisabledClickAction() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<QCRow> getRows() {
|
||||
return mRows;
|
||||
}
|
||||
|
||||
public static Creator<QCList> CREATOR = new Creator<QCList>() {
|
||||
@Override
|
||||
public QCList createFromParcel(Parcel source) {
|
||||
return new QCList(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public QCList[] newArray(int size) {
|
||||
return new QCList[size];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Builder for {@link QCList}.
|
||||
*/
|
||||
public static class Builder {
|
||||
private final List<QCRow> mRows = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Adds a {@link QCRow} to the list.
|
||||
*/
|
||||
public Builder addRow(@NonNull QCRow row) {
|
||||
mRows.add(row);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the final {@link QCList}.
|
||||
*/
|
||||
public QCList build() {
|
||||
return new QCList(mRows);
|
||||
}
|
||||
}
|
||||
}
|
||||
315
car-qc-lib/src/com/android/car/qc/QCRow.java
Normal file
315
car-qc-lib/src/com/android/car/qc/QCRow.java
Normal file
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
* 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.car.qc;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Quick Control Row Element
|
||||
* ------------------------------------
|
||||
* | | Title | |
|
||||
* | StartItems | Subtitle | EndItems |
|
||||
* | | Sliders | |
|
||||
* ------------------------------------
|
||||
*/
|
||||
public class QCRow extends QCItem {
|
||||
private final String mTitle;
|
||||
private final String mSubtitle;
|
||||
private final Icon mStartIcon;
|
||||
private final boolean mIsStartIconTintable;
|
||||
private final QCSlider mSlider;
|
||||
private final List<QCActionItem> mStartItems;
|
||||
private final List<QCActionItem> mEndItems;
|
||||
private final PendingIntent mPrimaryAction;
|
||||
private PendingIntent mDisabledClickAction;
|
||||
|
||||
public QCRow(@Nullable String title, @Nullable String subtitle, boolean isEnabled,
|
||||
boolean isClickableWhileDisabled, @Nullable PendingIntent primaryAction,
|
||||
@Nullable PendingIntent disabledClickAction, @Nullable Icon startIcon,
|
||||
boolean isIconTintable, @Nullable QCSlider slider,
|
||||
@NonNull List<QCActionItem> startItems, @NonNull List<QCActionItem> endItems) {
|
||||
super(QC_TYPE_ROW, isEnabled, isClickableWhileDisabled);
|
||||
mTitle = title;
|
||||
mSubtitle = subtitle;
|
||||
mPrimaryAction = primaryAction;
|
||||
mDisabledClickAction = disabledClickAction;
|
||||
mStartIcon = startIcon;
|
||||
mIsStartIconTintable = isIconTintable;
|
||||
mSlider = slider;
|
||||
mStartItems = Collections.unmodifiableList(startItems);
|
||||
mEndItems = Collections.unmodifiableList(endItems);
|
||||
}
|
||||
|
||||
public QCRow(@NonNull Parcel in) {
|
||||
super(in);
|
||||
mTitle = in.readString();
|
||||
mSubtitle = in.readString();
|
||||
boolean hasIcon = in.readBoolean();
|
||||
if (hasIcon) {
|
||||
mStartIcon = Icon.CREATOR.createFromParcel(in);
|
||||
} else {
|
||||
mStartIcon = null;
|
||||
}
|
||||
mIsStartIconTintable = in.readBoolean();
|
||||
boolean hasSlider = in.readBoolean();
|
||||
if (hasSlider) {
|
||||
mSlider = QCSlider.CREATOR.createFromParcel(in);
|
||||
} else {
|
||||
mSlider = null;
|
||||
}
|
||||
List<QCActionItem> startItems = new ArrayList<>();
|
||||
int startItemCount = in.readInt();
|
||||
for (int i = 0; i < startItemCount; i++) {
|
||||
startItems.add(QCActionItem.CREATOR.createFromParcel(in));
|
||||
}
|
||||
mStartItems = Collections.unmodifiableList(startItems);
|
||||
List<QCActionItem> endItems = new ArrayList<>();
|
||||
int endItemCount = in.readInt();
|
||||
for (int i = 0; i < endItemCount; i++) {
|
||||
endItems.add(QCActionItem.CREATOR.createFromParcel(in));
|
||||
}
|
||||
mEndItems = Collections.unmodifiableList(endItems);
|
||||
boolean hasPrimaryAction = in.readBoolean();
|
||||
if (hasPrimaryAction) {
|
||||
mPrimaryAction = PendingIntent.CREATOR.createFromParcel(in);
|
||||
} else {
|
||||
mPrimaryAction = null;
|
||||
}
|
||||
boolean hasDisabledClickAction = in.readBoolean();
|
||||
if (hasDisabledClickAction) {
|
||||
mDisabledClickAction = PendingIntent.CREATOR.createFromParcel(in);
|
||||
} else {
|
||||
mDisabledClickAction = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
super.writeToParcel(dest, flags);
|
||||
dest.writeString(mTitle);
|
||||
dest.writeString(mSubtitle);
|
||||
boolean hasStartIcon = mStartIcon != null;
|
||||
dest.writeBoolean(hasStartIcon);
|
||||
if (hasStartIcon) {
|
||||
mStartIcon.writeToParcel(dest, flags);
|
||||
}
|
||||
dest.writeBoolean(mIsStartIconTintable);
|
||||
boolean hasSlider = mSlider != null;
|
||||
dest.writeBoolean(hasSlider);
|
||||
if (hasSlider) {
|
||||
mSlider.writeToParcel(dest, flags);
|
||||
}
|
||||
dest.writeInt(mStartItems.size());
|
||||
for (QCActionItem startItem : mStartItems) {
|
||||
startItem.writeToParcel(dest, flags);
|
||||
}
|
||||
dest.writeInt(mEndItems.size());
|
||||
for (QCActionItem endItem : mEndItems) {
|
||||
endItem.writeToParcel(dest, flags);
|
||||
}
|
||||
boolean hasPrimaryAction = mPrimaryAction != null;
|
||||
dest.writeBoolean(hasPrimaryAction);
|
||||
if (hasPrimaryAction) {
|
||||
mPrimaryAction.writeToParcel(dest, flags);
|
||||
}
|
||||
boolean hasDisabledClickAction = mDisabledClickAction != null;
|
||||
dest.writeBoolean(hasDisabledClickAction);
|
||||
if (hasDisabledClickAction) {
|
||||
mDisabledClickAction.writeToParcel(dest, flags);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent getPrimaryAction() {
|
||||
return mPrimaryAction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent getDisabledClickAction() {
|
||||
return mDisabledClickAction;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getTitle() {
|
||||
return mTitle;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getSubtitle() {
|
||||
return mSubtitle;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Icon getStartIcon() {
|
||||
return mStartIcon;
|
||||
}
|
||||
|
||||
public boolean isStartIconTintable() {
|
||||
return mIsStartIconTintable;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public QCSlider getSlider() {
|
||||
return mSlider;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<QCActionItem> getStartItems() {
|
||||
return mStartItems;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<QCActionItem> getEndItems() {
|
||||
return mEndItems;
|
||||
}
|
||||
|
||||
public static Creator<QCRow> CREATOR = new Creator<QCRow>() {
|
||||
@Override
|
||||
public QCRow createFromParcel(Parcel source) {
|
||||
return new QCRow(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public QCRow[] newArray(int size) {
|
||||
return new QCRow[size];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Builder for {@link QCRow}.
|
||||
*/
|
||||
public static class Builder {
|
||||
private final List<QCActionItem> mStartItems = new ArrayList<>();
|
||||
private final List<QCActionItem> mEndItems = new ArrayList<>();
|
||||
private Icon mStartIcon;
|
||||
private boolean mIsStartIconTintable = true;
|
||||
private String mTitle;
|
||||
private String mSubtitle;
|
||||
private boolean mIsEnabled = true;
|
||||
private boolean mIsClickableWhileDisabled = false;
|
||||
private QCSlider mSlider;
|
||||
private PendingIntent mPrimaryAction;
|
||||
private PendingIntent mDisabledClickAction;
|
||||
|
||||
/**
|
||||
* Sets the row title.
|
||||
*/
|
||||
public Builder setTitle(@Nullable String title) {
|
||||
mTitle = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the row subtitle.
|
||||
*/
|
||||
public Builder setSubtitle(@Nullable String subtitle) {
|
||||
mSubtitle = subtitle;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not the row is enabled. Note that this only affects the main row area,
|
||||
* not the action items contained within the row.
|
||||
*/
|
||||
public Builder setEnabled(boolean enabled) {
|
||||
mIsEnabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not the row should be clickable while disabled.
|
||||
*/
|
||||
public Builder setClickableWhileDisabled(boolean clickable) {
|
||||
mIsClickableWhileDisabled = clickable;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the row icon.
|
||||
*/
|
||||
public Builder setIcon(@Nullable Icon icon) {
|
||||
mStartIcon = icon;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not the row icon is tintable.
|
||||
*/
|
||||
public Builder setIconTintable(boolean tintable) {
|
||||
mIsStartIconTintable = tintable;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a {@link QCSlider} to the slider area.
|
||||
*/
|
||||
public Builder addSlider(@Nullable QCSlider slider) {
|
||||
mSlider = slider;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the PendingIntent to be sent when the row is clicked.
|
||||
*/
|
||||
public Builder setPrimaryAction(@Nullable PendingIntent action) {
|
||||
mPrimaryAction = action;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the PendingIntent to be sent when the action item is clicked while disabled.
|
||||
*/
|
||||
public Builder setDisabledClickAction(@Nullable PendingIntent action) {
|
||||
mDisabledClickAction = action;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a {@link QCActionItem} to the start items area.
|
||||
*/
|
||||
public Builder addStartItem(@NonNull QCActionItem item) {
|
||||
mStartItems.add(item);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a {@link QCActionItem} to the end items area.
|
||||
*/
|
||||
public Builder addEndItem(@NonNull QCActionItem item) {
|
||||
mEndItems.add(item);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the final {@link QCRow}.
|
||||
*/
|
||||
public QCRow build() {
|
||||
return new QCRow(mTitle, mSubtitle, mIsEnabled, mIsClickableWhileDisabled,
|
||||
mPrimaryAction, mDisabledClickAction, mStartIcon, mIsStartIconTintable,
|
||||
mSlider, mStartItems, mEndItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
189
car-qc-lib/src/com/android/car/qc/QCSlider.java
Normal file
189
car-qc-lib/src/com/android/car/qc/QCSlider.java
Normal file
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* 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.car.qc;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Quick Control Slider included in {@link QCRow}
|
||||
*/
|
||||
public class QCSlider extends QCItem {
|
||||
private int mMin = 0;
|
||||
private int mMax = 100;
|
||||
private int mValue = 0;
|
||||
private PendingIntent mInputAction;
|
||||
private PendingIntent mDisabledClickAction;
|
||||
|
||||
public QCSlider(int min, int max, int value, boolean enabled, boolean clickableWhileDisabled,
|
||||
@Nullable PendingIntent inputAction, @Nullable PendingIntent disabledClickAction) {
|
||||
super(QC_TYPE_SLIDER, enabled, clickableWhileDisabled);
|
||||
mMin = min;
|
||||
mMax = max;
|
||||
mValue = value;
|
||||
mInputAction = inputAction;
|
||||
mDisabledClickAction = disabledClickAction;
|
||||
}
|
||||
|
||||
public QCSlider(@NonNull Parcel in) {
|
||||
super(in);
|
||||
mMin = in.readInt();
|
||||
mMax = in.readInt();
|
||||
mValue = in.readInt();
|
||||
boolean hasAction = in.readBoolean();
|
||||
if (hasAction) {
|
||||
mInputAction = PendingIntent.CREATOR.createFromParcel(in);
|
||||
}
|
||||
boolean hasDisabledClickAction = in.readBoolean();
|
||||
if (hasDisabledClickAction) {
|
||||
mDisabledClickAction = PendingIntent.CREATOR.createFromParcel(in);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
super.writeToParcel(dest, flags);
|
||||
dest.writeInt(mMin);
|
||||
dest.writeInt(mMax);
|
||||
dest.writeInt(mValue);
|
||||
boolean hasAction = mInputAction != null;
|
||||
dest.writeBoolean(hasAction);
|
||||
if (hasAction) {
|
||||
mInputAction.writeToParcel(dest, flags);
|
||||
}
|
||||
boolean hasDisabledClickAction = mDisabledClickAction != null;
|
||||
dest.writeBoolean(hasDisabledClickAction);
|
||||
if (hasDisabledClickAction) {
|
||||
mDisabledClickAction.writeToParcel(dest, flags);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent getPrimaryAction() {
|
||||
return mInputAction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent getDisabledClickAction() {
|
||||
return mDisabledClickAction;
|
||||
}
|
||||
|
||||
public int getMin() {
|
||||
return mMin;
|
||||
}
|
||||
|
||||
public int getMax() {
|
||||
return mMax;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return mValue;
|
||||
}
|
||||
|
||||
public static Creator<QCSlider> CREATOR = new Creator<QCSlider>() {
|
||||
@Override
|
||||
public QCSlider createFromParcel(Parcel source) {
|
||||
return new QCSlider(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public QCSlider[] newArray(int size) {
|
||||
return new QCSlider[size];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Builder for {@link QCSlider}.
|
||||
*/
|
||||
public static class Builder {
|
||||
private int mMin = 0;
|
||||
private int mMax = 100;
|
||||
private int mValue = 0;
|
||||
private boolean mIsEnabled = true;
|
||||
private boolean mIsClickableWhileDisabled = false;
|
||||
private PendingIntent mInputAction;
|
||||
private PendingIntent mDisabledClickAction;
|
||||
|
||||
/**
|
||||
* Set the minimum allowed value for the slider input.
|
||||
*/
|
||||
public Builder setMin(int min) {
|
||||
mMin = min;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum allowed value for the slider input.
|
||||
*/
|
||||
public Builder setMax(int max) {
|
||||
mMax = max;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current value for the slider input.
|
||||
*/
|
||||
public Builder setValue(int value) {
|
||||
mValue = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not the slider is enabled.
|
||||
*/
|
||||
public Builder setEnabled(boolean enabled) {
|
||||
mIsEnabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not a slider should be clickable while disabled.
|
||||
*/
|
||||
public Builder setClickableWhileDisabled(boolean clickable) {
|
||||
mIsClickableWhileDisabled = clickable;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the PendingIntent to be sent when the slider value is changed.
|
||||
*/
|
||||
public Builder setInputAction(@Nullable PendingIntent inputAction) {
|
||||
mInputAction = inputAction;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the PendingIntent to be sent when the action item is clicked while disabled.
|
||||
*/
|
||||
public Builder setDisabledClickAction(@Nullable PendingIntent action) {
|
||||
mDisabledClickAction = action;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the final {@link QCSlider}.
|
||||
*/
|
||||
public QCSlider build() {
|
||||
return new QCSlider(mMin, mMax, mValue, mIsEnabled, mIsClickableWhileDisabled,
|
||||
mInputAction, mDisabledClickAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
222
car-qc-lib/src/com/android/car/qc/QCTile.java
Normal file
222
car-qc-lib/src/com/android/car/qc/QCTile.java
Normal file
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
* 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.car.qc;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Quick Control Tile Element
|
||||
* ------------
|
||||
* | -------- |
|
||||
* | | Icon | |
|
||||
* | -------- |
|
||||
* | Subtitle |
|
||||
* ------------
|
||||
*/
|
||||
public class QCTile extends QCItem {
|
||||
private final boolean mIsChecked;
|
||||
private final boolean mIsAvailable;
|
||||
private final String mSubtitle;
|
||||
private Icon mIcon;
|
||||
private PendingIntent mAction;
|
||||
private PendingIntent mDisabledClickAction;
|
||||
|
||||
public QCTile(boolean isChecked, boolean isEnabled, boolean isAvailable,
|
||||
boolean isClickableWhileDisabled, @Nullable String subtitle, @Nullable Icon icon,
|
||||
@Nullable PendingIntent action, @Nullable PendingIntent disabledClickAction) {
|
||||
super(QC_TYPE_TILE, isEnabled, isClickableWhileDisabled);
|
||||
mIsChecked = isChecked;
|
||||
mIsAvailable = isAvailable;
|
||||
mSubtitle = subtitle;
|
||||
mIcon = icon;
|
||||
mAction = action;
|
||||
mDisabledClickAction = disabledClickAction;
|
||||
}
|
||||
|
||||
public QCTile(@NonNull Parcel in) {
|
||||
super(in);
|
||||
mIsChecked = in.readBoolean();
|
||||
mIsAvailable = in.readBoolean();
|
||||
mSubtitle = in.readString();
|
||||
boolean hasIcon = in.readBoolean();
|
||||
if (hasIcon) {
|
||||
mIcon = Icon.CREATOR.createFromParcel(in);
|
||||
}
|
||||
boolean hasAction = in.readBoolean();
|
||||
if (hasAction) {
|
||||
mAction = PendingIntent.CREATOR.createFromParcel(in);
|
||||
}
|
||||
boolean hasDisabledClickAction = in.readBoolean();
|
||||
if (hasDisabledClickAction) {
|
||||
mDisabledClickAction = PendingIntent.CREATOR.createFromParcel(in);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
super.writeToParcel(dest, flags);
|
||||
dest.writeBoolean(mIsChecked);
|
||||
dest.writeBoolean(mIsAvailable);
|
||||
dest.writeString(mSubtitle);
|
||||
boolean hasIcon = mIcon != null;
|
||||
dest.writeBoolean(hasIcon);
|
||||
if (hasIcon) {
|
||||
mIcon.writeToParcel(dest, flags);
|
||||
}
|
||||
boolean hasAction = mAction != null;
|
||||
dest.writeBoolean(hasAction);
|
||||
if (hasAction) {
|
||||
mAction.writeToParcel(dest, flags);
|
||||
}
|
||||
boolean hasDisabledClickAction = mDisabledClickAction != null;
|
||||
dest.writeBoolean(hasDisabledClickAction);
|
||||
if (hasDisabledClickAction) {
|
||||
mDisabledClickAction.writeToParcel(dest, flags);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent getPrimaryAction() {
|
||||
return mAction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent getDisabledClickAction() {
|
||||
return mDisabledClickAction;
|
||||
}
|
||||
|
||||
public boolean isChecked() {
|
||||
return mIsChecked;
|
||||
}
|
||||
|
||||
public boolean isAvailable() {
|
||||
return mIsAvailable;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getSubtitle() {
|
||||
return mSubtitle;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Icon getIcon() {
|
||||
return mIcon;
|
||||
}
|
||||
|
||||
public static Creator<QCTile> CREATOR = new Creator<QCTile>() {
|
||||
@Override
|
||||
public QCTile createFromParcel(Parcel source) {
|
||||
return new QCTile(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public QCTile[] newArray(int size) {
|
||||
return new QCTile[size];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Builder for {@link QCTile}.
|
||||
*/
|
||||
public static class Builder {
|
||||
private boolean mIsChecked;
|
||||
private boolean mIsEnabled = true;
|
||||
private boolean mIsAvailable = true;
|
||||
private boolean mIsClickableWhileDisabled = false;
|
||||
private String mSubtitle;
|
||||
private Icon mIcon;
|
||||
private PendingIntent mAction;
|
||||
private PendingIntent mDisabledClickAction;
|
||||
|
||||
/**
|
||||
* Sets whether or not the tile should be checked.
|
||||
*/
|
||||
public Builder setChecked(boolean checked) {
|
||||
mIsChecked = checked;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not the tile should be enabled.
|
||||
*/
|
||||
public Builder setEnabled(boolean enabled) {
|
||||
mIsEnabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not the action item is available.
|
||||
*/
|
||||
public Builder setAvailable(boolean available) {
|
||||
mIsAvailable = available;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not a tile should be clickable while disabled.
|
||||
*/
|
||||
public Builder setClickableWhileDisabled(boolean clickable) {
|
||||
mIsClickableWhileDisabled = clickable;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tile's subtitle.
|
||||
*/
|
||||
public Builder setSubtitle(@Nullable String subtitle) {
|
||||
mSubtitle = subtitle;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tile's icon.
|
||||
*/
|
||||
public Builder setIcon(@Nullable Icon icon) {
|
||||
mIcon = icon;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the PendingIntent to be sent when the tile is clicked.
|
||||
*/
|
||||
public Builder setAction(@Nullable PendingIntent action) {
|
||||
mAction = action;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the PendingIntent to be sent when the action item is clicked while disabled.
|
||||
*/
|
||||
public Builder setDisabledClickAction(@Nullable PendingIntent action) {
|
||||
mDisabledClickAction = action;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the final {@link QCTile}.
|
||||
*/
|
||||
public QCTile build() {
|
||||
return new QCTile(mIsChecked, mIsEnabled, mIsAvailable, mIsClickableWhileDisabled,
|
||||
mSubtitle, mIcon, mAction, mDisabledClickAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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.car.qc.controller;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import com.android.car.qc.QCItem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Base controller class for Quick Controls.
|
||||
*/
|
||||
public abstract class BaseQCController implements QCItemCallback {
|
||||
protected final Context mContext;
|
||||
protected final List<Observer<QCItem>> mObservers = new ArrayList<>();
|
||||
protected boolean mShouldListen = false;
|
||||
protected boolean mWasListening = false;
|
||||
protected QCItem mQCItem;
|
||||
|
||||
public BaseQCController(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update whether or not the controller should be listening to updates from the provider.
|
||||
*/
|
||||
public void listen(boolean shouldListen) {
|
||||
mShouldListen = shouldListen;
|
||||
updateListening();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a QCItem observer to the controller.
|
||||
*/
|
||||
@UiThread
|
||||
public void addObserver(Observer<QCItem> observer) {
|
||||
mObservers.add(observer);
|
||||
updateListening();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a QCItem observer from the controller.
|
||||
*/
|
||||
@UiThread
|
||||
public void removeObserver(Observer<QCItem> observer) {
|
||||
mObservers.remove(observer);
|
||||
updateListening();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@Override
|
||||
public void onQCItemUpdated(@Nullable QCItem item) {
|
||||
mQCItem = item;
|
||||
mObservers.forEach(o -> o.onChanged(mQCItem));
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the controller. This should be called when the controller is no longer needed so
|
||||
* the listeners can be cleaned up.
|
||||
*/
|
||||
public void destroy() {
|
||||
mShouldListen = false;
|
||||
mObservers.clear();
|
||||
updateListening();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a single retrieval from the provider (without subscribing to live updates).
|
||||
*/
|
||||
public abstract void bind();
|
||||
|
||||
/**
|
||||
* Subclasses must override this method to handle a listening update.
|
||||
*/
|
||||
protected abstract void updateListening();
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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.car.qc.controller;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.android.car.qc.provider.BaseLocalQCProvider;
|
||||
|
||||
/**
|
||||
* Controller for binding to local quick control providers.
|
||||
*/
|
||||
public class LocalQCController extends BaseQCController {
|
||||
|
||||
private final BaseLocalQCProvider mProvider;
|
||||
|
||||
private final BaseLocalQCProvider.Notifier mProviderNotifier =
|
||||
new BaseLocalQCProvider.Notifier() {
|
||||
@Override
|
||||
public void notifyUpdate() {
|
||||
if (mShouldListen && !mObservers.isEmpty()) {
|
||||
onQCItemUpdated(mProvider.getQCItem());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public LocalQCController(Context context, BaseLocalQCProvider provider) {
|
||||
super(context);
|
||||
mProvider = provider;
|
||||
mProvider.setNotifier(mProviderNotifier);
|
||||
mQCItem = mProvider.getQCItem();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind() {
|
||||
onQCItemUpdated(mProvider.getQCItem());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateListening() {
|
||||
boolean listen = mShouldListen && !mObservers.isEmpty();
|
||||
if (mWasListening != listen) {
|
||||
mWasListening = listen;
|
||||
mProvider.shouldListen(listen);
|
||||
if (listen) {
|
||||
mQCItem = mProvider.getQCItem();
|
||||
onQCItemUpdated(mQCItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
super.destroy();
|
||||
mProvider.onDestroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.car.qc.controller;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.car.qc.QCItem;
|
||||
|
||||
/**
|
||||
* Callback to be executed when a QCItem changes.
|
||||
*/
|
||||
public interface QCItemCallback {
|
||||
/**
|
||||
* Called when QCItem is updated.
|
||||
*
|
||||
* @param item The updated QCItem.
|
||||
*/
|
||||
void onQCItemUpdated(@Nullable QCItem item);
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
* 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.car.qc.controller;
|
||||
|
||||
import static com.android.car.qc.provider.BaseQCProvider.EXTRA_ITEM;
|
||||
import static com.android.car.qc.provider.BaseQCProvider.EXTRA_URI;
|
||||
import static com.android.car.qc.provider.BaseQCProvider.METHOD_BIND;
|
||||
import static com.android.car.qc.provider.BaseQCProvider.METHOD_DESTROY;
|
||||
import static com.android.car.qc.provider.BaseQCProvider.METHOD_SUBSCRIBE;
|
||||
import static com.android.car.qc.provider.BaseQCProvider.METHOD_UNSUBSCRIBE;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.Context;
|
||||
import android.database.ContentObserver;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerExecutor;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import android.os.Parcelable;
|
||||
import android.os.RemoteException;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.android.car.qc.QCItem;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Controller for binding to remote quick control providers.
|
||||
*/
|
||||
public class RemoteQCController extends BaseQCController {
|
||||
private static final String TAG = "RemoteQCController";
|
||||
private static final long PROVIDER_ANR_TIMEOUT = 3000L;
|
||||
|
||||
private final Uri mUri;
|
||||
private final Executor mBackgroundExecutor;
|
||||
private final HandlerThread mBackgroundHandlerThread;
|
||||
private final ArrayMap<Pair<Uri, QCItemCallback>, QCObserver> mObserverLookup =
|
||||
new ArrayMap<>();
|
||||
|
||||
public RemoteQCController(Context context, Uri uri) {
|
||||
super(context);
|
||||
mUri = uri;
|
||||
mBackgroundHandlerThread = new HandlerThread(/* name= */ TAG + "HandlerThread");
|
||||
mBackgroundHandlerThread.start();
|
||||
mBackgroundExecutor = new HandlerExecutor(
|
||||
new Handler(mBackgroundHandlerThread.getLooper()));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
RemoteQCController(Context context, Uri uri, Executor backgroundExecutor) {
|
||||
super(context);
|
||||
mUri = uri;
|
||||
mBackgroundHandlerThread = null;
|
||||
mBackgroundExecutor = backgroundExecutor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind() {
|
||||
mBackgroundExecutor.execute(this::updateQCItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateListening() {
|
||||
boolean listen = mShouldListen && !mObservers.isEmpty();
|
||||
mBackgroundExecutor.execute(() -> updateListeningBg(listen));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
super.destroy();
|
||||
if (mBackgroundHandlerThread != null) {
|
||||
mBackgroundHandlerThread.quit();
|
||||
}
|
||||
try (ContentProviderClient client = getClient()) {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
Bundle b = new Bundle();
|
||||
b.putParcelable(EXTRA_URI, mUri);
|
||||
try {
|
||||
client.call(METHOD_DESTROY, /* arg= */ null, b);
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "Error destroying QCItem", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void updateListeningBg(boolean isListening) {
|
||||
if (mWasListening != isListening) {
|
||||
mWasListening = isListening;
|
||||
if (isListening) {
|
||||
registerQCCallback(mContext.getMainExecutor(), /* callback= */ this);
|
||||
// Update one-time on a different thread so that it can display in parallel
|
||||
mBackgroundExecutor.execute(this::updateQCItem);
|
||||
} else {
|
||||
unregisterQCCallback(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void updateQCItem() {
|
||||
try {
|
||||
QCItem item = getQCItem();
|
||||
mContext.getMainExecutor().execute(() -> onQCItemUpdated(item));
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "Error fetching QCItem", e);
|
||||
}
|
||||
}
|
||||
|
||||
private QCItem getQCItem() {
|
||||
try (ContentProviderClient provider = getClient()) {
|
||||
if (provider == null) {
|
||||
return null;
|
||||
}
|
||||
Bundle extras = new Bundle();
|
||||
extras.putParcelable(EXTRA_URI, mUri);
|
||||
Bundle res = provider.call(METHOD_BIND, /* arg= */ null, extras);
|
||||
if (res == null) {
|
||||
return null;
|
||||
}
|
||||
res.setDefusable(true);
|
||||
res.setClassLoader(QCItem.class.getClassLoader());
|
||||
Parcelable parcelable = res.getParcelable(EXTRA_ITEM);
|
||||
if (parcelable instanceof QCItem) {
|
||||
return (QCItem) parcelable;
|
||||
}
|
||||
return null;
|
||||
} catch (RemoteException e) {
|
||||
Log.d(TAG, "Error binding QCItem", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void subscribe() {
|
||||
try (ContentProviderClient client = getClient()) {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
Bundle b = new Bundle();
|
||||
b.putParcelable(EXTRA_URI, mUri);
|
||||
try {
|
||||
client.call(METHOD_SUBSCRIBE, /* arg= */ null, b);
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "Error subscribing to QCItem", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void unsubscribe() {
|
||||
try (ContentProviderClient client = getClient()) {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
Bundle b = new Bundle();
|
||||
b.putParcelable(EXTRA_URI, mUri);
|
||||
try {
|
||||
client.call(METHOD_UNSUBSCRIBE, /* arg= */ null, b);
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "Error unsubscribing from QCItem", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
ContentProviderClient getClient() {
|
||||
ContentProviderClient client = mContext.getContentResolver()
|
||||
.acquireContentProviderClient(mUri);
|
||||
if (client == null) {
|
||||
return null;
|
||||
}
|
||||
client.setDetectNotResponding(PROVIDER_ANR_TIMEOUT);
|
||||
return client;
|
||||
}
|
||||
|
||||
private void registerQCCallback(@NonNull Executor executor, @NonNull QCItemCallback callback) {
|
||||
getObserver(callback, new QCObserver(mUri, executor, callback)).startObserving();
|
||||
}
|
||||
|
||||
private void unregisterQCCallback(@NonNull QCItemCallback callback) {
|
||||
synchronized (mObserverLookup) {
|
||||
QCObserver observer = mObserverLookup.remove(new Pair<>(mUri, callback));
|
||||
if (observer != null) {
|
||||
observer.stopObserving();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private QCObserver getObserver(QCItemCallback callback, QCObserver observer) {
|
||||
Pair<Uri, QCItemCallback> key = new Pair<>(mUri, callback);
|
||||
synchronized (mObserverLookup) {
|
||||
QCObserver oldObserver = mObserverLookup.put(key, observer);
|
||||
if (oldObserver != null) {
|
||||
oldObserver.stopObserving();
|
||||
}
|
||||
}
|
||||
return observer;
|
||||
}
|
||||
|
||||
private class QCObserver {
|
||||
private final Uri mUri;
|
||||
private final Executor mExecutor;
|
||||
private final QCItemCallback mCallback;
|
||||
private boolean mIsSubscribed;
|
||||
|
||||
private final Runnable mUpdateItem = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
trySubscribe();
|
||||
QCItem item = getQCItem();
|
||||
mExecutor.execute(() -> mCallback.onQCItemUpdated(item));
|
||||
}
|
||||
};
|
||||
|
||||
private final ContentObserver mObserver = new ContentObserver(
|
||||
new Handler(Looper.getMainLooper())) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
android.os.AsyncTask.execute(mUpdateItem);
|
||||
}
|
||||
};
|
||||
|
||||
QCObserver(Uri uri, Executor executor, QCItemCallback callback) {
|
||||
mUri = uri;
|
||||
mExecutor = executor;
|
||||
mCallback = callback;
|
||||
}
|
||||
|
||||
void startObserving() {
|
||||
ContentProviderClient provider =
|
||||
mContext.getContentResolver().acquireContentProviderClient(mUri);
|
||||
if (provider != null) {
|
||||
provider.close();
|
||||
mContext.getContentResolver().registerContentObserver(
|
||||
mUri, /* notifyForDescendants= */ true, mObserver);
|
||||
trySubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
void trySubscribe() {
|
||||
if (!mIsSubscribed) {
|
||||
subscribe();
|
||||
mIsSubscribed = true;
|
||||
}
|
||||
}
|
||||
|
||||
void stopObserving() {
|
||||
mContext.getContentResolver().unregisterContentObserver(mObserver);
|
||||
if (mIsSubscribed) {
|
||||
unsubscribe();
|
||||
mIsSubscribed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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.car.qc.provider;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.android.car.qc.QCItem;
|
||||
|
||||
/**
|
||||
* Base class for local Quick Control providers.
|
||||
*/
|
||||
public abstract class BaseLocalQCProvider {
|
||||
|
||||
/**
|
||||
* Callback to be executed when the QCItem updates.
|
||||
*/
|
||||
public interface Notifier {
|
||||
/**
|
||||
* Called when the QCItem has been updated.
|
||||
*/
|
||||
default void notifyUpdate() {
|
||||
}
|
||||
}
|
||||
|
||||
private Notifier mNotifier;
|
||||
private boolean mIsListening;
|
||||
protected final Context mContext;
|
||||
|
||||
public BaseLocalQCProvider(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the notifier that should be called when the QCItem updates.
|
||||
*/
|
||||
public void setNotifier(Notifier notifier) {
|
||||
mNotifier = notifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update whether or not the provider should be listening for live updates.
|
||||
*/
|
||||
public void shouldListen(boolean listen) {
|
||||
if (mIsListening == listen) {
|
||||
return;
|
||||
}
|
||||
mIsListening = listen;
|
||||
if (listen) {
|
||||
onSubscribed();
|
||||
} else {
|
||||
onUnsubscribed();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to create and return a {@link QCItem}.
|
||||
*/
|
||||
public abstract QCItem getQCItem();
|
||||
|
||||
/**
|
||||
* Called to inform the provider that it has been subscribed to.
|
||||
*/
|
||||
protected void onSubscribed() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to inform the provider that it has been unsubscribed from.
|
||||
*/
|
||||
protected void onUnsubscribed() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to inform the provider that it is being destroyed.
|
||||
*/
|
||||
public void onDestroy() {
|
||||
}
|
||||
|
||||
protected void notifyChange() {
|
||||
if (mNotifier != null) {
|
||||
mNotifier.notifyUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
235
car-qc-lib/src/com/android/car/qc/provider/BaseQCProvider.java
Normal file
235
car-qc-lib/src/com/android/car/qc/provider/BaseQCProvider.java
Normal file
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* 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.car.qc.provider;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Process;
|
||||
import android.os.StrictMode;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.car.qc.QCItem;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Base Quick Controls provider implementation.
|
||||
*/
|
||||
public abstract class BaseQCProvider extends ContentProvider {
|
||||
public static final String METHOD_BIND = "QC_METHOD_BIND";
|
||||
public static final String METHOD_SUBSCRIBE = "QC_METHOD_SUBSCRIBE";
|
||||
public static final String METHOD_UNSUBSCRIBE = "QC_METHOD_UNSUBSCRIBE";
|
||||
public static final String METHOD_DESTROY = "QC_METHOD_DESTROY";
|
||||
public static final String EXTRA_URI = "QC_EXTRA_URI";
|
||||
public static final String EXTRA_ITEM = "QC_EXTRA_ITEM";
|
||||
|
||||
private static final String TAG = "BaseQCProvider";
|
||||
private static final long QC_ANR_TIMEOUT = 3000L;
|
||||
private static final Handler MAIN_THREAD_HANDLER = new Handler(Looper.getMainLooper());
|
||||
private String mCallbackMethod;
|
||||
private final Runnable mAnr = () -> {
|
||||
Process.sendSignal(Process.myPid(), Process.SIGNAL_QUIT);
|
||||
Log.e(TAG, "Timed out while handling QC method " + mCallbackMethod);
|
||||
};
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle call(String method, String arg, Bundle extras) {
|
||||
enforceCallingPermissions();
|
||||
|
||||
Uri uri = getUriWithoutUserId(validateIncomingUriOrNull(
|
||||
extras.getParcelable(EXTRA_URI)));
|
||||
switch(method) {
|
||||
case METHOD_BIND:
|
||||
QCItem item = handleBind(uri);
|
||||
Bundle b = new Bundle();
|
||||
b.putParcelable(EXTRA_ITEM, item);
|
||||
return b;
|
||||
case METHOD_SUBSCRIBE:
|
||||
handleSubscribe(uri);
|
||||
break;
|
||||
case METHOD_UNSUBSCRIBE:
|
||||
handleUnsubscribe(uri);
|
||||
break;
|
||||
case METHOD_DESTROY:
|
||||
handleDestroy(uri);
|
||||
break;
|
||||
}
|
||||
return super.call(method, arg, extras);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
|
||||
String sortOrder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(Uri uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to create and return a {@link QCItem}.
|
||||
*
|
||||
* onBind is expected to return as quickly as possible. Therefore, no network or other IO
|
||||
* will be allowed. Any loading that needs to be done should happen in the background and
|
||||
* should then notify the content resolver of the change when ready to provide the
|
||||
* complete data in onBind.
|
||||
*/
|
||||
@Nullable
|
||||
protected QCItem onBind(@NonNull Uri uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to inform an app that an item has been subscribed to.
|
||||
*
|
||||
* Subscribing is a way that a host can notify apps of which QCItems they would like to
|
||||
* receive updates for. The providing apps are expected to keep the content up to date
|
||||
* and notify of change via the content resolver.
|
||||
*/
|
||||
protected void onSubscribed(@NonNull Uri uri) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to inform an app that an item has been unsubscribed from.
|
||||
*
|
||||
* This is used to notify providing apps that a host is no longer listening
|
||||
* to updates, so any background processes and/or listeners should be removed.
|
||||
*/
|
||||
protected void onUnsubscribed(@NonNull Uri uri) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to inform an app that an item is being destroyed.
|
||||
*
|
||||
* This is used to notify providing apps that a host is no longer going to use this QCItem
|
||||
* instance, so the relevant elements should be cleaned up.
|
||||
*/
|
||||
protected void onDestroy(@NonNull Uri uri) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Set of packages that are allowed to call this provider.
|
||||
*/
|
||||
@NonNull
|
||||
protected abstract Set<String> getAllowlistedPackages();
|
||||
|
||||
private QCItem handleBind(Uri uri) {
|
||||
mCallbackMethod = "handleBind";
|
||||
MAIN_THREAD_HANDLER.postDelayed(mAnr, QC_ANR_TIMEOUT);
|
||||
try {
|
||||
return onBindStrict(uri);
|
||||
} finally {
|
||||
MAIN_THREAD_HANDLER.removeCallbacks(mAnr);
|
||||
}
|
||||
}
|
||||
|
||||
private QCItem onBindStrict(@NonNull Uri uri) {
|
||||
StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
|
||||
try {
|
||||
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
|
||||
.detectAll()
|
||||
|
||||
// TODO(268275789): Revert back to penaltyDeath and ensure it works in
|
||||
// presubmit
|
||||
.penaltyLog()
|
||||
|
||||
.build());
|
||||
return onBind(uri);
|
||||
} finally {
|
||||
StrictMode.setThreadPolicy(oldPolicy);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSubscribe(@NonNull Uri uri) {
|
||||
mCallbackMethod = "handleSubscribe";
|
||||
MAIN_THREAD_HANDLER.postDelayed(mAnr, QC_ANR_TIMEOUT);
|
||||
try {
|
||||
onSubscribed(uri);
|
||||
} finally {
|
||||
MAIN_THREAD_HANDLER.removeCallbacks(mAnr);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleUnsubscribe(@NonNull Uri uri) {
|
||||
mCallbackMethod = "handleUnsubscribe";
|
||||
MAIN_THREAD_HANDLER.postDelayed(mAnr, QC_ANR_TIMEOUT);
|
||||
try {
|
||||
onUnsubscribed(uri);
|
||||
} finally {
|
||||
MAIN_THREAD_HANDLER.removeCallbacks(mAnr);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDestroy(@NonNull Uri uri) {
|
||||
mCallbackMethod = "handleDestroy";
|
||||
MAIN_THREAD_HANDLER.postDelayed(mAnr, QC_ANR_TIMEOUT);
|
||||
try {
|
||||
onDestroy(uri);
|
||||
} finally {
|
||||
MAIN_THREAD_HANDLER.removeCallbacks(mAnr);
|
||||
}
|
||||
}
|
||||
|
||||
private Uri validateIncomingUriOrNull(Uri uri) {
|
||||
if (uri == null) {
|
||||
throw new IllegalArgumentException("Uri cannot be null");
|
||||
}
|
||||
return validateIncomingUri(uri);
|
||||
}
|
||||
|
||||
private void enforceCallingPermissions() {
|
||||
String callingPackage = getCallingPackage();
|
||||
if (callingPackage == null) {
|
||||
throw new IllegalArgumentException("Calling package cannot be null");
|
||||
}
|
||||
if (!getAllowlistedPackages().contains(callingPackage)) {
|
||||
throw new SecurityException(
|
||||
String.format("%s is not permitted to access provider: %s", callingPackage,
|
||||
getClass().getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
101
car-qc-lib/src/com/android/car/qc/view/QCListView.java
Normal file
101
car-qc-lib/src/com/android/car/qc/view/QCListView.java
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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.car.qc.view;
|
||||
|
||||
import static com.android.car.qc.view.QCView.QCActionListener;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import com.android.car.qc.QCItem;
|
||||
import com.android.car.qc.QCList;
|
||||
|
||||
/**
|
||||
* Quick Controls view for {@link QCList} instances.
|
||||
*/
|
||||
public class QCListView extends LinearLayout implements Observer<QCItem> {
|
||||
|
||||
private QCActionListener mActionListener;
|
||||
|
||||
public QCListView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public QCListView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public QCListView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
public QCListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
setOrientation(VERTICAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the view's {@link QCActionListener}. This listener will propagate to all QCRows.
|
||||
*/
|
||||
public void setActionListener(QCActionListener listener) {
|
||||
mActionListener = listener;
|
||||
for (int i = 0; i < getChildCount(); i++) {
|
||||
QCRowView view = (QCRowView) getChildAt(i);
|
||||
view.setActionListener(mActionListener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(QCItem qcItem) {
|
||||
if (qcItem == null) {
|
||||
removeAllViews();
|
||||
return;
|
||||
}
|
||||
if (!qcItem.getType().equals(QCItem.QC_TYPE_LIST)) {
|
||||
throw new IllegalArgumentException("Expected QCList type for QCListView but got "
|
||||
+ qcItem.getType());
|
||||
}
|
||||
QCList qcList = (QCList) qcItem;
|
||||
int rowCount = qcList.getRows().size();
|
||||
for (int i = 0; i < rowCount; i++) {
|
||||
if (getChildAt(i) != null) {
|
||||
QCRowView view = (QCRowView) getChildAt(i);
|
||||
view.setRow(qcList.getRows().get(i));
|
||||
view.setActionListener(mActionListener);
|
||||
} else {
|
||||
QCRowView view = new QCRowView(getContext());
|
||||
view.setRow(qcList.getRows().get(i));
|
||||
view.setActionListener(mActionListener);
|
||||
addView(view);
|
||||
}
|
||||
}
|
||||
if (getChildCount() > rowCount) {
|
||||
// remove extra rows
|
||||
removeViews(rowCount, getChildCount() - rowCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
538
car-qc-lib/src/com/android/car/qc/view/QCRowView.java
Normal file
538
car-qc-lib/src/com/android/car/qc/view/QCRowView.java
Normal file
@@ -0,0 +1,538 @@
|
||||
/*
|
||||
* 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.car.qc.view;
|
||||
|
||||
import static com.android.car.qc.QCItem.QC_ACTION_SLIDER_VALUE;
|
||||
import static com.android.car.qc.QCItem.QC_ACTION_TOGGLE_STATE;
|
||||
import static com.android.car.qc.QCItem.QC_TYPE_ACTION_SWITCH;
|
||||
import static com.android.car.qc.view.QCView.QCActionListener;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.text.BidiFormatter;
|
||||
import android.text.TextDirectionHeuristics;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.car.qc.QCActionItem;
|
||||
import com.android.car.qc.QCItem;
|
||||
import com.android.car.qc.QCRow;
|
||||
import com.android.car.qc.QCSlider;
|
||||
import com.android.car.qc.R;
|
||||
import com.android.car.ui.utils.CarUiUtils;
|
||||
import com.android.car.ui.utils.DirectManipulationHelper;
|
||||
import com.android.car.ui.uxr.DrawableStateToggleButton;
|
||||
|
||||
/**
|
||||
* Quick Controls view for {@link QCRow} instances.
|
||||
*/
|
||||
public class QCRowView extends FrameLayout {
|
||||
private static final String TAG = "QCRowView";
|
||||
|
||||
private LayoutInflater mLayoutInflater;
|
||||
private BidiFormatter mBidiFormatter;
|
||||
private View mContentView;
|
||||
private TextView mTitle;
|
||||
private TextView mSubtitle;
|
||||
private ImageView mStartIcon;
|
||||
@ColorInt
|
||||
private int mStartIconTint;
|
||||
private LinearLayout mStartItemsContainer;
|
||||
private LinearLayout mEndItemsContainer;
|
||||
private LinearLayout mSeekBarContainer;
|
||||
@Nullable
|
||||
private QCSlider mQCSlider;
|
||||
private QCSeekBarView mSeekBar;
|
||||
private QCActionListener mActionListener;
|
||||
private boolean mInDirectManipulationMode;
|
||||
|
||||
private QCSeekbarChangeListener mSeekbarChangeListener;
|
||||
private final View.OnKeyListener mSeekBarKeyListener = new View.OnKeyListener() {
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
if (mSeekBar == null || (!mSeekBar.isEnabled()
|
||||
&& !mSeekBar.isClickableWhileDisabled())) {
|
||||
return false;
|
||||
}
|
||||
// Consume nudge events in direct manipulation mode.
|
||||
if (mInDirectManipulationMode
|
||||
&& (keyCode == KeyEvent.KEYCODE_DPAD_LEFT
|
||||
|| keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
|
||||
|| keyCode == KeyEvent.KEYCODE_DPAD_UP
|
||||
|| keyCode == KeyEvent.KEYCODE_DPAD_DOWN)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle events to enter or exit direct manipulation mode.
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
if (mQCSlider != null) {
|
||||
if (mQCSlider.isEnabled()) {
|
||||
setInDirectManipulationMode(v, mSeekBar, !mInDirectManipulationMode);
|
||||
} else {
|
||||
fireAction(mQCSlider, new Intent());
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
if (mInDirectManipulationMode) {
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
setInDirectManipulationMode(v, mSeekBar, false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't propagate confirm keys to the SeekBar to prevent a ripple effect on the thumb.
|
||||
if (KeyEvent.isConfirmKey(keyCode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
return mSeekBar.onKeyDown(keyCode, event);
|
||||
} else {
|
||||
return mSeekBar.onKeyUp(keyCode, event);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final View.OnFocusChangeListener mSeekBarFocusChangeListener =
|
||||
(v, hasFocus) -> {
|
||||
if (!hasFocus && mInDirectManipulationMode && mSeekBar != null) {
|
||||
setInDirectManipulationMode(v, mSeekBar, false);
|
||||
}
|
||||
};
|
||||
|
||||
private final View.OnGenericMotionListener mSeekBarScrollListener =
|
||||
(v, event) -> {
|
||||
if (!mInDirectManipulationMode || mSeekBar == null) {
|
||||
return false;
|
||||
}
|
||||
int adjustment = Math.round(event.getAxisValue(MotionEvent.AXIS_SCROLL));
|
||||
if (adjustment == 0) {
|
||||
return false;
|
||||
}
|
||||
int count = Math.abs(adjustment);
|
||||
int keyCode =
|
||||
adjustment < 0 ? KeyEvent.KEYCODE_DPAD_LEFT : KeyEvent.KEYCODE_DPAD_RIGHT;
|
||||
KeyEvent downEvent = new KeyEvent(event.getDownTime(), event.getEventTime(),
|
||||
KeyEvent.ACTION_DOWN, keyCode, /* repeat= */ 0);
|
||||
KeyEvent upEvent = new KeyEvent(event.getDownTime(), event.getEventTime(),
|
||||
KeyEvent.ACTION_UP, keyCode, /* repeat= */ 0);
|
||||
for (int i = 0; i < count; i++) {
|
||||
mSeekBar.onKeyDown(keyCode, downEvent);
|
||||
mSeekBar.onKeyUp(keyCode, upEvent);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
QCRowView(Context context) {
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
|
||||
QCRowView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
|
||||
QCRowView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(context);
|
||||
}
|
||||
|
||||
QCRowView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init(context);
|
||||
}
|
||||
|
||||
private void init(Context context) {
|
||||
mLayoutInflater = LayoutInflater.from(context);
|
||||
mBidiFormatter = BidiFormatter.getInstance();
|
||||
mLayoutInflater.inflate(R.layout.qc_row_view, /* root= */ this);
|
||||
mContentView = findViewById(R.id.qc_row_content);
|
||||
mTitle = findViewById(R.id.qc_title);
|
||||
mSubtitle = findViewById(R.id.qc_summary);
|
||||
mStartIcon = findViewById(R.id.qc_icon);
|
||||
mStartItemsContainer = findViewById(R.id.qc_row_start_items);
|
||||
mEndItemsContainer = findViewById(R.id.qc_row_end_items);
|
||||
mSeekBarContainer = findViewById(R.id.qc_seekbar_wrapper);
|
||||
mSeekBar = findViewById(R.id.qc_seekbar);
|
||||
}
|
||||
|
||||
void setActionListener(QCActionListener listener) {
|
||||
mActionListener = listener;
|
||||
}
|
||||
|
||||
void setRow(QCRow row) {
|
||||
if (row == null) {
|
||||
setVisibility(GONE);
|
||||
return;
|
||||
}
|
||||
setVisibility(VISIBLE);
|
||||
CarUiUtils.makeAllViewsEnabled(mContentView, row.isEnabled());
|
||||
if (!row.isEnabled()) {
|
||||
if (row.isClickableWhileDisabled() && (row.getDisabledClickAction() != null
|
||||
|| row.getDisabledClickActionHandler() != null)) {
|
||||
mContentView.setOnClickListener(v -> {
|
||||
fireAction(row, /* intent= */ null);
|
||||
});
|
||||
}
|
||||
} else if (row.getPrimaryAction() != null || row.getActionHandler() != null) {
|
||||
mContentView.setOnClickListener(v -> {
|
||||
fireAction(row, /* intent= */ null);
|
||||
});
|
||||
}
|
||||
if (!TextUtils.isEmpty(row.getTitle())) {
|
||||
mTitle.setVisibility(VISIBLE);
|
||||
mTitle.setText(
|
||||
mBidiFormatter.unicodeWrap(row.getTitle(), TextDirectionHeuristics.LOCALE));
|
||||
} else {
|
||||
mTitle.setVisibility(GONE);
|
||||
}
|
||||
if (!TextUtils.isEmpty(row.getSubtitle())) {
|
||||
mSubtitle.setVisibility(VISIBLE);
|
||||
mSubtitle.setText(
|
||||
mBidiFormatter.unicodeWrap(row.getSubtitle(), TextDirectionHeuristics.LOCALE));
|
||||
} else {
|
||||
mSubtitle.setVisibility(GONE);
|
||||
}
|
||||
if (row.getStartIcon() != null) {
|
||||
mStartIcon.setVisibility(VISIBLE);
|
||||
Drawable drawable = row.getStartIcon().loadDrawable(getContext());
|
||||
if (drawable != null && row.isStartIconTintable()) {
|
||||
if (mStartIconTint == 0) {
|
||||
mStartIconTint = getContext().getColor(R.color.qc_start_icon_color);
|
||||
}
|
||||
drawable.setTint(mStartIconTint);
|
||||
}
|
||||
mStartIcon.setImageDrawable(drawable);
|
||||
} else {
|
||||
mStartIcon.setImageDrawable(null);
|
||||
mStartIcon.setVisibility(GONE);
|
||||
}
|
||||
QCSlider slider = row.getSlider();
|
||||
if (slider != null) {
|
||||
mSeekBarContainer.setVisibility(View.VISIBLE);
|
||||
initSlider(slider);
|
||||
} else {
|
||||
mSeekBarContainer.setVisibility(View.GONE);
|
||||
mQCSlider = null;
|
||||
}
|
||||
|
||||
int startItemCount = row.getStartItems().size();
|
||||
for (int i = 0; i < startItemCount; i++) {
|
||||
QCActionItem action = row.getStartItems().get(i);
|
||||
initActionItem(mStartItemsContainer, mStartItemsContainer.getChildAt(i), action);
|
||||
}
|
||||
if (mStartItemsContainer.getChildCount() > startItemCount) {
|
||||
// remove extra items
|
||||
mStartItemsContainer.removeViews(startItemCount,
|
||||
mStartItemsContainer.getChildCount() - startItemCount);
|
||||
}
|
||||
if (startItemCount == 0) {
|
||||
mStartItemsContainer.setVisibility(View.GONE);
|
||||
} else {
|
||||
mStartItemsContainer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
int endItemCount = row.getEndItems().size();
|
||||
for (int i = 0; i < endItemCount; i++) {
|
||||
QCActionItem action = row.getEndItems().get(i);
|
||||
initActionItem(mEndItemsContainer, mEndItemsContainer.getChildAt(i), action);
|
||||
}
|
||||
if (mEndItemsContainer.getChildCount() > endItemCount) {
|
||||
// remove extra items
|
||||
mEndItemsContainer.removeViews(endItemCount,
|
||||
mEndItemsContainer.getChildCount() - endItemCount);
|
||||
}
|
||||
if (endItemCount == 0) {
|
||||
mEndItemsContainer.setVisibility(View.GONE);
|
||||
} else {
|
||||
mEndItemsContainer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void initActionItem(@NonNull ViewGroup root, @Nullable View actionView,
|
||||
@NonNull QCActionItem action) {
|
||||
if (action.getType().equals(QC_TYPE_ACTION_SWITCH)) {
|
||||
initSwitchView(action, root, actionView);
|
||||
} else {
|
||||
initToggleView(action, root, actionView);
|
||||
}
|
||||
}
|
||||
|
||||
private void initSwitchView(QCActionItem action, ViewGroup root, View actionView) {
|
||||
Switch switchView = actionView == null ? null : actionView.findViewById(
|
||||
android.R.id.switch_widget);
|
||||
if (switchView == null) {
|
||||
actionView = createActionView(root, actionView, R.layout.qc_action_switch);
|
||||
switchView = actionView.requireViewById(android.R.id.switch_widget);
|
||||
}
|
||||
CarUiUtils.makeAllViewsEnabled(switchView, action.isEnabled());
|
||||
|
||||
boolean shouldEnableView =
|
||||
(action.isEnabled() || action.isClickableWhileDisabled()) && action.isAvailable()
|
||||
&& action.isClickable();
|
||||
switchView.setOnCheckedChangeListener(null);
|
||||
switchView.setEnabled(shouldEnableView);
|
||||
switchView.setChecked(action.isChecked());
|
||||
switchView.setContentDescription(action.getContentDescription());
|
||||
switchView.setOnTouchListener((v, event) -> {
|
||||
if (!action.isEnabled()) {
|
||||
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
|
||||
fireAction(action, new Intent());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
switchView.setOnCheckedChangeListener(
|
||||
(buttonView, isChecked) -> {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(QC_ACTION_TOGGLE_STATE, isChecked);
|
||||
fireAction(action, intent);
|
||||
});
|
||||
}
|
||||
|
||||
private void initToggleView(QCActionItem action, ViewGroup root, View actionView) {
|
||||
DrawableStateToggleButton tmpToggleButton =
|
||||
actionView == null ? null : actionView.findViewById(R.id.qc_toggle_button);
|
||||
if (tmpToggleButton == null) {
|
||||
actionView = createActionView(root, actionView, R.layout.qc_action_toggle);
|
||||
tmpToggleButton = actionView.requireViewById(R.id.qc_toggle_button);
|
||||
}
|
||||
DrawableStateToggleButton toggleButton = tmpToggleButton; // must be effectively final
|
||||
boolean shouldEnableView =
|
||||
(action.isEnabled() || action.isClickableWhileDisabled()) && action.isAvailable()
|
||||
&& action.isClickable();
|
||||
toggleButton.setText(null);
|
||||
toggleButton.setTextOn(null);
|
||||
toggleButton.setTextOff(null);
|
||||
toggleButton.setOnCheckedChangeListener(null);
|
||||
Drawable icon = QCViewUtils.getToggleIcon(mContext, action.getIcon(), action.isAvailable());
|
||||
toggleButton.setContentDescription(action.getContentDescription());
|
||||
toggleButton.setButtonDrawable(icon);
|
||||
toggleButton.setChecked(action.isChecked());
|
||||
toggleButton.setEnabled(shouldEnableView);
|
||||
setToggleButtonDrawableState(toggleButton, action.isEnabled(), action.isAvailable());
|
||||
toggleButton.setOnTouchListener((v, event) -> {
|
||||
if (!action.isEnabled()) {
|
||||
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
|
||||
fireAction(action, new Intent());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
toggleButton.setOnCheckedChangeListener(
|
||||
(buttonView, isChecked) -> {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(QC_ACTION_TOGGLE_STATE, isChecked);
|
||||
fireAction(action, intent);
|
||||
});
|
||||
}
|
||||
|
||||
private void setToggleButtonDrawableState(DrawableStateToggleButton view,
|
||||
boolean enabled, boolean available) {
|
||||
int[] statesToAdd = null;
|
||||
int[] statesToRemove = null;
|
||||
if (enabled) {
|
||||
if (!available) {
|
||||
statesToAdd =
|
||||
new int[]{android.R.attr.state_enabled, R.attr.state_toggle_unavailable};
|
||||
} else {
|
||||
statesToAdd = new int[]{android.R.attr.state_enabled};
|
||||
statesToRemove = new int[]{R.attr.state_toggle_unavailable};
|
||||
}
|
||||
} else {
|
||||
if (available) {
|
||||
statesToRemove =
|
||||
new int[]{android.R.attr.state_enabled, R.attr.state_toggle_unavailable};
|
||||
} else {
|
||||
statesToAdd = new int[]{R.attr.state_toggle_unavailable};
|
||||
statesToRemove = new int[]{android.R.attr.state_enabled};
|
||||
}
|
||||
}
|
||||
CarUiUtils.applyDrawableStatesToAllViews(view, statesToAdd, statesToRemove);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private View createActionView(@NonNull ViewGroup root, @Nullable View actionView,
|
||||
@LayoutRes int resId) {
|
||||
if (actionView != null) {
|
||||
// remove current action view
|
||||
root.removeView(actionView);
|
||||
}
|
||||
actionView = mLayoutInflater.inflate(resId, root, /* attachToRoot= */ false);
|
||||
root.addView(actionView);
|
||||
return actionView;
|
||||
}
|
||||
|
||||
private void initSlider(QCSlider slider) {
|
||||
mQCSlider = slider;
|
||||
CarUiUtils.makeAllViewsEnabled(mSeekBar, slider.isEnabled());
|
||||
|
||||
mSeekBar.setOnSeekBarChangeListener(null);
|
||||
mSeekBar.setMin(slider.getMin());
|
||||
mSeekBar.setMax(slider.getMax());
|
||||
mSeekBar.setProgress(slider.getValue());
|
||||
mSeekBar.setEnabled(slider.isEnabled());
|
||||
mSeekBar.setClickableWhileDisabled(slider.isClickableWhileDisabled());
|
||||
mSeekBar.setDisabledClickListener(seekBar -> fireAction(slider, new Intent()));
|
||||
if (!slider.isEnabled() && mInDirectManipulationMode) {
|
||||
setInDirectManipulationMode(mSeekBarContainer, mSeekBar, false);
|
||||
}
|
||||
if (mSeekbarChangeListener == null) {
|
||||
mSeekbarChangeListener = new QCSeekbarChangeListener();
|
||||
}
|
||||
mSeekbarChangeListener.setSlider(slider);
|
||||
mSeekBar.setOnSeekBarChangeListener(mSeekbarChangeListener);
|
||||
// set up rotary support
|
||||
mSeekBarContainer.setOnKeyListener(mSeekBarKeyListener);
|
||||
mSeekBarContainer.setOnFocusChangeListener(mSeekBarFocusChangeListener);
|
||||
mSeekBarContainer.setOnGenericMotionListener(mSeekBarScrollListener);
|
||||
}
|
||||
|
||||
private void setInDirectManipulationMode(View view, SeekBar seekbar, boolean enable) {
|
||||
mInDirectManipulationMode = enable;
|
||||
DirectManipulationHelper.enableDirectManipulationMode(seekbar, enable);
|
||||
view.setSelected(enable);
|
||||
seekbar.setSelected(enable);
|
||||
}
|
||||
|
||||
private void fireAction(QCItem item, Intent intent) {
|
||||
if (!item.isEnabled()) {
|
||||
if (item.getDisabledClickAction() != null) {
|
||||
try {
|
||||
item.getDisabledClickAction().send(getContext(), 0, intent);
|
||||
if (mActionListener != null) {
|
||||
mActionListener.onQCAction(item, item.getDisabledClickAction());
|
||||
}
|
||||
} catch (PendingIntent.CanceledException e) {
|
||||
Log.d(TAG, "Error sending intent", e);
|
||||
}
|
||||
} else if (item.getDisabledClickActionHandler() != null) {
|
||||
item.getDisabledClickActionHandler().onAction(item, getContext(), intent);
|
||||
if (mActionListener != null) {
|
||||
mActionListener.onQCAction(item, item.getDisabledClickActionHandler());
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.getPrimaryAction() != null) {
|
||||
try {
|
||||
item.getPrimaryAction().send(getContext(), 0, intent);
|
||||
if (mActionListener != null) {
|
||||
mActionListener.onQCAction(item, item.getPrimaryAction());
|
||||
}
|
||||
} catch (PendingIntent.CanceledException e) {
|
||||
Log.d(TAG, "Error sending intent", e);
|
||||
}
|
||||
} else if (item.getActionHandler() != null) {
|
||||
item.getActionHandler().onAction(item, getContext(), intent);
|
||||
if (mActionListener != null) {
|
||||
mActionListener.onQCAction(item, item.getActionHandler());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class QCSeekbarChangeListener implements SeekBar.OnSeekBarChangeListener {
|
||||
// Interval of updates (in ms) sent in response to seekbar moving.
|
||||
private static final int SLIDER_UPDATE_INTERVAL = 200;
|
||||
|
||||
private final Handler mSliderUpdateHandler;
|
||||
private QCSlider mSlider;
|
||||
private int mCurrSliderValue;
|
||||
private boolean mSliderUpdaterRunning;
|
||||
private long mLastSentSliderUpdate;
|
||||
private final Runnable mSliderUpdater = () -> {
|
||||
sendSliderValue();
|
||||
mSliderUpdaterRunning = false;
|
||||
};
|
||||
|
||||
QCSeekbarChangeListener() {
|
||||
mSliderUpdateHandler = new Handler();
|
||||
}
|
||||
|
||||
void setSlider(QCSlider slider) {
|
||||
mSlider = slider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
mCurrSliderValue = progress;
|
||||
long now = System.currentTimeMillis();
|
||||
if (mLastSentSliderUpdate != 0
|
||||
&& now - mLastSentSliderUpdate > SLIDER_UPDATE_INTERVAL) {
|
||||
mSliderUpdaterRunning = false;
|
||||
mSliderUpdateHandler.removeCallbacks(mSliderUpdater);
|
||||
sendSliderValue();
|
||||
} else if (!mSliderUpdaterRunning) {
|
||||
mSliderUpdaterRunning = true;
|
||||
mSliderUpdateHandler.postDelayed(mSliderUpdater, SLIDER_UPDATE_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
if (mSliderUpdaterRunning) {
|
||||
mSliderUpdaterRunning = false;
|
||||
mSliderUpdateHandler.removeCallbacks(mSliderUpdater);
|
||||
}
|
||||
mCurrSliderValue = seekBar.getProgress();
|
||||
sendSliderValue();
|
||||
}
|
||||
|
||||
private void sendSliderValue() {
|
||||
if (mSlider == null) {
|
||||
return;
|
||||
}
|
||||
mLastSentSliderUpdate = System.currentTimeMillis();
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(QC_ACTION_SLIDER_VALUE, mCurrSliderValue);
|
||||
fireAction(mSlider, intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
78
car-qc-lib/src/com/android/car/qc/view/QCSeekBarView.java
Normal file
78
car-qc-lib/src/com/android/car/qc/view/QCSeekBarView.java
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.car.qc.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.widget.SeekBar;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.car.ui.uxr.DrawableStateSeekBar;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* A {@link SeekBar} specifically for Quick Controls that allows for a disabled click action
|
||||
* to execute on {@link MotionEvent.ACTION_UP}.
|
||||
*/
|
||||
public class QCSeekBarView extends DrawableStateSeekBar {
|
||||
private boolean mClickableWhileDisabled;
|
||||
private Consumer<SeekBar> mDisabledClickListener;
|
||||
|
||||
public QCSeekBarView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public QCSeekBarView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public QCSeekBarView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public QCSeekBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
// AbsSeekBar will ignore all touch events if not enabled. If this SeekBar should be
|
||||
// clickable while disabled, the touch event will be handled here.
|
||||
if (!isEnabled() && mClickableWhileDisabled) {
|
||||
if (event.getAction() == MotionEvent.ACTION_UP && mDisabledClickListener != null) {
|
||||
mDisabledClickListener.accept(this);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
public void setClickableWhileDisabled(boolean clickable) {
|
||||
mClickableWhileDisabled = clickable;
|
||||
}
|
||||
|
||||
public void setDisabledClickListener(@Nullable Consumer<SeekBar> disabledClickListener) {
|
||||
mDisabledClickListener = disabledClickListener;
|
||||
}
|
||||
|
||||
public boolean isClickableWhileDisabled() {
|
||||
return mClickableWhileDisabled;
|
||||
}
|
||||
}
|
||||
151
car-qc-lib/src/com/android/car/qc/view/QCTileView.java
Normal file
151
car-qc-lib/src/com/android/car/qc/view/QCTileView.java
Normal file
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* 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.car.qc.view;
|
||||
|
||||
import static com.android.car.qc.QCItem.QC_ACTION_TOGGLE_STATE;
|
||||
import static com.android.car.qc.view.QCView.QCActionListener;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import com.android.car.qc.QCItem;
|
||||
import com.android.car.qc.QCTile;
|
||||
import com.android.car.qc.R;
|
||||
import com.android.car.ui.utils.CarUiUtils;
|
||||
import com.android.car.ui.uxr.DrawableStateToggleButton;
|
||||
|
||||
/**
|
||||
* Quick Controls view for {@link QCTile} instances.
|
||||
*/
|
||||
public class QCTileView extends FrameLayout implements Observer<QCItem> {
|
||||
private static final String TAG = "QCTileView";
|
||||
|
||||
private View mTileWrapper;
|
||||
private DrawableStateToggleButton mToggleButton;
|
||||
private TextView mSubtitle;
|
||||
private QCActionListener mActionListener;
|
||||
|
||||
public QCTileView(Context context) {
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public QCTileView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public QCTileView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public QCTileView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tile's {@link QCActionListener}.
|
||||
*/
|
||||
public void setActionListener(QCActionListener listener) {
|
||||
mActionListener = listener;
|
||||
}
|
||||
|
||||
private void init(Context context) {
|
||||
View.inflate(context, R.layout.qc_tile_view, /* root= */ this);
|
||||
mTileWrapper = findViewById(R.id.qc_tile_wrapper);
|
||||
mToggleButton = findViewById(R.id.qc_tile_toggle_button);
|
||||
mSubtitle = findViewById(android.R.id.summary);
|
||||
mToggleButton.setText(null);
|
||||
mToggleButton.setTextOn(null);
|
||||
mToggleButton.setTextOff(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(QCItem qcItem) {
|
||||
if (qcItem == null) {
|
||||
removeAllViews();
|
||||
return;
|
||||
}
|
||||
if (!qcItem.getType().equals(QCItem.QC_TYPE_TILE)) {
|
||||
throw new IllegalArgumentException("Expected QCTile type for QCTileView but got "
|
||||
+ qcItem.getType());
|
||||
}
|
||||
QCTile qcTile = (QCTile) qcItem;
|
||||
mSubtitle.setText(qcTile.getSubtitle());
|
||||
CarUiUtils.makeAllViewsEnabled(mToggleButton, qcTile.isEnabled());
|
||||
mToggleButton.setOnCheckedChangeListener(null);
|
||||
mToggleButton.setChecked(qcTile.isChecked());
|
||||
mToggleButton.setEnabled(qcTile.isEnabled() || qcTile.isClickableWhileDisabled());
|
||||
mTileWrapper.setEnabled(
|
||||
(qcTile.isEnabled() || qcTile.isClickableWhileDisabled()) && qcTile.isAvailable());
|
||||
mTileWrapper.setOnClickListener(v -> {
|
||||
if (!qcTile.isEnabled()) {
|
||||
if (qcTile.getDisabledClickAction() != null) {
|
||||
try {
|
||||
qcTile.getDisabledClickAction().send(getContext(), 0, new Intent());
|
||||
if (mActionListener != null) {
|
||||
mActionListener.onQCAction(qcTile, qcTile.getDisabledClickAction());
|
||||
}
|
||||
} catch (PendingIntent.CanceledException e) {
|
||||
Log.d(TAG, "Error sending intent", e);
|
||||
}
|
||||
} else if (qcTile.getDisabledClickActionHandler() != null) {
|
||||
qcTile.getDisabledClickActionHandler().onAction(qcTile, getContext(),
|
||||
new Intent());
|
||||
if (mActionListener != null) {
|
||||
mActionListener.onQCAction(qcTile, qcTile.getDisabledClickActionHandler());
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
mToggleButton.toggle();
|
||||
});
|
||||
Drawable icon = QCViewUtils.getToggleIcon(mContext, qcTile.getIcon(), qcTile.isAvailable());
|
||||
mToggleButton.setButtonDrawable(icon);
|
||||
mToggleButton.setOnCheckedChangeListener(
|
||||
(buttonView, isChecked) -> {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(QC_ACTION_TOGGLE_STATE, isChecked);
|
||||
if (qcTile.getPrimaryAction() != null) {
|
||||
try {
|
||||
qcTile.getPrimaryAction().send(getContext(), 0, intent);
|
||||
if (mActionListener != null) {
|
||||
mActionListener.onQCAction(qcTile, qcTile.getPrimaryAction());
|
||||
}
|
||||
} catch (PendingIntent.CanceledException e) {
|
||||
Log.d(TAG, "Error sending intent", e);
|
||||
}
|
||||
} else if (qcTile.getActionHandler() != null) {
|
||||
qcTile.getActionHandler().onAction(qcTile, getContext(), intent);
|
||||
if (mActionListener != null) {
|
||||
mActionListener.onQCAction(qcTile, qcTile.getActionHandler());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
120
car-qc-lib/src/com/android/car/qc/view/QCView.java
Normal file
120
car-qc-lib/src/com/android/car/qc/view/QCView.java
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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.car.qc.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import com.android.car.qc.QCItem;
|
||||
|
||||
/**
|
||||
* Base Quick Controls View - supports {@link QCItem.QC_TYPE_TILE} and {@link QCItem.QC_TYPE_LIST}
|
||||
*/
|
||||
public class QCView extends FrameLayout implements Observer<QCItem> {
|
||||
@QCItem.QCItemType
|
||||
private String mType;
|
||||
private Observer<QCItem> mChildObserver;
|
||||
private QCActionListener mActionListener;
|
||||
|
||||
public QCView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public QCView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public QCView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public QCView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the view's {@link QCActionListener}. This listener will propagate to all sub-views.
|
||||
*/
|
||||
public void setActionListener(QCActionListener listener) {
|
||||
mActionListener = listener;
|
||||
if (mChildObserver instanceof QCTileView) {
|
||||
((QCTileView) mChildObserver).setActionListener(mActionListener);
|
||||
} else if (mChildObserver instanceof QCListView) {
|
||||
((QCListView) mChildObserver).setActionListener(mActionListener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(QCItem qcItem) {
|
||||
if (qcItem == null) {
|
||||
removeAllViews();
|
||||
mChildObserver = null;
|
||||
mType = null;
|
||||
return;
|
||||
}
|
||||
if (!isValidQCItemType(qcItem)) {
|
||||
throw new IllegalArgumentException("Expected QCTile or QCList type but got "
|
||||
+ qcItem.getType());
|
||||
}
|
||||
if (qcItem.getType().equals(mType)) {
|
||||
mChildObserver.onChanged(qcItem);
|
||||
return;
|
||||
}
|
||||
removeAllViews();
|
||||
mType = qcItem.getType();
|
||||
if (mType.equals(QCItem.QC_TYPE_TILE)) {
|
||||
QCTileView view = new QCTileView(getContext());
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
|
||||
LayoutParams.WRAP_CONTENT,
|
||||
LayoutParams.WRAP_CONTENT,
|
||||
Gravity.CENTER_HORIZONTAL);
|
||||
view.onChanged(qcItem);
|
||||
view.setActionListener(mActionListener);
|
||||
addView(view, params);
|
||||
mChildObserver = view;
|
||||
} else {
|
||||
QCListView view = new QCListView(getContext());
|
||||
view.onChanged(qcItem);
|
||||
view.setActionListener(mActionListener);
|
||||
addView(view);
|
||||
mChildObserver = view;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValidQCItemType(QCItem qcItem) {
|
||||
String type = qcItem.getType();
|
||||
return type.equals(QCItem.QC_TYPE_TILE) || type.equals(QCItem.QC_TYPE_LIST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener to be called when an action occurs on a QCView.
|
||||
*/
|
||||
public interface QCActionListener {
|
||||
/**
|
||||
* Called when an interaction has occurred with an element in this view.
|
||||
* @param item the specific item within the {@link QCItem} that was interacted with.
|
||||
* @param action the action that was executed - is generally either a
|
||||
* {@link android.app.PendingIntent} or {@link QCItem.ActionHandler}
|
||||
*/
|
||||
void onQCAction(@NonNull QCItem item, @NonNull Object action);
|
||||
}
|
||||
}
|
||||
74
car-qc-lib/src/com/android/car/qc/view/QCViewUtils.java
Normal file
74
car-qc-lib/src/com/android/car/qc/view/QCViewUtils.java
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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.car.qc.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.car.qc.R;
|
||||
|
||||
/**
|
||||
* Utility class used by {@link QCTileView} and {@link QCRowView}
|
||||
*/
|
||||
public class QCViewUtils {
|
||||
|
||||
/**
|
||||
* Create a return a Quick Control toggle icon - used for tiles and action toggles.
|
||||
*/
|
||||
public static Drawable getToggleIcon(@NonNull Context context, @Nullable Icon icon,
|
||||
boolean available) {
|
||||
Drawable defaultToggleBackground = context.getDrawable(R.drawable.qc_toggle_background);
|
||||
Drawable unavailableToggleBackground = context.getDrawable(
|
||||
R.drawable.qc_toggle_unavailable_background);
|
||||
int toggleForegroundIconInset = context.getResources()
|
||||
.getDimensionPixelSize(R.dimen.qc_toggle_foreground_icon_inset);
|
||||
|
||||
Drawable background = available
|
||||
? defaultToggleBackground.getConstantState().newDrawable().mutate()
|
||||
: unavailableToggleBackground.getConstantState().newDrawable().mutate();
|
||||
if (icon == null) {
|
||||
return background;
|
||||
}
|
||||
|
||||
Drawable iconDrawable = icon.loadDrawable(context);
|
||||
if (iconDrawable == null) {
|
||||
return background;
|
||||
}
|
||||
|
||||
if (!available) {
|
||||
int unavailableToggleIconTint = context.getColor(R.color.qc_toggle_unavailable_color);
|
||||
iconDrawable.setTint(unavailableToggleIconTint);
|
||||
} else {
|
||||
ColorStateList defaultToggleIconTint = context.getColorStateList(
|
||||
R.color.qc_toggle_icon_fill_color);
|
||||
iconDrawable.setTintList(defaultToggleIconTint);
|
||||
}
|
||||
|
||||
Drawable[] layers = {background, iconDrawable};
|
||||
LayerDrawable drawable = new LayerDrawable(layers);
|
||||
drawable.setLayerInsetRelative(/* index= */ 1, toggleForegroundIconInset,
|
||||
toggleForegroundIconInset, toggleForegroundIconInset,
|
||||
toggleForegroundIconInset);
|
||||
return drawable;
|
||||
}
|
||||
}
|
||||
50
car-qc-lib/tests/unit/Android.bp
Normal file
50
car-qc-lib/tests/unit/Android.bp
Normal file
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// 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 {
|
||||
default_applicable_licenses: ["Android-Apache-2.0"],
|
||||
}
|
||||
|
||||
android_test {
|
||||
name: "CarQCLibUnitTests",
|
||||
|
||||
certificate: "platform",
|
||||
privileged: true,
|
||||
|
||||
srcs: ["src/**/*.java"],
|
||||
|
||||
libs: [
|
||||
"android.test.runner",
|
||||
"android.test.base",
|
||||
"android.test.mock",
|
||||
],
|
||||
|
||||
static_libs: [
|
||||
"car-qc-lib",
|
||||
"androidx.test.core",
|
||||
"androidx.test.rules",
|
||||
"androidx.test.ext.junit",
|
||||
"androidx.test.ext.truth",
|
||||
"mockito-target-extended-minus-junit4",
|
||||
"platform-test-annotations",
|
||||
"truth",
|
||||
"testng",
|
||||
],
|
||||
|
||||
jni_libs: [
|
||||
"libdexmakerjvmtiagent",
|
||||
"libstaticjvmtiagent",
|
||||
],
|
||||
}
|
||||
41
car-qc-lib/tests/unit/AndroidManifest.xml
Normal file
41
car-qc-lib/tests/unit/AndroidManifest.xml
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.android.car.qc.tests.unit">
|
||||
|
||||
<application android:debuggable="true">
|
||||
<uses-library android:name="android.test.runner" />
|
||||
|
||||
<provider
|
||||
android:name="com.android.car.qc.testutils.AllowedTestQCProvider"
|
||||
android:authorities="com.android.car.qc.testutils.AllowedTestQCProvider"
|
||||
android:exported="true">
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="com.android.car.qc.testutils.DeniedTestQCProvider"
|
||||
android:authorities="com.android.car.qc.testutils.DeniedTestQCProvider"
|
||||
android:exported="true">
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
|
||||
android:targetPackage="com.android.car.qc.tests.unit"
|
||||
android:label="Quick Controls Library Unit Tests"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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.car.qc;
|
||||
|
||||
import static com.android.car.qc.QCItem.QC_TYPE_ACTION_SWITCH;
|
||||
import static com.android.car.qc.QCItem.QC_TYPE_ACTION_TOGGLE;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.testng.Assert.assertThrows;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.graphics.drawable.Icon;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class QCActionItemTest extends QCItemTestCase<QCActionItem> {
|
||||
private static final String TEST_CONTENT_DESCRIPTION = "test_content_description";
|
||||
|
||||
@Test
|
||||
public void onCreate_invalidType_throwsException() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> createAction("INVALID_TYPE", /* action= */ null,
|
||||
/* disabledAction= */ null, /* icon= */ null, /* contentDescription=*/
|
||||
null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onCreateSwitch_hasCorrectType() {
|
||||
QCActionItem action = createAction(QC_TYPE_ACTION_SWITCH, /* action= */ null,
|
||||
/* disabledAction= */ null, /* icon= */null, /* contentDescription=*/ null);
|
||||
assertThat(action.getType()).isEqualTo(QC_TYPE_ACTION_SWITCH);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onCreateToggle_hasCorrectType() {
|
||||
QCActionItem action = createAction(QC_TYPE_ACTION_TOGGLE, /* action= */ null,
|
||||
/* disabledAction= */ null, /* icon= */ null, /* contentDescription=*/ null);
|
||||
assertThat(action.getType()).isEqualTo(QC_TYPE_ACTION_TOGGLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onBundle_nullActions_noCrash() {
|
||||
QCActionItem action = createAction(QC_TYPE_ACTION_TOGGLE, /* action= */ null,
|
||||
/* disabledAction= */ null, mDefaultIcon, /* contentDescription=*/ null);
|
||||
writeAndLoadFromBundle(action);
|
||||
// Test passes if this doesn't crash
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onBundle_nullIcon_noCrash() {
|
||||
QCActionItem action = createAction(QC_TYPE_ACTION_TOGGLE, mDefaultAction,
|
||||
mDefaultDisabledAction, /* icon= */ null, /* contentDescription=*/ null);
|
||||
writeAndLoadFromBundle(action);
|
||||
// Test passes if this doesn't crash
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onBundle_switch_accurateData() {
|
||||
QCActionItem action = createAction(QC_TYPE_ACTION_SWITCH, mDefaultAction,
|
||||
mDefaultDisabledAction, /* icon= */ null, TEST_CONTENT_DESCRIPTION);
|
||||
QCActionItem newAction = writeAndLoadFromBundle(action);
|
||||
assertThat(newAction.getType()).isEqualTo(QC_TYPE_ACTION_SWITCH);
|
||||
assertThat(newAction.isChecked()).isTrue();
|
||||
assertThat(newAction.isEnabled()).isTrue();
|
||||
assertThat(newAction.isClickableWhileDisabled()).isFalse();
|
||||
assertThat(newAction.getPrimaryAction()).isNotNull();
|
||||
assertThat(newAction.getIcon()).isNull();
|
||||
assertThat(newAction.getContentDescription()).isEqualTo(TEST_CONTENT_DESCRIPTION);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onBundle_toggle_accurateDate() {
|
||||
QCActionItem action = createAction(QC_TYPE_ACTION_TOGGLE, mDefaultAction,
|
||||
mDefaultDisabledAction, mDefaultIcon, TEST_CONTENT_DESCRIPTION);
|
||||
QCActionItem newAction = writeAndLoadFromBundle(action);
|
||||
assertThat(newAction.getType()).isEqualTo(QC_TYPE_ACTION_TOGGLE);
|
||||
assertThat(newAction.isChecked()).isTrue();
|
||||
assertThat(newAction.isEnabled()).isTrue();
|
||||
assertThat(newAction.isClickableWhileDisabled()).isFalse();
|
||||
assertThat(newAction.getPrimaryAction()).isNotNull();
|
||||
assertThat(newAction.getIcon()).isNotNull();
|
||||
assertThat(newAction.getContentDescription()).isEqualTo(TEST_CONTENT_DESCRIPTION);
|
||||
}
|
||||
|
||||
private QCActionItem createAction(String type, PendingIntent action,
|
||||
PendingIntent disabledAction, Icon icon, String contentDescription) {
|
||||
return new QCActionItem.Builder(type)
|
||||
.setChecked(true)
|
||||
.setEnabled(true)
|
||||
.setAction(action)
|
||||
.setDisabledClickAction(disabledAction)
|
||||
.setIcon(icon)
|
||||
.setContentDescription(contentDescription)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.car.qc;
|
||||
|
||||
import android.R;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
public abstract class QCItemTestCase<T extends QCItem> {
|
||||
protected static final String BUNDLE_KEY = "BUNDLE_KEY";
|
||||
protected static final String TEST_TITLE = "TEST TITLE";
|
||||
protected static final String TEST_SUBTITLE = "TEST SUBTITLE";
|
||||
|
||||
protected final Context mContext = ApplicationProvider.getApplicationContext();
|
||||
|
||||
protected PendingIntent mDefaultAction = PendingIntent.getActivity(mContext,
|
||||
/* requestCode= */ 0, new Intent(), PendingIntent.FLAG_IMMUTABLE);
|
||||
protected PendingIntent mDefaultDisabledAction = PendingIntent.getActivity(mContext,
|
||||
/* requestCode= */ 1, new Intent(), PendingIntent.FLAG_IMMUTABLE);
|
||||
protected Icon mDefaultIcon = Icon.createWithResource(mContext, R.drawable.btn_star);
|
||||
|
||||
protected T writeAndLoadFromBundle(T item) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(BUNDLE_KEY, item);
|
||||
return bundle.getParcelable(BUNDLE_KEY);
|
||||
}
|
||||
}
|
||||
62
car-qc-lib/tests/unit/src/com/android/car/qc/QCListTest.java
Normal file
62
car-qc-lib/tests/unit/src/com/android/car/qc/QCListTest.java
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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.car.qc;
|
||||
|
||||
import static com.android.car.qc.QCItem.QC_TYPE_LIST;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class QCListTest extends QCItemTestCase<QCList> {
|
||||
|
||||
@Test
|
||||
public void onCreate_hasCorrectType() {
|
||||
QCList list = createList(Collections.emptyList());
|
||||
assertThat(list.getType()).isEqualTo(QC_TYPE_LIST);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createFromParcel_accurateData() {
|
||||
QCRow row = new QCRow.Builder()
|
||||
.setTitle(TEST_TITLE)
|
||||
.setSubtitle(TEST_SUBTITLE)
|
||||
.setIcon(mDefaultIcon)
|
||||
.setPrimaryAction(mDefaultAction)
|
||||
.build();
|
||||
|
||||
QCList list = createList(Collections.singletonList(row));
|
||||
QCList newList = writeAndLoadFromBundle(list);
|
||||
assertThat(newList.getType()).isEqualTo(QC_TYPE_LIST);
|
||||
assertThat(newList.getRows().size()).isEqualTo(1);
|
||||
}
|
||||
|
||||
private QCList createList(List<QCRow> rows) {
|
||||
QCList.Builder builder = new QCList.Builder();
|
||||
for (QCRow row : rows) {
|
||||
builder.addRow(row);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
124
car-qc-lib/tests/unit/src/com/android/car/qc/QCRowTest.java
Normal file
124
car-qc-lib/tests/unit/src/com/android/car/qc/QCRowTest.java
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 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.car.qc;
|
||||
|
||||
import static com.android.car.qc.QCItem.QC_TYPE_ACTION_SWITCH;
|
||||
import static com.android.car.qc.QCItem.QC_TYPE_ROW;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.graphics.drawable.Icon;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class QCRowTest extends QCItemTestCase<QCRow> {
|
||||
|
||||
@Test
|
||||
public void onCreate_hasCorrectType() {
|
||||
QCRow row = createRow(/* action= */ null, /* disabledAction= */ null, /* icon= */ null);
|
||||
assertThat(row.getType()).isEqualTo(QC_TYPE_ROW);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onBundle_nullActions_noCrash() {
|
||||
QCRow row = createRow(/* action= */ null, /* disabledAction= */ null, mDefaultIcon);
|
||||
writeAndLoadFromBundle(row);
|
||||
// Test passes if this doesn't crash
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onBundle_nullIcon_noCrash() {
|
||||
QCRow row = createRow(mDefaultAction, mDefaultDisabledAction, /* icon= */ null);
|
||||
writeAndLoadFromBundle(row);
|
||||
// Test passes if this doesn't crash
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createFromParcel_accurateData() {
|
||||
QCRow row = createRow(mDefaultAction, mDefaultDisabledAction, mDefaultIcon);
|
||||
QCRow newRow = writeAndLoadFromBundle(row);
|
||||
assertThat(newRow.getType()).isEqualTo(QC_TYPE_ROW);
|
||||
assertThat(newRow.getTitle()).isEqualTo(TEST_TITLE);
|
||||
assertThat(newRow.getSubtitle()).isEqualTo(TEST_SUBTITLE);
|
||||
assertThat(newRow.getPrimaryAction()).isNotNull();
|
||||
assertThat(newRow.getStartIcon()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createFromParcel_accurateData_startItem() {
|
||||
QCActionItem item = new QCActionItem.Builder(QC_TYPE_ACTION_SWITCH).build();
|
||||
|
||||
QCRow row = createRow(/* action= */ null, /* disabledAction= */ null, /* icon= */ null,
|
||||
Collections.singletonList(item), Collections.emptyList(), Collections.emptyList());
|
||||
QCRow newRow = writeAndLoadFromBundle(row);
|
||||
assertThat(newRow.getStartItems().size()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createFromParcel_accurateData_endItem() {
|
||||
QCActionItem item = new QCActionItem.Builder(QC_TYPE_ACTION_SWITCH).build();
|
||||
|
||||
QCRow row = createRow(/* action= */ null, /* disabledAction= */ null, /* icon= */ null,
|
||||
Collections.emptyList(), Collections.singletonList(item), Collections.emptyList());
|
||||
QCRow newRow = writeAndLoadFromBundle(row);
|
||||
assertThat(newRow.getEndItems().size()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createFromParcel_accurateData_slider() {
|
||||
QCSlider slider = new QCSlider.Builder().build();
|
||||
|
||||
QCRow row = createRow(/* action= */ null, /* disabledAction= */ null, /* icon= */ null,
|
||||
Collections.emptyList(), Collections.emptyList(),
|
||||
Collections.singletonList(slider));
|
||||
QCRow newRow = writeAndLoadFromBundle(row);
|
||||
assertThat(newRow.getSlider()).isNotNull();
|
||||
}
|
||||
|
||||
private QCRow createRow(PendingIntent action, PendingIntent disabledAction, Icon icon) {
|
||||
return createRow(action, disabledAction, icon, Collections.emptyList(),
|
||||
Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
private QCRow createRow(PendingIntent action, PendingIntent disabledAction, Icon icon,
|
||||
List<QCActionItem> startItems, List<QCActionItem> endItems, List<QCSlider> sliders) {
|
||||
QCRow.Builder builder = new QCRow.Builder()
|
||||
.setTitle(TEST_TITLE)
|
||||
.setSubtitle(TEST_SUBTITLE)
|
||||
.setIcon(icon)
|
||||
.setPrimaryAction(action)
|
||||
.setDisabledClickAction(disabledAction);
|
||||
for (QCActionItem item : startItems) {
|
||||
builder.addStartItem(item);
|
||||
}
|
||||
for (QCActionItem item : endItems) {
|
||||
builder.addEndItem(item);
|
||||
}
|
||||
for (QCSlider slider : sliders) {
|
||||
builder.addSlider(slider);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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.car.qc;
|
||||
|
||||
import static com.android.car.qc.QCItem.QC_TYPE_SLIDER;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class QCSliderTest extends QCItemTestCase<QCSlider> {
|
||||
private static final int MIN = 50;
|
||||
private static final int MAX = 150;
|
||||
private static final int VALUE = 75;
|
||||
|
||||
@Test
|
||||
public void onCreate_hasCorrectType() {
|
||||
QCSlider slider = createSlider(/* action= */ null, /* disabledAction= */ null);
|
||||
assertThat(slider.getType()).isEqualTo(QC_TYPE_SLIDER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onBundle_nullActions_noCrash() {
|
||||
QCSlider slider = createSlider(/* action= */ null, /* disabledAction= */ null);
|
||||
writeAndLoadFromBundle(slider);
|
||||
// Test passes if this doesn't crash
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createFromParcel_accurateData() {
|
||||
QCSlider slider = createSlider(mDefaultAction, mDefaultDisabledAction);
|
||||
QCSlider newSlider = writeAndLoadFromBundle(slider);
|
||||
assertThat(newSlider.getType()).isEqualTo(QC_TYPE_SLIDER);
|
||||
assertThat(newSlider.getPrimaryAction()).isNotNull();
|
||||
assertThat(newSlider.getDisabledClickAction()).isNotNull();
|
||||
assertThat(newSlider.getMin()).isEqualTo(MIN);
|
||||
assertThat(newSlider.getMax()).isEqualTo(MAX);
|
||||
assertThat(newSlider.getValue()).isEqualTo(VALUE);
|
||||
}
|
||||
|
||||
private QCSlider createSlider(PendingIntent action, PendingIntent disabledAction) {
|
||||
return new QCSlider.Builder()
|
||||
.setMin(MIN)
|
||||
.setMax(MAX)
|
||||
.setValue(VALUE)
|
||||
.setInputAction(action)
|
||||
.setDisabledClickAction(disabledAction)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
77
car-qc-lib/tests/unit/src/com/android/car/qc/QCTileTest.java
Normal file
77
car-qc-lib/tests/unit/src/com/android/car/qc/QCTileTest.java
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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.car.qc;
|
||||
|
||||
import static com.android.car.qc.QCItem.QC_TYPE_TILE;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.graphics.drawable.Icon;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class QCTileTest extends QCItemTestCase<QCTile> {
|
||||
|
||||
@Test
|
||||
public void onCreate_hasCorrectType() {
|
||||
QCTile tile = createTile(/* action= */ null, /* disabledAction= */ null, /* icon= */ null);
|
||||
assertThat(tile.getType()).isEqualTo(QC_TYPE_TILE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onBundle_nullAction_noCrash() {
|
||||
QCTile tile = createTile(/* action= */ null, /* disabledAction= */ null, mDefaultIcon);
|
||||
writeAndLoadFromBundle(tile);
|
||||
// Test passes if this doesn't crash
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onBundle_nullIcon_noCrash() {
|
||||
QCTile tile = createTile(mDefaultAction, mDefaultDisabledAction, /* icon= */ null);
|
||||
writeAndLoadFromBundle(tile);
|
||||
// Test passes if this doesn't crash
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createFromParcel_accurateData() {
|
||||
QCTile tile = createTile(mDefaultAction, mDefaultDisabledAction, mDefaultIcon);
|
||||
QCTile newTile = writeAndLoadFromBundle(tile);
|
||||
assertThat(newTile.getType()).isEqualTo(QC_TYPE_TILE);
|
||||
assertThat(newTile.getSubtitle()).isEqualTo(TEST_SUBTITLE);
|
||||
assertThat(newTile.isChecked()).isTrue();
|
||||
assertThat(newTile.isEnabled()).isTrue();
|
||||
assertThat(newTile.getPrimaryAction()).isNotNull();
|
||||
assertThat(newTile.getDisabledClickAction()).isNotNull();
|
||||
assertThat(newTile.getIcon()).isNotNull();
|
||||
}
|
||||
|
||||
private QCTile createTile(PendingIntent action, PendingIntent disabledAction, Icon icon) {
|
||||
return new QCTile.Builder()
|
||||
.setSubtitle(TEST_SUBTITLE)
|
||||
.setChecked(true)
|
||||
.setEnabled(true)
|
||||
.setAction(action)
|
||||
.setDisabledClickAction(disabledAction)
|
||||
.setIcon(icon)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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.car.qc.controller;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import com.android.car.qc.QCItem;
|
||||
import com.android.car.qc.QCTile;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public abstract class BaseQCControllerTestCase<T extends BaseQCController> {
|
||||
|
||||
protected final Context mContext = spy(ApplicationProvider.getApplicationContext());
|
||||
|
||||
protected abstract T getController();
|
||||
|
||||
@Test
|
||||
public void listen_updateListeningCalled() {
|
||||
T spiedController = spy(getController());
|
||||
spiedController.listen(true);
|
||||
verify(spiedController).updateListening();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addObserver_updateListeningCalled() {
|
||||
Observer<QCItem> observer = mock(Observer.class);
|
||||
T spiedController = spy(getController());
|
||||
spiedController.addObserver(observer);
|
||||
verify(spiedController).updateListening();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeObserver_updateListeningCalled() {
|
||||
Observer<QCItem> observer = mock(Observer.class);
|
||||
T spiedController = spy(getController());
|
||||
spiedController.removeObserver(observer);
|
||||
verify(spiedController).updateListening();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onQCItemUpdated_observersNotified() {
|
||||
Observer<QCItem> observer = mock(Observer.class);
|
||||
getController().addObserver(observer);
|
||||
getController().onQCItemUpdated(new QCTile.Builder().build());
|
||||
verify(observer).onChanged(any(QCItem.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onDestroy_cleanUpController() {
|
||||
Observer<QCItem> observer = mock(Observer.class);
|
||||
getController().addObserver(observer);
|
||||
getController().listen(true);
|
||||
getController().destroy();
|
||||
assertThat(getController().mObservers.size()).isEqualTo(0);
|
||||
assertThat(getController().mShouldListen).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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.car.qc.controller;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import com.android.car.qc.QCItem;
|
||||
import com.android.car.qc.provider.BaseLocalQCProvider;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class LocalQCControllerTest extends BaseQCControllerTestCase<LocalQCController> {
|
||||
|
||||
private LocalQCController mController;
|
||||
private BaseLocalQCProvider mProvider;
|
||||
|
||||
@Override
|
||||
protected LocalQCController getController() {
|
||||
if (mController == null) {
|
||||
mProvider = mock(BaseLocalQCProvider.class);
|
||||
mController = new LocalQCController(mContext, mProvider);
|
||||
}
|
||||
return mController;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onCreate_setsProviderNotifier() {
|
||||
getController(); // instantiate
|
||||
verify(mProvider).setNotifier(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onBind_updatesQCItem() {
|
||||
Observer<QCItem> observer = mock(Observer.class);
|
||||
LocalQCController spiedController = spy(getController());
|
||||
spiedController.addObserver(observer);
|
||||
Mockito.reset(mProvider);
|
||||
spiedController.bind();
|
||||
verify(mProvider).getQCItem();
|
||||
verify(spiedController).onQCItemUpdated(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateListening_updatesProviderListening() {
|
||||
Observer<QCItem> observer = mock(Observer.class);
|
||||
getController().addObserver(observer);
|
||||
getController().listen(true);
|
||||
verify(mProvider).shouldListen(true);
|
||||
getController().listen(false);
|
||||
verify(mProvider).shouldListen(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateListening_listen_updatesQCItem() {
|
||||
Observer<QCItem> observer = mock(Observer.class);
|
||||
LocalQCController spiedController = spy(getController());
|
||||
spiedController.addObserver(observer);
|
||||
Mockito.reset(mProvider);
|
||||
spiedController.listen(true);
|
||||
verify(mProvider).getQCItem();
|
||||
verify(spiedController).onQCItemUpdated(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onDestroy_callsProviderDestroy() {
|
||||
getController().destroy();
|
||||
verify(mProvider).onDestroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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.car.qc.controller;
|
||||
|
||||
import static com.android.car.qc.provider.BaseQCProvider.EXTRA_URI;
|
||||
import static com.android.car.qc.testutils.TestQCProvider.IS_DESTROYED_KEY;
|
||||
import static com.android.car.qc.testutils.TestQCProvider.IS_SUBSCRIBED_KEY;
|
||||
import static com.android.car.qc.testutils.TestQCProvider.KEY_DEFAULT;
|
||||
import static com.android.car.qc.testutils.TestQCProvider.METHOD_IS_DESTROYED;
|
||||
import static com.android.car.qc.testutils.TestQCProvider.METHOD_IS_SUBSCRIBED;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.notNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentResolver;
|
||||
import android.database.ContentObserver;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import com.android.car.qc.QCItem;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class RemoteQCControllerTest extends BaseQCControllerTestCase<RemoteQCController> {
|
||||
|
||||
private final Uri mDefaultUri = Uri.parse(
|
||||
"content://com.android.car.qc.testutils.AllowedTestQCProvider/" + KEY_DEFAULT);
|
||||
|
||||
private RemoteQCController mController;
|
||||
|
||||
@Override
|
||||
protected RemoteQCController getController() {
|
||||
if (mController == null) {
|
||||
mController = new RemoteQCController(mContext, mDefaultUri, mContext.getMainExecutor());
|
||||
}
|
||||
return mController;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onBind_updatesQCItem() {
|
||||
Observer<QCItem> observer = mock(Observer.class);
|
||||
RemoteQCController spiedController = spy(getController());
|
||||
spiedController.addObserver(observer);
|
||||
spiedController.bind();
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
|
||||
verify(spiedController).onQCItemUpdated(notNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateListening_listen_updatesQCItem() {
|
||||
Observer<QCItem> observer = mock(Observer.class);
|
||||
RemoteQCController spiedController = spy(getController());
|
||||
spiedController.addObserver(observer);
|
||||
spiedController.listen(true);
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
|
||||
verify(spiedController).onQCItemUpdated(notNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateListening_listen_providerSubscribed() throws RemoteException {
|
||||
Observer<QCItem> observer = mock(Observer.class);
|
||||
getController().addObserver(observer);
|
||||
getController().listen(true);
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
|
||||
|
||||
Bundle extras = new Bundle();
|
||||
extras.putParcelable(EXTRA_URI, mDefaultUri);
|
||||
Bundle res = getController().getClient().call(METHOD_IS_SUBSCRIBED, null, extras);
|
||||
assertThat(res).isNotNull();
|
||||
boolean isSubscribed = res.getBoolean(IS_SUBSCRIBED_KEY, false);
|
||||
assertThat(isSubscribed).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateListening_doNotListen_providerUnsubscribed() throws RemoteException {
|
||||
Observer<QCItem> observer = mock(Observer.class);
|
||||
getController().addObserver(observer);
|
||||
getController().listen(true);
|
||||
getController().listen(false);
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
|
||||
|
||||
Bundle extras = new Bundle();
|
||||
extras.putParcelable(EXTRA_URI, mDefaultUri);
|
||||
Bundle res = getController().getClient().call(METHOD_IS_SUBSCRIBED, null, extras);
|
||||
assertThat(res).isNotNull();
|
||||
boolean isSubscribed = res.getBoolean(IS_SUBSCRIBED_KEY, true);
|
||||
assertThat(isSubscribed).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateListening_listen_registerContentObserver() {
|
||||
ContentResolver resolver = mock(ContentResolver.class);
|
||||
when(mContext.getContentResolver()).thenReturn(resolver);
|
||||
when(resolver.acquireContentProviderClient(mDefaultUri)).thenReturn(
|
||||
mock(ContentProviderClient.class));
|
||||
Observer<QCItem> observer = mock(Observer.class);
|
||||
getController().addObserver(observer);
|
||||
getController().listen(true);
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
|
||||
verify(resolver).registerContentObserver(eq(mDefaultUri), eq(true),
|
||||
any(ContentObserver.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateListening_doNotListen_unregisterContentObserver() {
|
||||
ContentResolver resolver = mock(ContentResolver.class);
|
||||
when(mContext.getContentResolver()).thenReturn(resolver);
|
||||
when(resolver.acquireContentProviderClient(mDefaultUri)).thenReturn(
|
||||
mock(ContentProviderClient.class));
|
||||
Observer<QCItem> observer = mock(Observer.class);
|
||||
getController().addObserver(observer);
|
||||
getController().listen(true);
|
||||
getController().listen(false);
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
|
||||
verify(resolver).unregisterContentObserver(any(ContentObserver.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onDestroy_callsProviderOnDestroy() throws RemoteException {
|
||||
Observer<QCItem> observer = mock(Observer.class);
|
||||
getController().addObserver(observer);
|
||||
getController().listen(true);
|
||||
getController().listen(false);
|
||||
getController().destroy();
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
|
||||
|
||||
Bundle extras = new Bundle();
|
||||
extras.putParcelable(EXTRA_URI, mDefaultUri);
|
||||
Bundle res = getController().getClient().call(METHOD_IS_DESTROYED, null, extras);
|
||||
assertThat(res).isNotNull();
|
||||
boolean isDestroyed = res.getBoolean(IS_DESTROYED_KEY, false);
|
||||
assertThat(isDestroyed).isTrue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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.car.qc.provider;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import com.android.car.qc.QCItem;
|
||||
import com.android.car.qc.QCTile;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class BaseLocalQCProviderTest {
|
||||
|
||||
private final Context mContext = ApplicationProvider.getApplicationContext();
|
||||
private TestBaseLocalQCProvider mProvider;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mProvider = new TestBaseLocalQCProvider(mContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getQCItem_returnsItem() {
|
||||
QCItem item = mProvider.getQCItem();
|
||||
assertThat(item).isNotNull();
|
||||
assertThat(item instanceof QCTile).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listen_callsOnSubscribed() {
|
||||
mProvider.shouldListen(true);
|
||||
assertThat(mProvider.isSubscribed()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stopListening_callsOnUnsubscribed() {
|
||||
mProvider.shouldListen(true);
|
||||
mProvider.shouldListen(false);
|
||||
assertThat(mProvider.isSubscribed()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void notifyChange_updateNotified() {
|
||||
BaseLocalQCProvider.Notifier notifier = mock(BaseLocalQCProvider.Notifier.class);
|
||||
mProvider.setNotifier(notifier);
|
||||
mProvider.notifyChange();
|
||||
verify(notifier).notifyUpdate();
|
||||
}
|
||||
|
||||
private static class TestBaseLocalQCProvider extends BaseLocalQCProvider {
|
||||
|
||||
private boolean mIsSubscribed;
|
||||
|
||||
TestBaseLocalQCProvider(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public QCItem getQCItem() {
|
||||
return new QCTile.Builder().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSubscribed() {
|
||||
mIsSubscribed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUnsubscribed() {
|
||||
mIsSubscribed = false;
|
||||
}
|
||||
|
||||
boolean isSubscribed() {
|
||||
return mIsSubscribed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 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.car.qc.provider;
|
||||
|
||||
import static com.android.car.qc.provider.BaseQCProvider.EXTRA_ITEM;
|
||||
import static com.android.car.qc.provider.BaseQCProvider.EXTRA_URI;
|
||||
import static com.android.car.qc.provider.BaseQCProvider.METHOD_BIND;
|
||||
import static com.android.car.qc.provider.BaseQCProvider.METHOD_DESTROY;
|
||||
import static com.android.car.qc.provider.BaseQCProvider.METHOD_SUBSCRIBE;
|
||||
import static com.android.car.qc.provider.BaseQCProvider.METHOD_UNSUBSCRIBE;
|
||||
import static com.android.car.qc.testutils.TestQCProvider.IS_DESTROYED_KEY;
|
||||
import static com.android.car.qc.testutils.TestQCProvider.IS_SUBSCRIBED_KEY;
|
||||
import static com.android.car.qc.testutils.TestQCProvider.KEY_DEFAULT;
|
||||
import static com.android.car.qc.testutils.TestQCProvider.KEY_SLOW;
|
||||
import static com.android.car.qc.testutils.TestQCProvider.METHOD_IS_DESTROYED;
|
||||
import static com.android.car.qc.testutils.TestQCProvider.METHOD_IS_SUBSCRIBED;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.testng.Assert.assertThrows;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import com.android.car.qc.QCItem;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class BaseQCProviderTest {
|
||||
|
||||
private final Context mContext = ApplicationProvider.getApplicationContext();
|
||||
private final Uri mDefaultUri = Uri.parse(
|
||||
"content://com.android.car.qc.testutils.AllowedTestQCProvider/" + KEY_DEFAULT);
|
||||
private final Uri mSlowUri =
|
||||
Uri.parse("content://com.android.car.qc.testutils.AllowedTestQCProvider/" + KEY_SLOW);
|
||||
private final Uri mDeniedUri =
|
||||
Uri.parse("content://com.android.car.qc.testutils.DeniedTestQCProvider");
|
||||
|
||||
@Test
|
||||
public void callOnBind_allowed_returnsItem() throws RemoteException {
|
||||
ContentProviderClient provider = getClient(mDefaultUri);
|
||||
assertThat(provider).isNotNull();
|
||||
Bundle extras = new Bundle();
|
||||
extras.putParcelable(EXTRA_URI, mDefaultUri);
|
||||
Bundle res = provider.call(METHOD_BIND, null, extras);
|
||||
assertThat(res).isNotNull();
|
||||
res.setClassLoader(QCItem.class.getClassLoader());
|
||||
Parcelable parcelable = res.getParcelable(EXTRA_ITEM);
|
||||
assertThat(parcelable).isNotNull();
|
||||
assertThat(parcelable instanceof QCItem).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void callOnBind_noUri_throwsIllegalArgumentException() throws RemoteException {
|
||||
ContentProviderClient provider = getClient(mDefaultUri);
|
||||
assertThat(provider).isNotNull();
|
||||
Bundle extras = new Bundle();
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> provider.call(METHOD_BIND, null, extras));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void callOnBind_slowOperation_throwsRuntimeException() {
|
||||
ContentProviderClient provider = getClient(mSlowUri);
|
||||
assertThat(provider).isNotNull();
|
||||
Bundle extras = new Bundle();
|
||||
extras.putParcelable(EXTRA_URI, mSlowUri);
|
||||
assertThrows(RuntimeException.class,
|
||||
() -> provider.call(METHOD_BIND, null, extras));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void callOnBind_notAllowed_throwsSecurityException() {
|
||||
ContentProviderClient provider = getClient(mDeniedUri);
|
||||
assertThat(provider).isNotNull();
|
||||
Bundle extras = new Bundle();
|
||||
extras.putParcelable(EXTRA_URI, mDeniedUri);
|
||||
assertThrows(SecurityException.class,
|
||||
() -> provider.call(METHOD_BIND, null, extras));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void callOnSubscribed_isSubscribed() throws RemoteException {
|
||||
ContentProviderClient provider = getClient(mDefaultUri);
|
||||
assertThat(provider).isNotNull();
|
||||
Bundle extras = new Bundle();
|
||||
extras.putParcelable(EXTRA_URI, mDefaultUri);
|
||||
provider.call(METHOD_SUBSCRIBE, null, extras);
|
||||
|
||||
Bundle res = provider.call(METHOD_IS_SUBSCRIBED, null, extras);
|
||||
assertThat(res).isNotNull();
|
||||
boolean isSubscribed = res.getBoolean(IS_SUBSCRIBED_KEY, false);
|
||||
assertThat(isSubscribed).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void callOnUnsubscribed_isUnsubscribed() throws RemoteException {
|
||||
ContentProviderClient provider = getClient(mDefaultUri);
|
||||
assertThat(provider).isNotNull();
|
||||
Bundle extras = new Bundle();
|
||||
extras.putParcelable(EXTRA_URI, mDefaultUri);
|
||||
provider.call(METHOD_SUBSCRIBE, null, extras);
|
||||
provider.call(METHOD_UNSUBSCRIBE, null, extras);
|
||||
|
||||
Bundle res = provider.call(METHOD_IS_SUBSCRIBED, null, extras);
|
||||
assertThat(res).isNotNull();
|
||||
boolean isSubscribed = res.getBoolean(IS_SUBSCRIBED_KEY, true);
|
||||
assertThat(isSubscribed).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void callDestroy_isDestroyed() throws RemoteException {
|
||||
ContentProviderClient provider = getClient(mDefaultUri);
|
||||
assertThat(provider).isNotNull();
|
||||
Bundle extras = new Bundle();
|
||||
extras.putParcelable(EXTRA_URI, mDefaultUri);
|
||||
provider.call(METHOD_SUBSCRIBE, null, extras);
|
||||
provider.call(METHOD_UNSUBSCRIBE, null, extras);
|
||||
provider.call(METHOD_DESTROY, null, extras);
|
||||
|
||||
Bundle res = provider.call(METHOD_IS_DESTROYED, null, extras);
|
||||
assertThat(res).isNotNull();
|
||||
boolean isDestroyed = res.getBoolean(IS_DESTROYED_KEY, false);
|
||||
assertThat(isDestroyed).isTrue();
|
||||
}
|
||||
|
||||
private ContentProviderClient getClient(Uri uri) {
|
||||
return mContext.getContentResolver().acquireContentProviderClient(uri);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.car.qc.testutils;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class AllowedTestQCProvider extends TestQCProvider {
|
||||
@Override
|
||||
protected Set<String> getAllowlistedPackages() {
|
||||
Set<String> allowlist = new HashSet<>();
|
||||
allowlist.add("com.android.car.qc.tests.unit");
|
||||
return allowlist;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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.car.qc.testutils;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class DeniedTestQCProvider extends TestQCProvider {
|
||||
@Override
|
||||
protected Set<String> getAllowlistedPackages() {
|
||||
return new HashSet<>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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.car.qc.testutils;
|
||||
|
||||
import android.R;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.car.qc.QCItem;
|
||||
import com.android.car.qc.QCTile;
|
||||
import com.android.car.qc.provider.BaseQCProvider;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public abstract class TestQCProvider extends BaseQCProvider {
|
||||
|
||||
public static final String METHOD_IS_SUBSCRIBED = "METHOD_IS_SUBSCRIBED";
|
||||
public static final String IS_SUBSCRIBED_KEY = "IS_SUBSCRIBED";
|
||||
public static final String METHOD_IS_DESTROYED = "METHOD_IS_DESTROYED";
|
||||
public static final String IS_DESTROYED_KEY = "IS_DESTROYED";
|
||||
|
||||
public static final String KEY_DEFAULT = "DEFAULT";
|
||||
public static final String KEY_SLOW = "SLOW";
|
||||
|
||||
private final Set<Uri> mSubscribedUris = new HashSet<>();
|
||||
private final Set<Uri> mDestroyedUris = new HashSet<>();
|
||||
|
||||
@Override
|
||||
public Bundle call(String method, String arg, Bundle extras) {
|
||||
if (METHOD_IS_SUBSCRIBED.equals(method)) {
|
||||
Uri uri = getUriWithoutUserId(extras.getParcelable(EXTRA_URI));
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putBoolean(IS_SUBSCRIBED_KEY, mSubscribedUris.contains(uri));
|
||||
return bundle;
|
||||
}
|
||||
if (METHOD_IS_DESTROYED.equals(method)) {
|
||||
Uri uri = getUriWithoutUserId(extras.getParcelable(EXTRA_URI));
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putBoolean(IS_DESTROYED_KEY, mDestroyedUris.contains(uri));
|
||||
return bundle;
|
||||
}
|
||||
return super.call(method, arg, extras);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected QCItem onBind(@NonNull Uri uri) {
|
||||
List<String> pathSegments = uri.getPathSegments();
|
||||
String key = pathSegments.get(0);
|
||||
|
||||
if (KEY_DEFAULT.equals(key)) {
|
||||
return new QCTile.Builder()
|
||||
.setIcon(Icon.createWithResource(getContext(), R.drawable.btn_star))
|
||||
.build();
|
||||
} else if (KEY_SLOW.equals(key)) {
|
||||
// perform a slow operation that should trigger the strict thread policy
|
||||
Drawable d = getContext().getDrawable(R.drawable.btn_star);
|
||||
Bitmap bitmap = drawableToBitmap(d);
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
|
||||
byte[] b = baos.toByteArray();
|
||||
Icon icon = Icon.createWithData(b, 0, b.length);
|
||||
return new QCTile.Builder()
|
||||
.setIcon(icon)
|
||||
.build();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSubscribed(@NonNull Uri uri) {
|
||||
mSubscribedUris.add(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUnsubscribed(@NonNull Uri uri) {
|
||||
mSubscribedUris.remove(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy(@NonNull Uri uri) {
|
||||
mDestroyedUris.add(uri);
|
||||
}
|
||||
|
||||
private static Bitmap drawableToBitmap(Drawable drawable) {
|
||||
|
||||
if (drawable instanceof BitmapDrawable) {
|
||||
return ((BitmapDrawable) drawable).getBitmap();
|
||||
}
|
||||
|
||||
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
|
||||
drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||
drawable.draw(canvas);
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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.car.qc.view;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.testng.Assert.assertThrows;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import com.android.car.qc.QCList;
|
||||
import com.android.car.qc.QCRow;
|
||||
import com.android.dx.mockito.inline.extended.ExtendedMockito;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class QCListViewTest {
|
||||
|
||||
private final Context mContext = ApplicationProvider.getApplicationContext();
|
||||
private QCListView mView;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mView = new QCListView(mContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onChanged_null_noViews() {
|
||||
mView.onChanged(null);
|
||||
assertThat(mView.getChildCount()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onChanged_invalidType_throwsIllegalArgumentException() {
|
||||
QCRow row = new QCRow.Builder().build();
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> mView.onChanged(row));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onChanged_createsRows() {
|
||||
QCList list = new QCList.Builder()
|
||||
.addRow(new QCRow.Builder().build())
|
||||
.addRow(new QCRow.Builder().build())
|
||||
.build();
|
||||
mView.onChanged(list);
|
||||
assertThat(mView.getChildCount()).isEqualTo(2);
|
||||
assertThat(mView.getChildAt(0) instanceof QCRowView).isTrue();
|
||||
assertThat(mView.getChildAt(1) instanceof QCRowView).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onChanged_decreasedRowCount_removesExtraRows() {
|
||||
QCList list = new QCList.Builder()
|
||||
.addRow(new QCRow.Builder().build())
|
||||
.addRow(new QCRow.Builder().build())
|
||||
.build();
|
||||
mView.onChanged(list);
|
||||
assertThat(mView.getChildCount()).isEqualTo(2);
|
||||
list = new QCList.Builder()
|
||||
.addRow(new QCRow.Builder().build())
|
||||
.build();
|
||||
mView.onChanged(list);
|
||||
assertThat(mView.getChildCount()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setActionListener_setsOnChildView() {
|
||||
QCList list = new QCList.Builder()
|
||||
.addRow(new QCRow.Builder().build())
|
||||
.addRow(new QCRow.Builder().build())
|
||||
.build();
|
||||
mView.onChanged(list);
|
||||
assertThat(mView.getChildCount()).isEqualTo(2);
|
||||
QCRowView row1 = (QCRowView) mView.getChildAt(0);
|
||||
QCRowView row2 = (QCRowView) mView.getChildAt(1);
|
||||
ExtendedMockito.spyOn(row1);
|
||||
ExtendedMockito.spyOn(row2);
|
||||
QCView.QCActionListener listener = mock(QCView.QCActionListener.class);
|
||||
mView.setActionListener(listener);
|
||||
ExtendedMockito.verify(row1).setActionListener(listener);
|
||||
ExtendedMockito.verify(row2).setActionListener(listener);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* 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.car.qc.view;
|
||||
|
||||
import static com.android.car.qc.QCItem.QC_TYPE_ACTION_SWITCH;
|
||||
import static com.android.car.qc.QCItem.QC_TYPE_ACTION_TOGGLE;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.test.annotation.UiThreadTest;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import com.android.car.qc.QCActionItem;
|
||||
import com.android.car.qc.QCRow;
|
||||
import com.android.car.qc.QCSlider;
|
||||
import com.android.car.qc.R;
|
||||
import com.android.dx.mockito.inline.extended.ExtendedMockito;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class QCRowViewTest {
|
||||
|
||||
private final Context mContext = ApplicationProvider.getApplicationContext();
|
||||
private QCRowView mView;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mView = new QCRowView(mContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setRow_null_notVisible() {
|
||||
mView.setRow(null);
|
||||
assertThat(mView.getVisibility()).isEqualTo(View.GONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setRow_notNull_visible() {
|
||||
QCRow row = new QCRow.Builder().build();
|
||||
mView.setRow(row);
|
||||
assertThat(mView.getVisibility()).isEqualTo(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setRow_setsTitle() {
|
||||
String title = "TEST_TITLE";
|
||||
QCRow row = new QCRow.Builder().setTitle(title).build();
|
||||
mView.setRow(row);
|
||||
TextView titleView = mView.findViewById(R.id.qc_title);
|
||||
assertThat(titleView.getVisibility()).isEqualTo(View.VISIBLE);
|
||||
assertThat(titleView.getText().toString()).isEqualTo(title);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setRow_setsSubtitle() {
|
||||
String subtitle = "TEST_TITLE";
|
||||
QCRow row = new QCRow.Builder().setSubtitle(subtitle).build();
|
||||
mView.setRow(row);
|
||||
TextView subtitleView = mView.findViewById(R.id.qc_summary);
|
||||
assertThat(subtitleView.getVisibility()).isEqualTo(View.VISIBLE);
|
||||
assertThat(subtitleView.getText().toString()).isEqualTo(subtitle);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setRow_setsIcon() {
|
||||
Icon icon = Icon.createWithResource(mContext, android.R.drawable.btn_star);
|
||||
QCRow row = new QCRow.Builder().setIcon(icon).build();
|
||||
mView.setRow(row);
|
||||
ImageView iconView = mView.findViewById(R.id.qc_icon);
|
||||
assertThat(iconView.getVisibility()).isEqualTo(View.VISIBLE);
|
||||
assertThat(iconView.getDrawable()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void setRow_createsStartItems() {
|
||||
QCRow row = new QCRow.Builder()
|
||||
.addStartItem(new QCActionItem.Builder(QC_TYPE_ACTION_SWITCH).build())
|
||||
.addStartItem(new QCActionItem.Builder(QC_TYPE_ACTION_TOGGLE).build())
|
||||
.build();
|
||||
mView.setRow(row);
|
||||
LinearLayout startContainer = mView.findViewById(R.id.qc_row_start_items);
|
||||
assertThat(startContainer.getChildCount()).isEqualTo(2);
|
||||
assertThat((View) startContainer.getChildAt(0).findViewById(
|
||||
android.R.id.switch_widget)).isNotNull();
|
||||
assertThat((View) startContainer.getChildAt(1).findViewById(
|
||||
R.id.qc_toggle_button)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void setRow_createsEndItems() {
|
||||
QCRow row = new QCRow.Builder()
|
||||
.addEndItem(new QCActionItem.Builder(QC_TYPE_ACTION_SWITCH).build())
|
||||
.addEndItem(new QCActionItem.Builder(QC_TYPE_ACTION_TOGGLE).build())
|
||||
.build();
|
||||
mView.setRow(row);
|
||||
LinearLayout endContainer = mView.findViewById(R.id.qc_row_end_items);
|
||||
assertThat(endContainer.getChildCount()).isEqualTo(2);
|
||||
assertThat((View) endContainer.getChildAt(0).findViewById(
|
||||
android.R.id.switch_widget)).isNotNull();
|
||||
assertThat((View) endContainer.getChildAt(1).findViewById(
|
||||
R.id.qc_toggle_button)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setRow_noSlider_sliderViewNotVisible() {
|
||||
QCRow row = new QCRow.Builder().build();
|
||||
mView.setRow(row);
|
||||
LinearLayout sliderContainer = mView.findViewById(R.id.qc_seekbar_wrapper);
|
||||
assertThat(sliderContainer.getVisibility()).isEqualTo(View.GONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void setRow_hasSlider_sliderViewVisible() {
|
||||
QCRow row = new QCRow.Builder()
|
||||
.addSlider(new QCSlider.Builder().build())
|
||||
.build();
|
||||
mView.setRow(row);
|
||||
LinearLayout sliderContainer = mView.findViewById(R.id.qc_seekbar_wrapper);
|
||||
assertThat(sliderContainer.getVisibility()).isEqualTo(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onRowClick_firesAction() throws PendingIntent.CanceledException {
|
||||
PendingIntent action = mock(PendingIntent.class);
|
||||
QCRow row = new QCRow.Builder().setPrimaryAction(action).build();
|
||||
mView.setRow(row);
|
||||
mView.findViewById(R.id.qc_row_content).performClick();
|
||||
verify(action).send(any(Context.class), anyInt(), eq(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onSwitchClick_firesAction() throws PendingIntent.CanceledException {
|
||||
PendingIntent action = mock(PendingIntent.class);
|
||||
QCRow row = new QCRow.Builder()
|
||||
.addEndItem(
|
||||
new QCActionItem.Builder(QC_TYPE_ACTION_SWITCH).setAction(action).build())
|
||||
.build();
|
||||
mView.setRow(row);
|
||||
LinearLayout endContainer = mView.findViewById(R.id.qc_row_end_items);
|
||||
assertThat(endContainer.getChildCount()).isEqualTo(1);
|
||||
endContainer.getChildAt(0).performClick();
|
||||
verify(action).send(any(Context.class), anyInt(), any(Intent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onToggleClick_firesAction() throws PendingIntent.CanceledException {
|
||||
PendingIntent action = mock(PendingIntent.class);
|
||||
QCRow row = new QCRow.Builder()
|
||||
.addEndItem(
|
||||
new QCActionItem.Builder(QC_TYPE_ACTION_TOGGLE).setAction(action).build())
|
||||
.build();
|
||||
mView.setRow(row);
|
||||
LinearLayout endContainer = mView.findViewById(R.id.qc_row_end_items);
|
||||
assertThat(endContainer.getChildCount()).isEqualTo(1);
|
||||
endContainer.getChildAt(0).performClick();
|
||||
verify(action).send(any(Context.class), anyInt(), any(Intent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onSliderChange_firesAction() throws PendingIntent.CanceledException {
|
||||
PendingIntent action = mock(PendingIntent.class);
|
||||
QCRow row = new QCRow.Builder()
|
||||
.addSlider(new QCSlider.Builder().setInputAction(action).build())
|
||||
.build();
|
||||
mView.setRow(row);
|
||||
SeekBar seekBar = mView.findViewById(R.id.qc_seekbar);
|
||||
seekBar.setProgress(50);
|
||||
MotionEvent motionEvent = ExtendedMockito.mock(MotionEvent.class);
|
||||
ExtendedMockito.when(motionEvent.getAction()).thenReturn(MotionEvent.ACTION_UP);
|
||||
seekBar.onTouchEvent(motionEvent);
|
||||
verify(action).send(any(Context.class), anyInt(), any(Intent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void setRow_switchViewThumbTintList() {
|
||||
PendingIntent action = mock(PendingIntent.class);
|
||||
QCRow row = new QCRow.Builder()
|
||||
.addEndItem(
|
||||
new QCActionItem.Builder(QC_TYPE_ACTION_SWITCH).setAction(action).build())
|
||||
.build();
|
||||
mView.setRow(row);
|
||||
LinearLayout endContainer = mView.findViewById(R.id.qc_row_end_items);
|
||||
assertThat(endContainer.getChildCount()).isEqualTo(1);
|
||||
Switch switchView = (Switch) endContainer.getChildAt(0);
|
||||
assertThat(switchView.getThumbTintList()).isNotNull();
|
||||
|
||||
ColorStateList switchColorStateList = switchView.getThumbTintList();
|
||||
int[] enabledState = {android.R.attr.state_enabled};
|
||||
int[] disabledState = {-android.R.attr.state_enabled};
|
||||
assertThat(switchColorStateList.getColorForState(enabledState, 0)).isNotEqualTo(
|
||||
switchColorStateList.getColorForState(disabledState, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void setRow_sliderViewThumbTintList() {
|
||||
PendingIntent action = mock(PendingIntent.class);
|
||||
QCRow row = new QCRow.Builder()
|
||||
.addSlider(new QCSlider.Builder().setInputAction(action).build())
|
||||
.build();
|
||||
mView.setRow(row);
|
||||
SeekBar seekBar = mView.findViewById(R.id.qc_seekbar);
|
||||
assertThat(seekBar.getThumbTintList()).isNotNull();
|
||||
|
||||
ColorStateList seekBarColorStateList = seekBar.getThumbTintList();
|
||||
int[] enabledState = {android.R.attr.state_enabled};
|
||||
int[] disabledState = {-android.R.attr.state_enabled};
|
||||
assertThat(seekBarColorStateList.getColorForState(enabledState, 0)).isNotEqualTo(
|
||||
seekBarColorStateList.getColorForState(disabledState, 0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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.car.qc.view;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.MotionEvent;
|
||||
import android.widget.SeekBar;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class QCSeekBarViewTest {
|
||||
private final Context mContext = ApplicationProvider.getApplicationContext();
|
||||
private final QCSeekBarView mView = new QCSeekBarView(mContext);
|
||||
|
||||
@Mock
|
||||
private MotionEvent mMotionEvent;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_UP);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void enabled_standardTouchEvent() {
|
||||
assertThat(mView.onTouchEvent(mMotionEvent)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disabled_standardTouchEvent() {
|
||||
mView.setEnabled(false);
|
||||
|
||||
assertThat(mView.onTouchEvent(mMotionEvent)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clickableWhileDisabled_customTouchEvent() {
|
||||
mView.setEnabled(false);
|
||||
mView.setClickableWhileDisabled(true);
|
||||
|
||||
assertThat(mView.onTouchEvent(mMotionEvent)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clickableWhileDisabled_actionDown_doesNotTriggerDisabledClickListener() {
|
||||
AtomicBoolean called = new AtomicBoolean(false);
|
||||
Consumer<SeekBar> disabledClickListener = seekBar -> called.set(true);
|
||||
mView.setEnabled(false);
|
||||
mView.setClickableWhileDisabled(true);
|
||||
mView.setDisabledClickListener(disabledClickListener);
|
||||
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_DOWN);
|
||||
|
||||
assertThat(mView.onTouchEvent(mMotionEvent)).isTrue();
|
||||
assertThat(called.get()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clickableWhileDisabled_actionUp_triggersDisabledClickListener() {
|
||||
AtomicBoolean called = new AtomicBoolean(false);
|
||||
Consumer<SeekBar> disabledClickListener = seekBar -> called.set(true);
|
||||
mView.setEnabled(false);
|
||||
mView.setClickableWhileDisabled(true);
|
||||
mView.setDisabledClickListener(disabledClickListener);
|
||||
|
||||
assertThat(mView.onTouchEvent(mMotionEvent)).isTrue();
|
||||
assertThat(called.get()).isTrue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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.car.qc.view;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.testng.Assert.assertThrows;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.test.annotation.UiThreadTest;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import com.android.car.qc.QCRow;
|
||||
import com.android.car.qc.QCTile;
|
||||
import com.android.car.qc.R;
|
||||
import com.android.car.ui.uxr.DrawableStateToggleButton;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class QCTileViewTest {
|
||||
|
||||
private final Context mContext = ApplicationProvider.getApplicationContext();
|
||||
private QCTileView mView;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mView = new QCTileView(mContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onChanged_null_noViews() {
|
||||
mView.onChanged(null);
|
||||
assertThat(mView.getChildCount()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onChanged_invalidType_throwsIllegalArgumentException() {
|
||||
QCRow row = new QCRow.Builder().build();
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> mView.onChanged(row));
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onChanged_setsSubtitleView() {
|
||||
String subtitle = "TEST_SUBTITLE";
|
||||
QCTile tile = new QCTile.Builder().setSubtitle(subtitle).build();
|
||||
mView.onChanged(tile);
|
||||
TextView subtitleView = mView.findViewById(android.R.id.summary);
|
||||
assertThat(subtitleView.getText().toString()).isEqualTo(subtitle);
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onChanged_setsButtonState() {
|
||||
QCTile tile = new QCTile.Builder().setChecked(true).setEnabled(true).build();
|
||||
mView.onChanged(tile);
|
||||
DrawableStateToggleButton button = mView.findViewById(R.id.qc_tile_toggle_button);
|
||||
assertThat(button.isEnabled()).isTrue();
|
||||
assertThat(button.isChecked()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onChanged_setsIcon() {
|
||||
Icon icon = Icon.createWithResource(mContext, android.R.drawable.btn_star);
|
||||
QCTile tile = new QCTile.Builder().setIcon(icon).build();
|
||||
mView.onChanged(tile);
|
||||
DrawableStateToggleButton button = mView.findViewById(R.id.qc_tile_toggle_button);
|
||||
Drawable buttonDrawable = button.getButtonDrawable();
|
||||
assertThat(buttonDrawable).isNotNull();
|
||||
assertThat(buttonDrawable instanceof LayerDrawable).isTrue();
|
||||
assertThat(((LayerDrawable) buttonDrawable).getNumberOfLayers()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onClick_firesAction() throws PendingIntent.CanceledException {
|
||||
PendingIntent action = mock(PendingIntent.class);
|
||||
QCTile tile = new QCTile.Builder().setChecked(false).setAction(action).build();
|
||||
mView.onChanged(tile);
|
||||
mView.findViewById(R.id.qc_tile_wrapper).performClick();
|
||||
DrawableStateToggleButton button = mView.findViewById(R.id.qc_tile_toggle_button);
|
||||
assertThat(button.isChecked()).isTrue();
|
||||
verify(action).send(any(Context.class), anyInt(), any(Intent.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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.car.qc.view;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.testng.Assert.assertThrows;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.annotation.UiThreadTest;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import com.android.car.qc.QCList;
|
||||
import com.android.car.qc.QCRow;
|
||||
import com.android.car.qc.QCTile;
|
||||
import com.android.dx.mockito.inline.extended.ExtendedMockito;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class QCViewTest {
|
||||
|
||||
private final Context mContext = ApplicationProvider.getApplicationContext();
|
||||
private QCView mView;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mView = new QCView(mContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onChanged_null_noViews() {
|
||||
mView.onChanged(null);
|
||||
assertThat(mView.getChildCount()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onChanged_invalidType_throwsIllegalArgumentException() {
|
||||
QCRow row = new QCRow.Builder().build();
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> mView.onChanged(row));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onChanged_list_createsListView() {
|
||||
QCList list = new QCList.Builder().build();
|
||||
mView.onChanged(list);
|
||||
assertThat(mView.getChildCount()).isEqualTo(1);
|
||||
assertThat(mView.getChildAt(0) instanceof QCListView).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onChanged_tile_createsTileView() {
|
||||
QCTile tile = new QCTile.Builder().build();
|
||||
mView.onChanged(tile);
|
||||
assertThat(mView.getChildCount()).isEqualTo(1);
|
||||
assertThat(mView.getChildAt(0) instanceof QCTileView).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onChanged_alreadyHasView_callsOnChanged() {
|
||||
QCTile tile = new QCTile.Builder().build();
|
||||
mView.onChanged(tile);
|
||||
assertThat(mView.getChildCount()).isEqualTo(1);
|
||||
assertThat(mView.getChildAt(0) instanceof QCTileView).isTrue();
|
||||
QCTileView tileView = (QCTileView) mView.getChildAt(0);
|
||||
ExtendedMockito.spyOn(tileView);
|
||||
mView.onChanged(tile);
|
||||
verify(tileView).onChanged(tile);
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void setActionListener_setsOnChildView() {
|
||||
QCTile tile = new QCTile.Builder().build();
|
||||
mView.onChanged(tile);
|
||||
assertThat(mView.getChildCount()).isEqualTo(1);
|
||||
assertThat(mView.getChildAt(0) instanceof QCTileView).isTrue();
|
||||
QCTileView tileView = (QCTileView) mView.getChildAt(0);
|
||||
ExtendedMockito.spyOn(tileView);
|
||||
QCView.QCActionListener listener = mock(QCView.QCActionListener.class);
|
||||
mView.setActionListener(listener);
|
||||
ExtendedMockito.verify(tileView).setActionListener(listener);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user