Merge changes from topic "dp2_ffwd" into pi-preview1-androidx-dev
* changes:
DO NOT MERGE Partial revert of "Fix breakages in SDK drop"
Merge remote-tracking branch 'goog/pi-androidx-dev' into merge_for_io
diff --git a/adding-support-library-as-included-build.md b/adding-support-library-as-included-build.md
index 136929f..b77233e 100644
--- a/adding-support-library-as-included-build.md
+++ b/adding-support-library-as-included-build.md
@@ -1,5 +1,8 @@
# Adding the Support Library Build Within Another Build
+Sorry, this doesn't seem to be working at the moment.
+For now, run `./gradlew createArchive` and copy the output to where your project can use it, as described fuller in go/support-dev
+
Would you like to make a change in Support Library and have it be propagated to
your downstream Gradle build (generally an app) without having to separately
build Support Library and then build your application?
diff --git a/app-toolkit/common/src/main/java/androidx/arch/core/internal/SafeIterableMap.java b/app-toolkit/common/src/main/java/androidx/arch/core/internal/SafeIterableMap.java
index bda1259..8f3d7ce 100644
--- a/app-toolkit/common/src/main/java/androidx/arch/core/internal/SafeIterableMap.java
+++ b/app-toolkit/common/src/main/java/androidx/arch/core/internal/SafeIterableMap.java
@@ -240,6 +240,7 @@
return mNext != null;
}
+ @SuppressWarnings("ReferenceEquality")
@Override
public void supportRemove(@NonNull Entry<K, V> entry) {
if (mExpectedEnd == entry && entry == mNext) {
@@ -256,6 +257,7 @@
}
}
+ @SuppressWarnings("ReferenceEquality")
private Entry<K, V> nextNode() {
if (mNext == mExpectedEnd || mExpectedEnd == null) {
return null;
@@ -312,6 +314,7 @@
private Entry<K, V> mCurrent;
private boolean mBeforeStart = true;
+ @SuppressWarnings("ReferenceEquality")
@Override
public void supportRemove(@NonNull Entry<K, V> entry) {
if (entry == mCurrent) {
@@ -379,6 +382,7 @@
return mKey + "=" + mValue;
}
+ @SuppressWarnings("ReferenceEquality")
@Override
public boolean equals(Object obj) {
if (obj == this) {
diff --git a/app-toolkit/settings.gradle b/app-toolkit/settings.gradle
index 89f6388..ad2350a 100644
--- a/app-toolkit/settings.gradle
+++ b/app-toolkit/settings.gradle
@@ -60,9 +60,11 @@
includeProject(":lifecycle:lifecycle-livedata", new File(supportRoot, "lifecycle/livedata"))
includeProject(":lifecycle:lifecycle-process", new File(supportRoot, "lifecycle/process"))
includeProject(":lifecycle:lifecycle-reactivestreams", new File(supportRoot, "lifecycle/reactivestreams"))
+includeProject(":lifecycle:lifecycle-reactivestreams-ktx", new File(supportRoot, "lifecycle/reactivestreams/ktx"))
includeProject(":lifecycle:lifecycle-runtime", new File(supportRoot, "lifecycle/runtime"))
includeProject(":lifecycle:lifecycle-service", new File(supportRoot, "lifecycle/service"))
includeProject(":lifecycle:lifecycle-viewmodel", new File(supportRoot, "lifecycle/viewmodel"))
+includeProject(":lifecycle:lifecycle-viewmodel-ktx", new File(supportRoot, "lifecycle/viewmodel/ktx"))
includeProject(":paging:integration-tests:testapp", new File(supportRoot, "paging/integration-tests/testapp"))
includeProject(":paging:paging-common", new File(supportRoot, "paging/common"))
includeProject(":paging:paging-runtime", new File(supportRoot, "paging/runtime"))
@@ -78,6 +80,7 @@
includeProject(":room:room-testing", new File(supportRoot, "room/testing"))
includeProject(":sqlite:sqlite", new File(supportRoot, "persistence/db"))
includeProject(":sqlite:sqlite-framework", new File(supportRoot, "persistence/db-framework"))
+includeProject(":sqlite:sqlite-ktx", new File(supportRoot, "persistence/db/ktx"))
includeProject(":jetifier-core", new File(supportRoot, "jetifier/jetifier/core"))
includeProject(":jetifier-processor", new File(supportRoot, "jetifier/jetifier/processor"))
diff --git a/buildSrc/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt b/buildSrc/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
index be10aa6..7b0887d 100644
--- a/buildSrc/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
@@ -45,6 +45,8 @@
"-Xep:JavaLangClash:ERROR",
"-Xep:PrivateConstructorForUtilityClass:ERROR",
"-Xep:TypeParameterUnusedInFormals:ERROR",
+ "-Xep:StringSplitter:ERROR",
+ "-Xep:ReferenceEquality:ERROR",
// Nullaway
"-XepIgnoreUnknownCheckNames", // https://github.com/uber/NullAway/issues/25
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt
index 15c6f42..e85acec 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt
@@ -47,7 +47,6 @@
const val LOCALBROADCASTMANAGER = "androidx.localbroadcastmanager"
const val MEDIA = "androidx.media"
const val MEDIAROUTER = "androidx.mediarouter"
- const val MEDIAWIDGET = "androidx.mediawidget"
const val PALETTE = "androidx.palette"
const val PERCENTLAYOUT = "androidx.percentlayout"
const val PREFERENCE = "androidx.preference"
diff --git a/buildSrc/src/main/kotlin/androidx/build/SupportJavaLibraryPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/SupportJavaLibraryPlugin.kt
index e8bd816..cee2955 100644
--- a/buildSrc/src/main/kotlin/androidx/build/SupportJavaLibraryPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/SupportJavaLibraryPlugin.kt
@@ -23,6 +23,7 @@
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.JavaPluginConvention
+import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.compile.JavaCompile
/**
@@ -47,6 +48,11 @@
convention.targetCompatibility = JavaVersion.VERSION_1_7
}
DiffAndDocs.registerJavaProject(project, supportLibraryExtension)
+
+ project.tasks.withType(Jar::class.java) { jarTask ->
+ jarTask.setReproducibleFileOrder(true)
+ jarTask.setPreserveFileTimestamps(false)
+ }
}
project.apply(mapOf("plugin" to ErrorProneBasePlugin::class.java))
diff --git a/buildSrc/src/main/kotlin/androidx/build/SupportKotlinLibraryPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/SupportKotlinLibraryPlugin.kt
index d2a6ddb..9558f76 100644
--- a/buildSrc/src/main/kotlin/androidx/build/SupportKotlinLibraryPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/SupportKotlinLibraryPlugin.kt
@@ -20,6 +20,7 @@
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.JavaPluginConvention
+import org.gradle.api.tasks.bundling.Jar
class SupportKotlinLibraryPlugin : Plugin<Project> {
override fun apply(project: Project) {
@@ -38,6 +39,12 @@
convention.sourceCompatibility = JavaVersion.VERSION_1_7
convention.targetCompatibility = JavaVersion.VERSION_1_7
}
+
+ project.tasks.withType(Jar::class.java) { jarTask ->
+ jarTask.setReproducibleFileOrder(true)
+ jarTask.setPreserveFileTimestamps(false)
+ }
+
}
CheckExternalDependencyLicensesTask.configure(project)
diff --git a/car/api/current.txt b/car/api/current.txt
index d643b33..1396dc5 100644
--- a/car/api/current.txt
+++ b/car/api/current.txt
@@ -378,19 +378,25 @@
}
public class SeekbarListItem extends androidx.car.widget.ListItem {
- ctor public SeekbarListItem(android.content.Context, int, int, android.widget.SeekBar.OnSeekBarChangeListener, java.lang.String);
+ ctor public deprecated SeekbarListItem(android.content.Context, int, int, android.widget.SeekBar.OnSeekBarChangeListener, java.lang.String);
+ ctor public SeekbarListItem(android.content.Context);
method public static androidx.car.widget.SeekbarListItem.ViewHolder createViewHolder(android.view.View);
method public int getViewType();
method protected void onBind(androidx.car.widget.SeekbarListItem.ViewHolder);
method protected void resolveDirtyState();
+ method public void setMax(int);
+ method public void setOnSeekBarChangeListener(android.widget.SeekBar.OnSeekBarChangeListener);
method public void setPrimaryActionEmptyIcon();
method public void setPrimaryActionIcon(int);
method public void setPrimaryActionIcon(android.graphics.drawable.Drawable);
+ method public void setProgress(int);
+ method public void setSecondaryProgress(int);
method public void setSupplementalEmptyIcon(boolean);
method public void setSupplementalIcon(int, boolean);
method public void setSupplementalIcon(int, boolean, android.view.View.OnClickListener);
method public void setSupplementalIcon(android.graphics.drawable.Drawable, boolean);
method public void setSupplementalIcon(android.graphics.drawable.Drawable, boolean, android.view.View.OnClickListener);
+ method public void setText(java.lang.String);
}
public static class SeekbarListItem.ViewHolder extends androidx.car.widget.ListItem.ViewHolder {
diff --git a/car/build.gradle b/car/build.gradle
index 8139c8c..d2e9be6 100644
--- a/car/build.gradle
+++ b/car/build.gradle
@@ -9,7 +9,8 @@
dependencies {
api project(':appcompat')
api project(':cardview')
- api "com.android.temp.support:design-widget:28.0.0-alpha1", {
+ api "com.android.temp.support:design-widget:28.0.0-alpha1@aar", {
+ transitive = true
exclude group: 'androidx.annotation'
exclude group: 'androidx.core'
exclude group: 'androidx.legacy'
@@ -47,7 +48,7 @@
supportLibrary {
name = "Android Car Support UI"
- publish = false
+ publish = true
mavenVersion = LibraryVersions.SUPPORT_LIBRARY
mavenGroup = LibraryGroups.CAR
inceptionYear = "2017"
diff --git a/car/res/drawable/ic_nav_arrow_back.xml b/car/res/drawable/ic_nav_arrow_back.xml
new file mode 100644
index 0000000..574e816
--- /dev/null
+++ b/car/res/drawable/ic_nav_arrow_back.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+
+ <path
+ android:pathData="M0 0h48v48H0z" />
+ <path
+ android:fillColor="#000000"
+ android:pathData="M40 22H15.66l11.17-11.17L24 8 8 24l16 16 2.83-2.83L15.66 26H40v-4z" />
+</vector>
+
diff --git a/car/res/layout/car_list_item_seekbar_content.xml b/car/res/layout/car_list_item_seekbar_content.xml
index cd68f3c..7fae5c1 100644
--- a/car/res/layout/car_list_item_seekbar_content.xml
+++ b/car/res/layout/car_list_item_seekbar_content.xml
@@ -47,6 +47,7 @@
android:layout_height="wrap_content"
android:paddingTop="@dimen/car_seekbar_padding"
android:paddingBottom="@dimen/car_seekbar_padding"
+ android:min="0"
android:splitTrack="false"/>
</LinearLayout>
diff --git a/car/res/values-af/strings.xml b/car/res/values-af/strings.xml
index f307fc7..1e90b40 100644
--- a/car/res/values-af/strings.xml
+++ b/car/res/values-af/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Konsentreer op die pad"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Vou knoppie in/uit"</string>
</resources>
diff --git a/car/res/values-am/strings.xml b/car/res/values-am/strings.xml
index 30ae48e..6759ee3 100644
--- a/car/res/values-am/strings.xml
+++ b/car/res/values-am/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"መንገዱ ላይ ያተኩሩ"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"አዝራርን ዘርጋ/ሰብስብ"</string>
</resources>
diff --git a/car/res/values-ar/strings.xml b/car/res/values-ar/strings.xml
index e970ed9..845908b 100644
--- a/car/res/values-ar/strings.xml
+++ b/car/res/values-ar/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"ركِّز في الطريق"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"زر التوسيع/التصغير"</string>
</resources>
diff --git a/car/res/values-as/strings.xml b/car/res/values-as/strings.xml
new file mode 100644
index 0000000..e5a0015
--- /dev/null
+++ b/car/res/values-as/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="speed_bump_lockout_message" msgid="5405697774899378511">"ৰাষ্টাত মনোযোগ দিয়ক"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"সম্প্ৰসাৰণ/সংকোচন বুটাম"</string>
+</resources>
diff --git a/car/res/values-az/strings.xml b/car/res/values-az/strings.xml
index e813849..b5e25dc 100644
--- a/car/res/values-az/strings.xml
+++ b/car/res/values-az/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Diqqətinizi yola yönəldin"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Düyməni genişləndirin/yığcamlaşdırın"</string>
</resources>
diff --git a/car/res/values-b+sr+Latn/strings.xml b/car/res/values-b+sr+Latn/strings.xml
index a83a82c..43dbac2 100644
--- a/car/res/values-b+sr+Latn/strings.xml
+++ b/car/res/values-b+sr+Latn/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Fokusirajte se na put"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Dugme Proširi/skupi"</string>
</resources>
diff --git a/car/res/values-be/strings.xml b/car/res/values-be/strings.xml
index 80912e4..cfef873 100644
--- a/car/res/values-be/strings.xml
+++ b/car/res/values-be/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Увага на дарогу"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Кнопка \"Разгарнуць/згарнуць\""</string>
</resources>
diff --git a/car/res/values-bg/strings.xml b/car/res/values-bg/strings.xml
index dd5811f..49e7a61 100644
--- a/car/res/values-bg/strings.xml
+++ b/car/res/values-bg/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Съсредоточете се върху пътя"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Бутон за разгъване/свиване"</string>
</resources>
diff --git a/car/res/values-bn/strings.xml b/car/res/values-bn/strings.xml
index fcf0165..4ab2ca4 100644
--- a/car/res/values-bn/strings.xml
+++ b/car/res/values-bn/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"মনোযোগ দিয়ে গাড়ি চালান"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"বোতাম বড় করুন/আড়াল করুন"</string>
</resources>
diff --git a/car/res/values-bs/strings.xml b/car/res/values-bs/strings.xml
index 99e655e..6ff5714 100644
--- a/car/res/values-bs/strings.xml
+++ b/car/res/values-bs/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Fokusirajte se na cestu"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Dugme proširi/suzi"</string>
</resources>
diff --git a/car/res/values-ca/strings.xml b/car/res/values-ca/strings.xml
index 758f6e7..8e20fd8 100644
--- a/car/res/values-ca/strings.xml
+++ b/car/res/values-ca/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Concentra\'t en la carretera"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Botó per desplegar o replegar"</string>
</resources>
diff --git a/car/res/values-cs/strings.xml b/car/res/values-cs/strings.xml
index 27090ef..1b4cad2 100644
--- a/car/res/values-cs/strings.xml
+++ b/car/res/values-cs/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Soustřeďte se na silnici"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Tlačítko rozbalení/sbalení"</string>
</resources>
diff --git a/car/res/values-da/strings.xml b/car/res/values-da/strings.xml
index 96cc062..4a0e180 100644
--- a/car/res/values-da/strings.xml
+++ b/car/res/values-da/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Hold øjnene på vejen"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Knappen Udvid/skjul"</string>
</resources>
diff --git a/car/res/values-de/strings.xml b/car/res/values-de/strings.xml
index ee289ac..f670bd6 100644
--- a/car/res/values-de/strings.xml
+++ b/car/res/values-de/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Achte auf den Verkehr"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Schaltfläche zum Maximieren/Minimieren"</string>
</resources>
diff --git a/car/res/values-el/strings.xml b/car/res/values-el/strings.xml
index cf8ab8f..4010875 100644
--- a/car/res/values-el/strings.xml
+++ b/car/res/values-el/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Επικεντρωθείτε στον δρόμο"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Κουμπί ανάπτυξης/σύμπτυξης"</string>
</resources>
diff --git a/car/res/values-en-rAU/strings.xml b/car/res/values-en-rAU/strings.xml
index 8cc3360..a307f7e 100644
--- a/car/res/values-en-rAU/strings.xml
+++ b/car/res/values-en-rAU/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Focus on the road"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Expand/collapse button"</string>
</resources>
diff --git a/car/res/values-en-rCA/strings.xml b/car/res/values-en-rCA/strings.xml
index 8cc3360..a307f7e 100644
--- a/car/res/values-en-rCA/strings.xml
+++ b/car/res/values-en-rCA/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Focus on the road"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Expand/collapse button"</string>
</resources>
diff --git a/car/res/values-en-rGB/strings.xml b/car/res/values-en-rGB/strings.xml
index 8cc3360..a307f7e 100644
--- a/car/res/values-en-rGB/strings.xml
+++ b/car/res/values-en-rGB/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Focus on the road"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Expand/collapse button"</string>
</resources>
diff --git a/car/res/values-en-rIN/strings.xml b/car/res/values-en-rIN/strings.xml
index 8cc3360..a307f7e 100644
--- a/car/res/values-en-rIN/strings.xml
+++ b/car/res/values-en-rIN/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Focus on the road"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Expand/collapse button"</string>
</resources>
diff --git a/car/res/values-en-rXC/strings.xml b/car/res/values-en-rXC/strings.xml
index c11c1cb..16d0daf 100644
--- a/car/res/values-en-rXC/strings.xml
+++ b/car/res/values-en-rXC/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Focus on the road"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Expand/collapse button"</string>
</resources>
diff --git a/car/res/values-es-rUS/strings.xml b/car/res/values-es-rUS/strings.xml
index 6ac20d7..46a5e11 100644
--- a/car/res/values-es-rUS/strings.xml
+++ b/car/res/values-es-rUS/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Concéntrate en el camino"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Botón Expandir/contraer"</string>
</resources>
diff --git a/car/res/values-es/strings.xml b/car/res/values-es/strings.xml
index 09a493f..33ed10b 100644
--- a/car/res/values-es/strings.xml
+++ b/car/res/values-es/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Céntrate en la carretera"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Botón para mostrar u ocultar"</string>
</resources>
diff --git a/car/res/values-et/strings.xml b/car/res/values-et/strings.xml
index 66f6e48..7561817 100644
--- a/car/res/values-et/strings.xml
+++ b/car/res/values-et/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Keskenduge teele"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Nupp Laienda/Ahenda"</string>
</resources>
diff --git a/car/res/values-eu/strings.xml b/car/res/values-eu/strings.xml
index 3773f71..0e92575 100644
--- a/car/res/values-eu/strings.xml
+++ b/car/res/values-eu/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Jarri arreta errepidean"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Zabaltzeko/Tolesteko botoia"</string>
</resources>
diff --git a/car/res/values-fa/strings.xml b/car/res/values-fa/strings.xml
index 8668d6b..59152f0 100644
--- a/car/res/values-fa/strings.xml
+++ b/car/res/values-fa/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"روی جاده تمرکز داشته باشید"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"دکمه بزرگ کردن/کوچک کردن"</string>
</resources>
diff --git a/car/res/values-fi/strings.xml b/car/res/values-fi/strings.xml
index e93cb9c..5bbb440 100644
--- a/car/res/values-fi/strings.xml
+++ b/car/res/values-fi/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Pidä katse tiessä"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Laajennus- ja tiivistyspainike"</string>
</resources>
diff --git a/car/res/values-fr-rCA/strings.xml b/car/res/values-fr-rCA/strings.xml
index f32315b..e90ddf2 100644
--- a/car/res/values-fr-rCA/strings.xml
+++ b/car/res/values-fr-rCA/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Concentrez-vous sur la route"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Bouton Développer/Réduire"</string>
</resources>
diff --git a/car/res/values-fr/strings.xml b/car/res/values-fr/strings.xml
index f32315b..e90ddf2 100644
--- a/car/res/values-fr/strings.xml
+++ b/car/res/values-fr/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Concentrez-vous sur la route"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Bouton Développer/Réduire"</string>
</resources>
diff --git a/car/res/values-gl/strings.xml b/car/res/values-gl/strings.xml
index 42bc515..c89cc35 100644
--- a/car/res/values-gl/strings.xml
+++ b/car/res/values-gl/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Céntrate na estrada"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Botón despregar/contraer"</string>
</resources>
diff --git a/car/res/values-gu/strings.xml b/car/res/values-gu/strings.xml
index f215ec2..4397f5d 100644
--- a/car/res/values-gu/strings.xml
+++ b/car/res/values-gu/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"રસ્તા પર ફોકસ કરો"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"વિસ્તાર કરો/સંકુચિત કરો બટન"</string>
</resources>
diff --git a/car/res/values-hi/strings.xml b/car/res/values-hi/strings.xml
index 53f8a8d..08d973b 100644
--- a/car/res/values-hi/strings.xml
+++ b/car/res/values-hi/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"सड़क पर ध्यान दें"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"बड़ा/छोटा करने वाला बटन"</string>
</resources>
diff --git a/car/res/values-hr/strings.xml b/car/res/values-hr/strings.xml
index 0a08dcc..5714327 100644
--- a/car/res/values-hr/strings.xml
+++ b/car/res/values-hr/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Usredotočite se na cestu"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Gumb za proširivanje/sažimanje"</string>
</resources>
diff --git a/car/res/values-hu/strings.xml b/car/res/values-hu/strings.xml
index 3618719..88c2577 100644
--- a/car/res/values-hu/strings.xml
+++ b/car/res/values-hu/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Figyeljen az útra"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Gomb kibontása/összecsukása"</string>
</resources>
diff --git a/car/res/values-hy/strings.xml b/car/res/values-hy/strings.xml
index 3a85b41..d97cd0a 100644
--- a/car/res/values-hy/strings.xml
+++ b/car/res/values-hy/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Հետևեք ճանապարհին"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"«Ծավալել/ծալել» կոճակ"</string>
</resources>
diff --git a/car/res/values-in/strings.xml b/car/res/values-in/strings.xml
index 81daf29..faf2f43 100644
--- a/car/res/values-in/strings.xml
+++ b/car/res/values-in/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Konsentrasi saat mengemudi"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Tombol luaskan/ciutkan"</string>
</resources>
diff --git a/car/res/values-is/strings.xml b/car/res/values-is/strings.xml
index 4b5d26d..7c2dd76 100644
--- a/car/res/values-is/strings.xml
+++ b/car/res/values-is/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Einbeittu þér að akstrinum"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Hnappur til að stækka/minnka"</string>
</resources>
diff --git a/car/res/values-it/strings.xml b/car/res/values-it/strings.xml
index b395409..d2b3f4f 100644
--- a/car/res/values-it/strings.xml
+++ b/car/res/values-it/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Concentrati sulla strada"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Pulsante Espandi/Comprimi"</string>
</resources>
diff --git a/car/res/values-iw/strings.xml b/car/res/values-iw/strings.xml
index ab92be5..79bad9c 100644
--- a/car/res/values-iw/strings.xml
+++ b/car/res/values-iw/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"עליך להתמקד בכביש"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"לחצן הרחבה וכיווץ"</string>
</resources>
diff --git a/car/res/values-ja/strings.xml b/car/res/values-ja/strings.xml
index 89fce3b..87deba0 100644
--- a/car/res/values-ja/strings.xml
+++ b/car/res/values-ja/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"運転に集中してください"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"展開 / 折りたたみボタン"</string>
</resources>
diff --git a/car/res/values-ka/strings.xml b/car/res/values-ka/strings.xml
index e3f2e07..b525a9b 100644
--- a/car/res/values-ka/strings.xml
+++ b/car/res/values-ka/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"კონცენტრირდით გზაზე"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"ღილაკის გაფართოება/ჩაკეცვა"</string>
</resources>
diff --git a/car/res/values-kk/strings.xml b/car/res/values-kk/strings.xml
index bbccd56..ff327d3 100644
--- a/car/res/values-kk/strings.xml
+++ b/car/res/values-kk/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Жолға назар аударыңыз"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"\"Жаю/Жию\" түймесі"</string>
</resources>
diff --git a/car/res/values-km/strings.xml b/car/res/values-km/strings.xml
index 50fe2db..f9d9111 100644
--- a/car/res/values-km/strings.xml
+++ b/car/res/values-km/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"ផ្តោតលើការបើកបរ"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"ប៊ូតុងពង្រីក/បង្រួម"</string>
</resources>
diff --git a/car/res/values-kn/strings.xml b/car/res/values-kn/strings.xml
index 6562ee2..50ba985 100644
--- a/car/res/values-kn/strings.xml
+++ b/car/res/values-kn/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"ರಸ್ತೆಯ ಮೇಲೆ ಗಮನಹರಿಸಿ"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"ವಿಸ್ತರಿಸಿ/ಕುಗ್ಗಿಸಿ ಬಟನ್"</string>
</resources>
diff --git a/car/res/values-ko/strings.xml b/car/res/values-ko/strings.xml
index ac5865a..0081b5c 100644
--- a/car/res/values-ko/strings.xml
+++ b/car/res/values-ko/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"도로 상황에 집중하세요."</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"펼치기/접기 버튼"</string>
</resources>
diff --git a/car/res/values-ky/strings.xml b/car/res/values-ky/strings.xml
index 3640239..8a752fa 100644
--- a/car/res/values-ky/strings.xml
+++ b/car/res/values-ky/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Жолго көңүл буруңуз"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Жайып көрсөтүү/жыйыштыруу баскычы"</string>
</resources>
diff --git a/car/res/values-lo/strings.xml b/car/res/values-lo/strings.xml
index 4af3152..02afe60 100644
--- a/car/res/values-lo/strings.xml
+++ b/car/res/values-lo/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"ຕັ້ງໃຈຂັບລົດ"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"ປຸ່ມຫຍໍ້/ຂະຫຍາຍ"</string>
</resources>
diff --git a/car/res/values-lt/strings.xml b/car/res/values-lt/strings.xml
index 685bbe5..5281e18 100644
--- a/car/res/values-lt/strings.xml
+++ b/car/res/values-lt/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Sutelkite dėmesį į kelią"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Mygtukas „Išskleisti / sutraukti“"</string>
</resources>
diff --git a/car/res/values-lv/strings.xml b/car/res/values-lv/strings.xml
index 417d331..7b4372b 100644
--- a/car/res/values-lv/strings.xml
+++ b/car/res/values-lv/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Pievērsieties autovadīšanai"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Izvēršanas/sakļaušanas poga"</string>
</resources>
diff --git a/car/res/values-mk/strings.xml b/car/res/values-mk/strings.xml
index 7377299..5184503 100644
--- a/car/res/values-mk/strings.xml
+++ b/car/res/values-mk/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Фокусирајте се на патот"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Копче за проширување/собирање"</string>
</resources>
diff --git a/car/res/values-ml/strings.xml b/car/res/values-ml/strings.xml
index d5ad91d..8047b4c 100644
--- a/car/res/values-ml/strings.xml
+++ b/car/res/values-ml/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"റോഡിൽ ശ്രദ്ധിക്കുക"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"വികസിപ്പിക്കുക/ചുരുക്കുക ബട്ടൺ"</string>
</resources>
diff --git a/car/res/values-mn/strings.xml b/car/res/values-mn/strings.xml
index 4b249a4..a8ef13c 100644
--- a/car/res/values-mn/strings.xml
+++ b/car/res/values-mn/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Зам дээр төвлөрөх"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Дэлгэх/буулгах товчлуур"</string>
</resources>
diff --git a/car/res/values-mr/strings.xml b/car/res/values-mr/strings.xml
index c79f3f3..3032c97 100644
--- a/car/res/values-mr/strings.xml
+++ b/car/res/values-mr/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"रस्त्यावर फोकस करा"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"बटण विस्तृत करा/कोलॅप्स करा"</string>
</resources>
diff --git a/car/res/values-ms/strings.xml b/car/res/values-ms/strings.xml
index d209113..301f7eb 100644
--- a/car/res/values-ms/strings.xml
+++ b/car/res/values-ms/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Beri tumpuan pada jalan raya"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Butang kembangkan/runtuhkan"</string>
</resources>
diff --git a/car/res/values-my/strings.xml b/car/res/values-my/strings.xml
index 438729a..f5317f7 100644
--- a/car/res/values-my/strings.xml
+++ b/car/res/values-my/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"လမ်းကို အာရုံစိုက်ရန်"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"ချဲ့ရန်/ခေါက်သိမ်းရန် ခလုတ်"</string>
</resources>
diff --git a/car/res/values-nb/strings.xml b/car/res/values-nb/strings.xml
index eb3a144..e4c4810 100644
--- a/car/res/values-nb/strings.xml
+++ b/car/res/values-nb/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Fokuser på veien"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Vis/skjul-knapp"</string>
</resources>
diff --git a/car/res/values-ne/strings.xml b/car/res/values-ne/strings.xml
index d066c0b..c4499b8 100644
--- a/car/res/values-ne/strings.xml
+++ b/car/res/values-ne/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"सडकमा ध्यान केन्द्रित गर्नु…"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"विस्तृत/संक्षिप्त गर्ने बटन"</string>
</resources>
diff --git a/car/res/values-nl/strings.xml b/car/res/values-nl/strings.xml
index 7fcb11e..de08f63 100644
--- a/car/res/values-nl/strings.xml
+++ b/car/res/values-nl/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Houd je aandacht op de weg"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Knop voor uitvouwen/samenvouwen"</string>
</resources>
diff --git a/car/res/values-or/strings.xml b/car/res/values-or/strings.xml
new file mode 100644
index 0000000..3a003b8
--- /dev/null
+++ b/car/res/values-or/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="speed_bump_lockout_message" msgid="5405697774899378511">"ରାସ୍ତା ଉପରେ ଧ୍ୟାନରଖନ୍ତୁ"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"ବିସ୍ତାର/ସଂକୋଚନ ବଟନ୍"</string>
+</resources>
diff --git a/car/res/values-pa/strings.xml b/car/res/values-pa/strings.xml
index 137bd2a..63a19f3 100644
--- a/car/res/values-pa/strings.xml
+++ b/car/res/values-pa/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"ਸੜਕ \'ਤੇ ਧਿਆਨ ਦਿਓ"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"ਵਿਸਤਾਰ ਕਰੋ/ਸਮੇਟੋ ਬਟਨ"</string>
</resources>
diff --git a/car/res/values-pl/strings.xml b/car/res/values-pl/strings.xml
index c5aa323..133d27e 100644
--- a/car/res/values-pl/strings.xml
+++ b/car/res/values-pl/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Skup się na drodze"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Przycisk zwijania/rozwijania"</string>
</resources>
diff --git a/car/res/values-pt-rBR/strings.xml b/car/res/values-pt-rBR/strings.xml
index a6c515a..6c6e459 100644
--- a/car/res/values-pt-rBR/strings.xml
+++ b/car/res/values-pt-rBR/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Foco na estrada"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Botão \"Expandir/Recolher\""</string>
</resources>
diff --git a/car/res/values-pt-rPT/strings.xml b/car/res/values-pt-rPT/strings.xml
index 2338efe..911b120 100644
--- a/car/res/values-pt-rPT/strings.xml
+++ b/car/res/values-pt-rPT/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Concentre-se na estrada."</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Botão Expandir/reduzir"</string>
</resources>
diff --git a/car/res/values-pt/strings.xml b/car/res/values-pt/strings.xml
index a6c515a..6c6e459 100644
--- a/car/res/values-pt/strings.xml
+++ b/car/res/values-pt/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Foco na estrada"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Botão \"Expandir/Recolher\""</string>
</resources>
diff --git a/car/res/values-ro/strings.xml b/car/res/values-ro/strings.xml
index 20cc3e7..7fe204a 100644
--- a/car/res/values-ro/strings.xml
+++ b/car/res/values-ro/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Concentrați-vă asupra drumului"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Butonul de extindere/restrângere"</string>
</resources>
diff --git a/car/res/values-ru/strings.xml b/car/res/values-ru/strings.xml
index 198f7fa..6554dc3 100644
--- a/car/res/values-ru/strings.xml
+++ b/car/res/values-ru/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Следите за дорогой"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Кнопка \"Развернуть/свернуть\""</string>
</resources>
diff --git a/car/res/values-si/strings.xml b/car/res/values-si/strings.xml
index 530c12a..48977bd 100644
--- a/car/res/values-si/strings.xml
+++ b/car/res/values-si/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"මාර්ගයට අවධානය යොමු කරන්න"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"දිග හැරීමේ/හැකිළීමේ බොත්තම"</string>
</resources>
diff --git a/car/res/values-sk/strings.xml b/car/res/values-sk/strings.xml
index a959d09..d8175c4 100644
--- a/car/res/values-sk/strings.xml
+++ b/car/res/values-sk/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Sústreďte sa na cestu"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Tlačidlo rozbalenia/zbalenia"</string>
</resources>
diff --git a/car/res/values-sl/strings.xml b/car/res/values-sl/strings.xml
index c0a8164..85f2602 100644
--- a/car/res/values-sl/strings.xml
+++ b/car/res/values-sl/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Glejte na cesto"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Gumb za razširitev/strnitev"</string>
</resources>
diff --git a/car/res/values-sq/strings.xml b/car/res/values-sq/strings.xml
index c8c91ef..87cb023 100644
--- a/car/res/values-sq/strings.xml
+++ b/car/res/values-sq/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Përqendrohu te rruga"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Butoni i zgjerimit/palosjes"</string>
</resources>
diff --git a/car/res/values-sr/strings.xml b/car/res/values-sr/strings.xml
index d7a6b85..d3fe526 100644
--- a/car/res/values-sr/strings.xml
+++ b/car/res/values-sr/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Фокусирајте се на пут"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Дугме Прошири/скупи"</string>
</resources>
diff --git a/car/res/values-sv/strings.xml b/car/res/values-sv/strings.xml
index 3798509..c4ae438 100644
--- a/car/res/values-sv/strings.xml
+++ b/car/res/values-sv/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Fokusera på körningen"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Knappen Utöka/komprimera"</string>
</resources>
diff --git a/car/res/values-sw/strings.xml b/car/res/values-sw/strings.xml
index a5b76c7..d3472f5 100644
--- a/car/res/values-sw/strings.xml
+++ b/car/res/values-sw/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Tia makini barabarani"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Kitufe cha kupanua/kukunja"</string>
</resources>
diff --git a/car/res/values-ta/strings.xml b/car/res/values-ta/strings.xml
index e97f385..e13ede5 100644
--- a/car/res/values-ta/strings.xml
+++ b/car/res/values-ta/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"வாகனம் ஓட்டும்போது கவனம் தேவை"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"விரிவாக்குவதற்கான/சுருக்குவதற்கான பட்டன்"</string>
</resources>
diff --git a/car/res/values-te/strings.xml b/car/res/values-te/strings.xml
index 9079a09..b106363 100644
--- a/car/res/values-te/strings.xml
+++ b/car/res/values-te/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"రహదారిపై దృష్టి ఉంచండి"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"విస్తరించు/కుదించు బటన్"</string>
</resources>
diff --git a/car/res/values-th/strings.xml b/car/res/values-th/strings.xml
index 3ba0d6b..05be415 100644
--- a/car/res/values-th/strings.xml
+++ b/car/res/values-th/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"จดจ่อกับถนน"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"ปุ่มขยาย/ยุบ"</string>
</resources>
diff --git a/car/res/values-tl/strings.xml b/car/res/values-tl/strings.xml
index 395e555..a56c37c 100644
--- a/car/res/values-tl/strings.xml
+++ b/car/res/values-tl/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Tumuon sa kalsada"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Button na i-expand/i-collapse"</string>
</resources>
diff --git a/car/res/values-tr/strings.xml b/car/res/values-tr/strings.xml
index a0f0b22..b28b66e 100644
--- a/car/res/values-tr/strings.xml
+++ b/car/res/values-tr/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Dikkatinizi yola verin"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Genişlet/daralt düğmesi"</string>
</resources>
diff --git a/car/res/values-uk/strings.xml b/car/res/values-uk/strings.xml
index 2657348..1964936 100644
--- a/car/res/values-uk/strings.xml
+++ b/car/res/values-uk/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Зосередьтеся на дорозі"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Кнопка \"Розгорнути або згорнути\""</string>
</resources>
diff --git a/car/res/values-ur/strings.xml b/car/res/values-ur/strings.xml
index 60c578c..c1d6b3e 100644
--- a/car/res/values-ur/strings.xml
+++ b/car/res/values-ur/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"سڑک پر توجہ مرکوز کریں"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"پھیلائیں/سکیڑیں بٹن"</string>
</resources>
diff --git a/car/res/values-uz/strings.xml b/car/res/values-uz/strings.xml
index bdaba48..a830b84 100644
--- a/car/res/values-uz/strings.xml
+++ b/car/res/values-uz/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Diqqatingizni yo‘lga qarating"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Yoyish/yig‘ish tugmasi"</string>
</resources>
diff --git a/car/res/values-vi/strings.xml b/car/res/values-vi/strings.xml
index 37457d4..144f41a 100644
--- a/car/res/values-vi/strings.xml
+++ b/car/res/values-vi/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Tập trung vào đường đi"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Nút mở rộng/thu gọn"</string>
</resources>
diff --git a/car/res/values-zh-rCN/strings.xml b/car/res/values-zh-rCN/strings.xml
index ec0c3c4..0c2caea 100644
--- a/car/res/values-zh-rCN/strings.xml
+++ b/car/res/values-zh-rCN/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"请专心驾驶"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"“展开”/“收起”按钮"</string>
</resources>
diff --git a/car/res/values-zh-rHK/strings.xml b/car/res/values-zh-rHK/strings.xml
index 61102cc..09c1a9e 100644
--- a/car/res/values-zh-rHK/strings.xml
+++ b/car/res/values-zh-rHK/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"請專心駕駛"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"展開/收合按鈕"</string>
</resources>
diff --git a/car/res/values-zh-rTW/strings.xml b/car/res/values-zh-rTW/strings.xml
index 61102cc..09c1a9e 100644
--- a/car/res/values-zh-rTW/strings.xml
+++ b/car/res/values-zh-rTW/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"請專心駕駛"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"展開/收合按鈕"</string>
</resources>
diff --git a/car/res/values-zu/strings.xml b/car/res/values-zu/strings.xml
index bdf7337..9403834 100644
--- a/car/res/values-zu/strings.xml
+++ b/car/res/values-zu/strings.xml
@@ -17,4 +17,5 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="speed_bump_lockout_message" msgid="5405697774899378511">"Gxila emgwaqweni"</string>
+ <string name="action_bar_expand_collapse_button" msgid="196909968432559564">"Inkinobho yokunweba/ukugoqa"</string>
</resources>
diff --git a/car/res/values/styles.xml b/car/res/values/styles.xml
index d6b6d28..aee10d2 100644
--- a/car/res/values/styles.xml
+++ b/car/res/values/styles.xml
@@ -173,6 +173,20 @@
<item name="android:textColor">@color/car_body4</item>
</style>
+ <!-- The styling for action button text. -->
+ <style name="TextAppearance.Car.Action1">
+ <item name="android:fontFamily">sans-serif-medium</item>
+ <item name="android:textStyle">normal</item>
+ <item name="android:textAllCaps">true</item>
+ <item name="android:textSize">@dimen/car_action1_size</item>
+ <item name="android:textColor">@color/car_accent</item>
+ </style>
+
+ <!-- The styling for menu text in action bar. -->
+ <style name="TextAppearance.Car.ActionBar.Menu" parent="TextAppearance.Car.Action1">
+ <item name="android:textColor">?attr/actionMenuTextColor</item>
+ </style>
+
<!-- Styles for TextInputLayout hints. -->
<style name="TextAppearance.Car.Hint" parent="TextAppearance.Car.Body2" />
@@ -181,11 +195,33 @@
<!-- ======= -->
<eat-comment />
- <!-- The styling for the action bar. -->
+ <!-- The styling for Toolbar used as action bar. -->
<style name="Widget.Car.Toolbar" parent="Widget.AppCompat.Toolbar">
- <item name="titleTextAppearance">@style/TextAppearance.Car.Title.Light</item>
- <item name="contentInsetStart">@dimen/car_keyline_1</item>
+ <item name="android:minHeight">?attr/actionBarSize</item>
+ <item name="background">@color/car_card</item>
<item name="contentInsetEnd">@dimen/car_keyline_1</item>
+ <item name="contentInsetStart">@dimen/car_keyline_1</item>
+ <item name="elevation">@dimen/car_action_bar_elevation</item>
+ <item name="subtitleTextAppearance">@style/TextAppearance.Car.Body2</item>
+ <item name="titleTextAppearance">@style/TextAppearance.Car.Title2</item>
+ <item name="navigationIcon">@drawable/ic_nav_arrow_back</item>
+ </style>
+
+ <!-- The styling for the navigation button in action bar. -->
+ <style name="Widget.Car.Toolbar.Button.Navigation"
+ parent="Widget.AppCompat.Toolbar.Button.Navigation">
+ <item name="android:background">@drawable/car_card_ripple_background</item>
+ <item name="android:scaleType">center</item>
+ <item name="android:tint">@color/car_tint</item>
+ </style>
+
+ <style name="Widget.Car.ActionButton" parent="Widget.AppCompat.ActionButton">
+ <item name="android:background">?attr/actionBarItemBackground</item>
+ <item name="android:minHeight">@dimen/car_action_bar_height</item>
+ <item name="android:paddingLeft">@dimen/car_padding_2</item>
+ <item name="android:paddingRight">@dimen/car_padding_2</item>
+ <item name="android:scaleType">fitCenter</item>
+ <item name="android:tint">@color/car_tint</item>
</style>
<!-- The style for the menu bar (i.e. hamburger) and back arrow in the navigation drawer. -->
diff --git a/car/res/values/themes.xml b/car/res/values/themes.xml
index 526fd79..8d8c6ce 100644
--- a/car/res/values/themes.xml
+++ b/car/res/values/themes.xml
@@ -32,12 +32,19 @@
<item name="android:editTextColor">@color/car_body1</item>
<item name="android:colorControlNormal">@color/car_body2</item>
<item name="android:seekBarStyle">@style/Widget.Car.SeekBar</item>
+ <item name="actionBarItemBackground">@drawable/car_card_ripple_background</item>
+ <item name="actionBarSize">@dimen/car_app_bar_height</item>
+ <item name="actionButtonStyle">@style/Widget.Car.ActionButton</item>
+ <item name="actionMenuTextAppearance">@style/TextAppearance.Car.ActionBar.Menu</item>
+ <item name="actionMenuTextColor">@color/car_accent</item>
<item name="carDialogTheme">@style/Theme.Car.Dialog</item>
- <item name="pagedListViewStyle">@style/Widget.Car.List</item>
<item name="listItemBackgroundColor">@color/car_card</item>
<item name="listItemTitleTextAppearance">@style/TextAppearance.Car.Body1</item>
<item name="listItemBodyTextAppearance">@style/TextAppearance.Car.Body2</item>
<item name="listItemSubheaderTextAppearance">@style/TextAppearance.Car.Subheader</item>
+ <item name="pagedListViewStyle">@style/Widget.Car.List</item>
+ <item name="toolbarNavigationButtonStyle">@style/Widget.Car.Toolbar.Button.Navigation</item>
+ <item name="toolbarStyle">@style/Widget.Car.Toolbar</item>
</style>
<!-- Theme for the Car that is a passthrough for the default theme. -->
diff --git a/car/src/androidTest/java/androidx/car/widget/SeekbarListItemTest.java b/car/src/androidTest/java/androidx/car/widget/SeekbarListItemTest.java
index f8b8d33..5233734 100644
--- a/car/src/androidTest/java/androidx/car/widget/SeekbarListItemTest.java
+++ b/car/src/androidTest/java/androidx/car/widget/SeekbarListItemTest.java
@@ -40,6 +40,8 @@
import android.view.ViewGroup;
import android.widget.SeekBar;
+import androidx.car.test.R;
+
import org.hamcrest.Matcher;
import org.junit.Assume;
import org.junit.Before;
@@ -51,8 +53,6 @@
import java.util.Arrays;
import java.util.List;
-import androidx.car.test.R;
-
/**
* Tests the layout configuration in {@link SeekbarListItem}.
*/
@@ -67,11 +67,6 @@
private PagedListViewTestActivity mActivity;
private PagedListView mPagedListView;
- private boolean isAutoDevice() {
- PackageManager packageManager = mActivityRule.getActivity().getPackageManager();
- return packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
- }
-
@Before
public void setUp() {
Assume.assumeTrue(isAutoDevice());
@@ -79,44 +74,6 @@
mPagedListView = mActivity.findViewById(R.id.paged_list_view);
}
- private void setupPagedListView(List<? extends ListItem> items) {
- ListItemProvider provider = new ListItemProvider.ListProvider(
- new ArrayList<>(items));
- try {
- mActivityRule.runOnUiThread(() -> {
- mPagedListView.setAdapter(new ListItemAdapter(mActivity, provider));
- });
- } catch (Throwable throwable) {
- throwable.printStackTrace();
- throw new RuntimeException(throwable);
- }
- // Wait for paged list view to layout by using espresso to scroll to a position.
- onView(withId(R.id.recycler_view)).perform(scrollToPosition(0));
- }
-
- private void verifyViewDefaultVisibility(View view) {
- if (view instanceof ViewGroup) {
- ViewGroup viewGroup = (ViewGroup) view;
- final int childCount = viewGroup.getChildCount();
- for (int i = 0; i < childCount; i++) {
- verifyViewDefaultVisibility(viewGroup.getChildAt(i));
- }
- } else if (view instanceof SeekBar) {
- assertThat(view.getVisibility(), is(equalTo(View.VISIBLE)));
- } else {
- assertThat("Visibility of view "
- + mActivity.getResources().getResourceEntryName(view.getId())
- + " by default should be GONE.",
- view.getVisibility(), is(equalTo(View.GONE)));
- }
- }
-
- private SeekbarListItem.ViewHolder getViewHolderAtPosition(int position) {
- return (SeekbarListItem.ViewHolder) mPagedListView.getRecyclerView()
- .findViewHolderForAdapterPosition(
- position);
- }
-
@Test
public void testOnlySliderIsVisibleInEmptyItem() {
SeekbarListItem item = new SeekbarListItem(mActivity, 0, 0, null, null);
@@ -182,6 +139,82 @@
}
@Test
+ public void testSettingMax() {
+ final int max = 50;
+ SeekbarListItem item = new SeekbarListItem(mActivity);
+ item.setMax(max);
+
+ setupPagedListView(Arrays.asList(item));
+
+ assertThat(getViewHolderAtPosition(0).getSeekBar().getMax(), is(equalTo(max)));
+ }
+
+ @Test
+ public void testSettingProgress() {
+ final int progress = 100;
+ SeekbarListItem item = new SeekbarListItem(mActivity);
+ item.setMax(progress);
+ item.setProgress(progress);
+
+ setupPagedListView(Arrays.asList(item));
+
+ assertThat(getViewHolderAtPosition(0).getSeekBar().getProgress(), is(equalTo(progress)));
+ }
+
+ @Test
+ public void testSettingSecondaryProgress() {
+ final int secondaryProgress = 50;
+ SeekbarListItem item = new SeekbarListItem(mActivity);
+ item.setMax(secondaryProgress);
+ item.setSecondaryProgress(secondaryProgress);
+
+ setupPagedListView(Arrays.asList(item));
+
+ assertThat(getViewHolderAtPosition(0).getSeekBar().getSecondaryProgress(),
+ is(equalTo(secondaryProgress)));
+ }
+
+ @Test
+ public void testSettingOnSeekBarChangeListener() throws Throwable {
+ boolean[] changed = new boolean[]{false};
+
+ SeekbarListItem item = new SeekbarListItem(mActivity);
+ item.setMax(100);
+ item.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ changed[0] = true;
+ }
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {}
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {}
+ });
+
+ setupPagedListView(Arrays.asList(item));
+
+ mActivityRule.runOnUiThread(() -> getViewHolderAtPosition(0).getSeekBar().setProgress(50));
+ assertTrue(changed[0]);
+ }
+
+ @Test
+ public void testUpdatingProgress() {
+ final int progress = 50;
+ final int newProgress = 100;
+
+ SeekbarListItem item = new SeekbarListItem(mActivity);
+ item.setMax(newProgress);
+ item.setProgress(progress);
+ setupPagedListView(Arrays.asList(item));
+
+ item.setProgress(newProgress);
+ refreshUi();
+
+ assertThat(getViewHolderAtPosition(0).getSeekBar().getProgress(), is(equalTo(newProgress)));
+ }
+
+ @Test
public void testPrimaryIconIsNotClickable() {
SeekbarListItem item0 = new SeekbarListItem(mActivity, 0, 0, null, null);
item0.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon);
@@ -291,6 +324,62 @@
is(equalTo(drawable)));
}
+ private boolean isAutoDevice() {
+ PackageManager packageManager = mActivityRule.getActivity().getPackageManager();
+ return packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+ }
+
+ private void setupPagedListView(List<? extends ListItem> items) {
+ ListItemProvider provider = new ListItemProvider.ListProvider(
+ new ArrayList<>(items));
+ try {
+ mActivityRule.runOnUiThread(() -> {
+ mPagedListView.setAdapter(new ListItemAdapter(mActivity, provider));
+ });
+ } catch (Throwable throwable) {
+ throwable.printStackTrace();
+ throw new RuntimeException(throwable);
+ }
+ // Wait for paged list view to layout by using espresso to scroll to a position.
+ onView(withId(R.id.recycler_view)).perform(scrollToPosition(0));
+ }
+
+ private void verifyViewDefaultVisibility(View view) {
+ if (view instanceof ViewGroup) {
+ ViewGroup viewGroup = (ViewGroup) view;
+ final int childCount = viewGroup.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ verifyViewDefaultVisibility(viewGroup.getChildAt(i));
+ }
+ } else if (view instanceof SeekBar) {
+ assertThat(view.getVisibility(), is(equalTo(View.VISIBLE)));
+ } else {
+ assertThat("Visibility of view "
+ + mActivity.getResources().getResourceEntryName(view.getId())
+ + " by default should be GONE.",
+ view.getVisibility(), is(equalTo(View.GONE)));
+ }
+ }
+
+ private SeekbarListItem.ViewHolder getViewHolderAtPosition(int position) {
+ return (SeekbarListItem.ViewHolder) mPagedListView.getRecyclerView()
+ .findViewHolderForAdapterPosition(
+ position);
+ }
+
+ private void refreshUi() {
+ try {
+ mActivityRule.runOnUiThread(() -> {
+ mPagedListView.getAdapter().notifyDataSetChanged();
+ });
+ } catch (Throwable throwable) {
+ throwable.printStackTrace();
+ throw new RuntimeException(throwable);
+ }
+ // Wait for paged list view to layout by using espresso to scroll to a position.
+ onView(withId(R.id.recycler_view)).perform(scrollToPosition(0));
+ }
+
private static ViewAction clickChildViewWithId(final int id) {
return new ViewAction() {
@Override
diff --git a/car/src/main/java/androidx/car/widget/DayNightStyle.java b/car/src/main/java/androidx/car/widget/DayNightStyle.java
index 37d0d51..73f9ce4 100644
--- a/car/src/main/java/androidx/car/widget/DayNightStyle.java
+++ b/car/src/main/java/androidx/car/widget/DayNightStyle.java
@@ -16,8 +16,12 @@
package androidx.car.widget;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
import androidx.annotation.IntDef;
+import java.lang.annotation.Retention;
+
/**
* Specifies how the system UI should respond to day/night mode events.
*
@@ -37,6 +41,7 @@
DayNightStyle.FORCE_NIGHT,
DayNightStyle.FORCE_DAY,
})
+@Retention(SOURCE)
public @interface DayNightStyle {
/**
* Sets the foreground color to be automatically changed based on day/night mode, assuming the
diff --git a/car/src/main/java/androidx/car/widget/PagedListView.java b/car/src/main/java/androidx/car/widget/PagedListView.java
index cddc89d..b951bc1 100644
--- a/car/src/main/java/androidx/car/widget/PagedListView.java
+++ b/car/src/main/java/androidx/car/widget/PagedListView.java
@@ -18,6 +18,8 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
@@ -50,6 +52,8 @@
import androidx.recyclerview.widget.OrientationHelper;
import androidx.recyclerview.widget.RecyclerView;
+import java.lang.annotation.Retention;
+
/**
* View that wraps a {@link RecyclerView} and a scroll bar that has
* page up and down arrows. Interaction with this view is similar to a {@code RecyclerView} as it
@@ -187,6 +191,7 @@
Gutter.END,
Gutter.BOTH,
})
+ @Retention(SOURCE)
public @interface Gutter {
/**
* No gutter on either side of the list items. The items will span the full width of the
diff --git a/car/src/main/java/androidx/car/widget/SeekbarListItem.java b/car/src/main/java/androidx/car/widget/SeekbarListItem.java
index b6477c1..994ba4f 100644
--- a/car/src/main/java/androidx/car/widget/SeekbarListItem.java
+++ b/car/src/main/java/androidx/car/widget/SeekbarListItem.java
@@ -66,6 +66,8 @@
*
* <p>When conflicting methods are called (e.g. setting primary action to both primary icon and
* no icon), the last called method wins.
+ *
+ * {@code minimum value} is set to 0.
*/
public class SeekbarListItem extends ListItem<SeekbarListItem.ViewHolder> {
@@ -99,8 +101,9 @@
private String mText;
- private int mProgress;
private int mMax;
+ private int mProgress;
+ private int mSecondaryProgress;
private SeekBar.OnSeekBarChangeListener mOnSeekBarChangeListener;
@SupplementalActionType private int mSupplementalActionType = SUPPLEMENTAL_ACTION_NO_ACTION;
@@ -123,16 +126,22 @@
* @param progress the current progress of the specified value.
* @param listener listener to receive notification of changes to progress level.
* @param text displays a text on top of the SeekBar.
+ *
+ * @deprecated use {@link #SeekbarListItem(Context)} and individual field setters instead.
*/
+ @Deprecated
public SeekbarListItem(Context context, int max, int progress,
SeekBar.OnSeekBarChangeListener listener, String text) {
+ this(context);
+
+ setMax(max);
+ setProgress(progress);
+ setOnSeekBarChangeListener(listener);
+ setText(text);
+ }
+
+ public SeekbarListItem(Context context) {
mContext = context;
-
- mMax = max;
- mProgress = progress;
- mOnSeekBarChangeListener = listener;
- mText = text;
-
markDirty();
}
@@ -145,6 +154,46 @@
}
/**
+ * Sets max value of seekbar.
+ */
+ public void setMax(int max) {
+ mMax = max;
+ markDirty();
+ }
+
+ /**
+ * Sets progress of seekbar.
+ */
+ public void setProgress(int progress) {
+ mProgress = progress;
+ markDirty();
+ }
+
+ /**
+ * Sets secondary progress of seekbar.
+ */
+ public void setSecondaryProgress(int secondaryProgress) {
+ mSecondaryProgress = secondaryProgress;
+ markDirty();
+ }
+
+ /**
+ * Sets {@link SeekBar.OnSeekBarChangeListener}.
+ */
+ public void setOnSeekBarChangeListener(SeekBar.OnSeekBarChangeListener listener) {
+ mOnSeekBarChangeListener = listener;
+ markDirty();
+ }
+
+ /**
+ * Sets text that sits on top of seekbar.
+ */
+ public void setText(String text) {
+ mText = text;
+ markDirty();
+ }
+
+ /**
* Calculates the layout params for views in {@link ViewHolder}.
*/
@Override
@@ -272,6 +321,7 @@
mBinders.add(vh -> {
vh.getSeekBar().setMax(mMax);
vh.getSeekBar().setProgress(mProgress);
+ vh.getSeekBar().setSecondaryProgress(mSecondaryProgress);
vh.getSeekBar().setOnSeekBarChangeListener(mOnSeekBarChangeListener);
if (!TextUtils.isEmpty(mText)) {
diff --git a/collection/ktx/OWNERS b/collection/ktx/OWNERS
new file mode 100644
index 0000000..e450f4c
--- /dev/null
+++ b/collection/ktx/OWNERS
@@ -0,0 +1 @@
+jakew@google.com
diff --git a/collection/ktx/build.gradle b/collection/ktx/build.gradle
new file mode 100644
index 0000000..83944f2
--- /dev/null
+++ b/collection/ktx/build.gradle
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+
+plugins {
+ id("SupportKotlinLibraryPlugin")
+}
+
+dependencies {
+ compile(project(":collection"))
+ compile(KOTLIN_STDLIB)
+ testCompile(JUNIT)
+ testCompile(TRUTH)
+ testCompile(project(":internal-testutils-ktx"))
+}
+
+supportLibrary {
+ name = "Collections Kotlin Extensions"
+ publish = true
+ mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+ mavenGroup = LibraryGroups.COLLECTION
+ inceptionYear = "2018"
+ description = "Kotlin extensions for 'collection' artifact"
+}
diff --git a/collection/ktx/src/main/java/androidx/collection/ArrayMap.kt b/collection/ktx/src/main/java/androidx/collection/ArrayMap.kt
new file mode 100644
index 0000000..8f3299e
--- /dev/null
+++ b/collection/ktx/src/main/java/androidx/collection/ArrayMap.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.collection
+
+/** Returns an empty new [ArrayMap]. */
+inline fun <K, V> arrayMapOf(): ArrayMap<K, V> = ArrayMap()
+
+/**
+ * Returns a new [ArrayMap] with the specified contents, given as a list of pairs where the first
+ * component is the key and the second component is the value.
+ *
+ * If multiple pairs have the same key, the resulting map will contain the value from the last of
+ * those pairs.
+ */
+fun <K, V> arrayMapOf(vararg pairs: Pair<K, V>): ArrayMap<K, V> {
+ val map = ArrayMap<K, V>(pairs.size)
+ for (pair in pairs) {
+ map[pair.first] = pair.second
+ }
+ return map
+}
diff --git a/collection/ktx/src/main/java/androidx/collection/ArraySet.kt b/collection/ktx/src/main/java/androidx/collection/ArraySet.kt
new file mode 100644
index 0000000..07b4be7
--- /dev/null
+++ b/collection/ktx/src/main/java/androidx/collection/ArraySet.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.collection
+
+/** Returns an empty new [ArraySet]. */
+inline fun <T> arraySetOf(): ArraySet<T> = ArraySet()
+
+/** Returns a new [ArraySet] with the specified contents. */
+fun <T> arraySetOf(vararg values: T): ArraySet<T> {
+ val set = ArraySet<T>(values.size)
+ @Suppress("LoopToCallChain") // Causes needless copy to a list.
+ for (value in values) {
+ set.add(value)
+ }
+ return set
+}
diff --git a/collection/ktx/src/main/java/androidx/collection/LongSparseArray.kt b/collection/ktx/src/main/java/androidx/collection/LongSparseArray.kt
new file mode 100644
index 0000000..fd6201a
--- /dev/null
+++ b/collection/ktx/src/main/java/androidx/collection/LongSparseArray.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.collection
+
+/** Returns the number of key/value pairs in the collection. */
+inline val <T> LongSparseArray<T>.size get() = size()
+
+/** Returns true if the collection contains [key]. */
+inline operator fun <T> LongSparseArray<T>.contains(key: Long) = indexOfKey(key) >= 0
+
+/** Allows the use of the index operator for storing values in the collection. */
+inline operator fun <T> LongSparseArray<T>.set(key: Long, value: T) = put(key, value)
+
+/** Creates a new collection by adding or replacing entries from [other]. */
+operator fun <T> LongSparseArray<T>.plus(other: LongSparseArray<T>): LongSparseArray<T> {
+ val new = LongSparseArray<T>(size() + other.size())
+ new.putAll(this)
+ new.putAll(other)
+ return new
+}
+
+/** Returns true if the collection contains [key]. */
+inline fun <T> LongSparseArray<T>.containsKey(key: Long) = indexOfKey(key) >= 0
+
+/** Returns true if the collection contains [value]. */
+inline fun <T> LongSparseArray<T>.containsValue(value: T) = indexOfValue(value) != -1
+
+/** Return the value corresponding to [key], or [defaultValue] when not present. */
+inline fun <T> LongSparseArray<T>.getOrDefault(key: Long, defaultValue: T) =
+ get(key) ?: defaultValue
+
+/** Return the value corresponding to [key], or from [defaultValue] when not present. */
+inline fun <T> LongSparseArray<T>.getOrElse(key: Long, defaultValue: () -> T) =
+ get(key) ?: defaultValue()
+
+/** Return true when the collection contains elements. */
+inline fun <T> LongSparseArray<T>.isNotEmpty() = size() != 0
+
+/** Removes the entry for [key] only if it is mapped to [value]. */
+fun <T> LongSparseArray<T>.remove(key: Long, value: T): Boolean {
+ val index = indexOfKey(key)
+ if (index != -1 && value == valueAt(index)) {
+ removeAt(index)
+ return true
+ }
+ return false
+}
+
+/** Update this collection by adding or replacing entries from [other]. */
+fun <T> LongSparseArray<T>.putAll(other: LongSparseArray<T>) = other.forEach(::put)
+
+/** Performs the given [action] for each key/value entry. */
+inline fun <T> LongSparseArray<T>.forEach(action: (key: Long, value: T) -> Unit) {
+ for (index in 0 until size()) {
+ action(keyAt(index), valueAt(index))
+ }
+}
+
+/** Return an iterator over the collection's keys. */
+fun <T> LongSparseArray<T>.keyIterator(): LongIterator = object : LongIterator() {
+ var index = 0
+ override fun hasNext() = index < size()
+ override fun nextLong() = keyAt(index++)
+}
+
+/** Return an iterator over the collection's values. */
+fun <T> LongSparseArray<T>.valueIterator(): Iterator<T> = object : Iterator<T> {
+ var index = 0
+ override fun hasNext() = index < size()
+ override fun next() = valueAt(index++)
+}
diff --git a/collection/ktx/src/main/java/androidx/collection/LruCache.kt b/collection/ktx/src/main/java/androidx/collection/LruCache.kt
new file mode 100644
index 0000000..554a012
--- /dev/null
+++ b/collection/ktx/src/main/java/androidx/collection/LruCache.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+/**
+ * Creates an [LruCache] with the given parameters.
+ *
+ * @param maxSize for caches that do not specify [sizeOf], this is
+ * the maximum number of entries in the cache. For all other caches,
+ * this is the maximum sum of the sizes of the entries in this cache.
+ * @param sizeOf function that returns the size of the entry for key and value in
+ * user-defined units. The default implementation returns 1.
+ * @param create a create called after a cache miss to compute a value for the corresponding key.
+ * Returns the computed value or null if no value can be computed. The default implementation
+ * returns null.
+ * @param onEntryRemoved a function called for entries that have been evicted or removed.
+ *
+ * @see LruCache.sizeOf
+ * @see LruCache.create
+ * @see LruCache.entryRemoved
+ */
+inline fun <K : Any, V : Any> lruCache(
+ maxSize: Int,
+ crossinline sizeOf: (key: K, value: V) -> Int = { _, _ -> 1 },
+ @Suppress("USELESS_CAST") // https://youtrack.jetbrains.com/issue/KT-21946
+ crossinline create: (key: K) -> V? = { null as V? },
+ crossinline onEntryRemoved: (evicted: Boolean, key: K, oldValue: V, newValue: V?) -> Unit =
+ { _, _, _, _ -> }
+): LruCache<K, V> {
+ return object : LruCache<K, V>(maxSize) {
+ override fun sizeOf(key: K, value: V) = sizeOf(key, value)
+ override fun create(key: K) = create(key)
+ override fun entryRemoved(evicted: Boolean, key: K, oldValue: V, newValue: V?) {
+ onEntryRemoved(evicted, key, oldValue, newValue)
+ }
+ }
+}
diff --git a/collection/ktx/src/main/java/androidx/collection/SparseArray.kt b/collection/ktx/src/main/java/androidx/collection/SparseArray.kt
new file mode 100644
index 0000000..e1d2382
--- /dev/null
+++ b/collection/ktx/src/main/java/androidx/collection/SparseArray.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.collection
+
+/** Returns the number of key/value pairs in the collection. */
+inline val <T> SparseArrayCompat<T>.size get() = size()
+
+/** Returns true if the collection contains [key]. */
+inline operator fun <T> SparseArrayCompat<T>.contains(key: Int) = indexOfKey(key) >= 0
+
+/** Allows the use of the index operator for storing values in the collection. */
+inline operator fun <T> SparseArrayCompat<T>.set(key: Int, value: T) = put(key, value)
+
+/** Creates a new collection by adding or replacing entries from [other]. */
+operator fun <T> SparseArrayCompat<T>.plus(other: SparseArrayCompat<T>): SparseArrayCompat<T> {
+ val new = SparseArrayCompat<T>(size() + other.size())
+ new.putAll(this)
+ new.putAll(other)
+ return new
+}
+
+/** Returns true if the collection contains [key]. */
+inline fun <T> SparseArrayCompat<T>.containsKey(key: Int) = indexOfKey(key) >= 0
+
+/** Returns true if the collection contains [value]. */
+inline fun <T> SparseArrayCompat<T>.containsValue(value: T) = indexOfValue(value) != -1
+
+/** Return the value corresponding to [key], or [defaultValue] when not present. */
+inline fun <T> SparseArrayCompat<T>.getOrDefault(key: Int, defaultValue: T) =
+ get(key) ?: defaultValue
+
+/** Return the value corresponding to [key], or from [defaultValue] when not present. */
+inline fun <T> SparseArrayCompat<T>.getOrElse(key: Int, defaultValue: () -> T) =
+ get(key) ?: defaultValue()
+
+/** Return true when the collection contains elements. */
+inline fun <T> SparseArrayCompat<T>.isNotEmpty() = size() != 0
+
+/** Removes the entry for [key] only if it is mapped to [value]. */
+fun <T> SparseArrayCompat<T>.remove(key: Int, value: T): Boolean {
+ val index = indexOfKey(key)
+ if (index != -1 && value == valueAt(index)) {
+ removeAt(index)
+ return true
+ }
+ return false
+}
+
+/** Update this collection by adding or replacing entries from [other]. */
+fun <T> SparseArrayCompat<T>.putAll(other: SparseArrayCompat<T>) = other.forEach(::put)
+
+/** Performs the given [action] for each key/value entry. */
+inline fun <T> SparseArrayCompat<T>.forEach(action: (key: Int, value: T) -> Unit) {
+ for (index in 0 until size()) {
+ action(keyAt(index), valueAt(index))
+ }
+}
+
+/** Return an iterator over the collection's keys. */
+fun <T> SparseArrayCompat<T>.keyIterator(): IntIterator = object : IntIterator() {
+ var index = 0
+ override fun hasNext() = index < size()
+ override fun nextInt() = keyAt(index++)
+}
+
+/** Return an iterator over the collection's values. */
+fun <T> SparseArrayCompat<T>.valueIterator(): Iterator<T> = object : Iterator<T> {
+ var index = 0
+ override fun hasNext() = index < size()
+ override fun next() = valueAt(index++)
+}
diff --git a/collection/ktx/src/test/java/androidx/collection/ArrayMapTest.kt b/collection/ktx/src/test/java/androidx/collection/ArrayMapTest.kt
new file mode 100644
index 0000000..8c002dd
--- /dev/null
+++ b/collection/ktx/src/test/java/androidx/collection/ArrayMapTest.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class ArrayMapTest {
+ @Test fun empty() {
+ val map = arrayMapOf<String, String>()
+ assertEquals(0, map.size)
+ }
+
+ @Test fun nonEmpty() {
+ val map = arrayMapOf("foo" to "bar", "bar" to "baz")
+ assertThat(map).containsExactly("foo", "bar", "bar", "baz")
+ }
+
+ @Test fun duplicateKeyKeepsLast() {
+ val map = arrayMapOf("foo" to "bar", "foo" to "baz")
+ assertThat(map).containsExactly("foo", "baz")
+ }
+}
diff --git a/collection/ktx/src/test/java/androidx/collection/ArraySetTest.kt b/collection/ktx/src/test/java/androidx/collection/ArraySetTest.kt
new file mode 100644
index 0000000..71a561d
--- /dev/null
+++ b/collection/ktx/src/test/java/androidx/collection/ArraySetTest.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class ArraySetTest {
+ @Test fun empty() {
+ val set = arraySetOf<String>()
+ assertEquals(0, set.size)
+ }
+
+ @Test fun nonEmpty() {
+ val set = arraySetOf("foo", "bar", "baz")
+ assertThat(set).containsExactly("foo", "bar", "baz")
+ }
+}
diff --git a/collection/ktx/src/test/java/androidx/collection/LongSparseArrayTest.kt b/collection/ktx/src/test/java/androidx/collection/LongSparseArrayTest.kt
new file mode 100644
index 0000000..3419664
--- /dev/null
+++ b/collection/ktx/src/test/java/androidx/collection/LongSparseArrayTest.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import androidx.testutils.fail
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class LongSparseArrayTest {
+ @Test fun sizeProperty() {
+ val array = LongSparseArray<String>()
+ assertEquals(0, array.size)
+ array.put(1L, "one")
+ assertEquals(1, array.size)
+ }
+
+ @Test fun containsOperator() {
+ val array = LongSparseArray<String>()
+ assertFalse(1L in array)
+ array.put(1L, "one")
+ assertTrue(1L in array)
+ }
+
+ @Test fun containsOperatorWithValue() {
+ val array = LongSparseArray<String>()
+
+ array.put(1L, "one")
+ assertFalse(2L in array)
+
+ array.put(2L, "two")
+ assertTrue(2L in array)
+ }
+
+ @Test fun setOperator() {
+ val array = LongSparseArray<String>()
+ array[1L] = "one"
+ assertEquals("one", array.get(1L))
+ }
+
+ @Test fun plusOperator() {
+ val first = LongSparseArray<String>().apply { put(1L, "one") }
+ val second = LongSparseArray<String>().apply { put(2L, "two") }
+ val combined = first + second
+ assertEquals(2, combined.size())
+ assertEquals(1L, combined.keyAt(0))
+ assertEquals("one", combined.valueAt(0))
+ assertEquals(2L, combined.keyAt(1))
+ assertEquals("two", combined.valueAt(1))
+ }
+
+ @Test fun containsKey() {
+ val array = LongSparseArray<String>()
+ assertFalse(array.containsKey(1L))
+ array.put(1L, "one")
+ assertTrue(array.containsKey(1L))
+ }
+
+ @Test fun containsKeyWithValue() {
+ val array = LongSparseArray<String>()
+
+ array.put(1L, "one")
+ assertFalse(array.containsKey(2L))
+
+ array.put(2L, "one")
+ assertTrue(array.containsKey(2L))
+ }
+
+ @Test fun containsValue() {
+ val array = LongSparseArray<String>()
+ assertFalse(array.containsValue("one"))
+ array.put(1L, "one")
+ assertTrue(array.containsValue("one"))
+ }
+
+ @Test fun getOrDefault() {
+ val array = LongSparseArray<Any>()
+ val default = Any()
+ assertSame(default, array.getOrDefault(1L, default))
+ array.put(1L, "one")
+ assertEquals("one", array.getOrDefault(1L, default))
+ }
+
+ @Test fun getOrElse() {
+ val array = LongSparseArray<Any>()
+ val default = Any()
+ assertSame(default, array.getOrElse(1L) { default })
+ array.put(1L, "one")
+ assertEquals("one", array.getOrElse(1L) { fail() })
+ }
+
+ @Test fun isNotEmpty() {
+ val array = LongSparseArray<String>()
+ assertFalse(array.isNotEmpty())
+ array.put(1L, "one")
+ assertTrue(array.isNotEmpty())
+ }
+
+ @Test fun removeValue() {
+ val array = LongSparseArray<String>()
+ array.put(1L, "one")
+ assertFalse(array.remove(0L, "one"))
+ assertEquals(1, array.size())
+ assertFalse(array.remove(1L, "two"))
+ assertEquals(1, array.size())
+ assertTrue(array.remove(1L, "one"))
+ assertEquals(0, array.size())
+ }
+
+ @Test fun putAll() {
+ val dest = LongSparseArray<String>()
+ val source = LongSparseArray<String>()
+ source.put(1L, "one")
+
+ assertEquals(0, dest.size())
+ dest.putAll(source)
+ assertEquals(1, dest.size())
+ }
+
+ @Test fun forEach() {
+ val array = LongSparseArray<String>()
+ array.forEach { _, _ -> fail() }
+
+ array.put(1L, "one")
+ array.put(2L, "two")
+ array.put(6L, "six")
+
+ val keys = mutableListOf<Long>()
+ val values = mutableListOf<String>()
+ array.forEach { key, value ->
+ keys.add(key)
+ values.add(value)
+ }
+ assertThat(keys).containsExactly(1L, 2L, 6L)
+ assertThat(values).containsExactly("one", "two", "six")
+ }
+
+ @Test fun keyIterator() {
+ val array = LongSparseArray<String>()
+ assertFalse(array.keyIterator().hasNext())
+
+ array.put(1L, "one")
+ array.put(2L, "two")
+ array.put(6L, "six")
+
+ val iterator = array.keyIterator()
+ assertTrue(iterator.hasNext())
+ assertEquals(1L, iterator.nextLong())
+ assertTrue(iterator.hasNext())
+ assertEquals(2L, iterator.nextLong())
+ assertTrue(iterator.hasNext())
+ assertEquals(6L, iterator.nextLong())
+ assertFalse(iterator.hasNext())
+ }
+
+ @Test fun valueIterator() {
+ val array = LongSparseArray<String>()
+ assertFalse(array.valueIterator().hasNext())
+
+ array.put(1L, "one")
+ array.put(2L, "two")
+ array.put(6L, "six")
+
+ val iterator = array.valueIterator()
+ assertTrue(iterator.hasNext())
+ assertEquals("one", iterator.next())
+ assertTrue(iterator.hasNext())
+ assertEquals("two", iterator.next())
+ assertTrue(iterator.hasNext())
+ assertEquals("six", iterator.next())
+ assertFalse(iterator.hasNext())
+ }
+}
diff --git a/collection/ktx/src/test/java/androidx/collection/LruCacheTest.kt b/collection/ktx/src/test/java/androidx/collection/LruCacheTest.kt
new file mode 100644
index 0000000..c47b423
--- /dev/null
+++ b/collection/ktx/src/test/java/androidx/collection/LruCacheTest.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class LruCacheTest {
+ private data class TestData(val x: String = "bla")
+
+ @Test fun size() {
+ val cache = lruCache<String, TestData>(200, { k, (x) -> k.length * x.length })
+ cache.put("long", TestData())
+ assertEquals(cache.size(), 12)
+ }
+
+ @Test fun create() {
+ val cache = lruCache<String, TestData>(200, create = { key -> TestData("$key foo") })
+ assertEquals(cache.get("kung"), TestData("kung foo"))
+ }
+
+ @Test fun onEntryRemoved() {
+ var wasCalled = false
+
+ val cache = lruCache<String, TestData>(200, onEntryRemoved = { _, _, _, _ ->
+ wasCalled = true
+ })
+ val initial = TestData()
+ cache.put("a", initial)
+ cache.remove("a")
+ assertTrue(wasCalled)
+ }
+}
diff --git a/collection/ktx/src/test/java/androidx/collection/SparseArrayTest.kt b/collection/ktx/src/test/java/androidx/collection/SparseArrayTest.kt
new file mode 100644
index 0000000..90d46c8
--- /dev/null
+++ b/collection/ktx/src/test/java/androidx/collection/SparseArrayTest.kt
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import androidx.testutils.fail
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class SparseArrayCompatTest {
+ @Test fun sizeProperty() {
+ val array = SparseArrayCompat<String>()
+ assertEquals(0, array.size)
+ array.put(1, "one")
+ assertEquals(1, array.size)
+ }
+
+ @Test fun containsOperator() {
+ val array = SparseArrayCompat<String>()
+ assertFalse(1 in array)
+ array.put(1, "one")
+ assertTrue(1 in array)
+ }
+
+ @Test fun containsOperatorWithItem() {
+ val array = SparseArrayCompat<String>()
+
+ array.put(1, "one")
+ assertFalse(2 in array)
+
+ array.put(2, "two")
+ assertTrue(2 in array)
+ }
+
+ @Test fun setOperator() {
+ val array = SparseArrayCompat<String>()
+ array[1] = "one"
+ assertEquals("one", array.get(1))
+ }
+
+ @Test fun plusOperator() {
+ val first = SparseArrayCompat<String>().apply { put(1, "one") }
+ val second = SparseArrayCompat<String>().apply { put(2, "two") }
+ val combined = first + second
+ assertEquals(2, combined.size())
+ assertEquals(1, combined.keyAt(0))
+ assertEquals("one", combined.valueAt(0))
+ assertEquals(2, combined.keyAt(1))
+ assertEquals("two", combined.valueAt(1))
+ }
+
+ @Test fun containsKey() {
+ val array = SparseArrayCompat<String>()
+ assertFalse(array.containsKey(1))
+ array.put(1, "one")
+ assertTrue(array.containsKey(1))
+ }
+
+ @Test fun containsValue() {
+ val array = SparseArrayCompat<String>()
+ assertFalse(array.containsValue("one"))
+ array.put(1, "one")
+ assertTrue(array.containsValue("one"))
+ }
+
+ @Test fun getOrDefault() {
+ val array = SparseArrayCompat<Any>()
+ val default = Any()
+ assertSame(default, array.getOrDefault(1, default))
+ array.put(1, "one")
+ assertEquals("one", array.getOrDefault(1, default))
+ }
+
+ @Test fun getOrElse() {
+ val array = SparseArrayCompat<Any>()
+ val default = Any()
+ assertSame(default, array.getOrElse(1) { default })
+ array.put(1, "one")
+ assertEquals("one", array.getOrElse(1) { fail() })
+ }
+
+ @Test fun isNotEmpty() {
+ val array = SparseArrayCompat<String>()
+ assertFalse(array.isNotEmpty())
+ array.put(1, "one")
+ assertTrue(array.isNotEmpty())
+ }
+
+ @Test fun removeValue() {
+ val array = SparseArrayCompat<String>()
+ array.put(1, "one")
+ assertFalse(array.remove(0, "one"))
+ assertEquals(1, array.size())
+ assertFalse(array.remove(1, "two"))
+ assertEquals(1, array.size())
+ assertTrue(array.remove(1, "one"))
+ assertEquals(0, array.size())
+ }
+
+ @Test fun putAll() {
+ val dest = SparseArrayCompat<String>()
+ val source = SparseArrayCompat<String>()
+ source.put(1, "one")
+
+ assertEquals(0, dest.size())
+ dest.putAll(source)
+ assertEquals(1, dest.size())
+ }
+
+ @Test fun forEach() {
+ val array = SparseArrayCompat<String>()
+ array.forEach { _, _ -> fail() }
+
+ array.put(1, "one")
+ array.put(2, "two")
+ array.put(6, "six")
+
+ val keys = mutableListOf<Int>()
+ val values = mutableListOf<String>()
+ array.forEach { key, value ->
+ keys.add(key)
+ values.add(value)
+ }
+ assertThat(keys).containsExactly(1, 2, 6)
+ assertThat(values).containsExactly("one", "two", "six")
+ }
+
+ @Test fun keyIterator() {
+ val array = SparseArrayCompat<String>()
+ assertFalse(array.keyIterator().hasNext())
+
+ array.put(1, "one")
+ array.put(2, "two")
+ array.put(6, "six")
+
+ val iterator = array.keyIterator()
+ assertTrue(iterator.hasNext())
+ assertEquals(1, iterator.nextInt())
+ assertTrue(iterator.hasNext())
+ assertEquals(2, iterator.nextInt())
+ assertTrue(iterator.hasNext())
+ assertEquals(6, iterator.nextInt())
+ assertFalse(iterator.hasNext())
+ }
+
+ @Test fun valueIterator() {
+ val array = SparseArrayCompat<String>()
+ assertFalse(array.valueIterator().hasNext())
+
+ array.put(1, "one")
+ array.put(2, "two")
+ array.put(6, "six")
+
+ val iterator = array.valueIterator()
+ assertTrue(iterator.hasNext())
+ assertEquals("one", iterator.next())
+ assertTrue(iterator.hasNext())
+ assertEquals("two", iterator.next())
+ assertTrue(iterator.hasNext())
+ assertEquals("six", iterator.next())
+ assertFalse(iterator.hasNext())
+ }
+}
diff --git a/compat/api/current.txt b/compat/api/current.txt
index c80da7b..529cbba 100644
--- a/compat/api/current.txt
+++ b/compat/api/current.txt
@@ -81,6 +81,20 @@
method public static void setExactAndAllowWhileIdle(android.app.AlarmManager, int, long, android.app.PendingIntent);
}
+ public class AppComponentFactory extends android.app.AppComponentFactory {
+ ctor public AppComponentFactory();
+ method public final android.app.Activity instantiateActivity(java.lang.ClassLoader, java.lang.String, android.content.Intent) throws java.lang.ClassNotFoundException, java.lang.IllegalAccessException, java.lang.InstantiationException;
+ method public android.app.Activity instantiateActivityCompat(java.lang.ClassLoader, java.lang.String, android.content.Intent) throws java.lang.ClassNotFoundException, java.lang.IllegalAccessException, java.lang.InstantiationException;
+ method public final android.app.Application instantiateApplication(java.lang.ClassLoader, java.lang.String) throws java.lang.ClassNotFoundException, java.lang.IllegalAccessException, java.lang.InstantiationException;
+ method public android.app.Application instantiateApplicationCompat(java.lang.ClassLoader, java.lang.String) throws java.lang.ClassNotFoundException, java.lang.IllegalAccessException, java.lang.InstantiationException;
+ method public final android.content.ContentProvider instantiateProvider(java.lang.ClassLoader, java.lang.String) throws java.lang.ClassNotFoundException, java.lang.IllegalAccessException, java.lang.InstantiationException;
+ method public android.content.ContentProvider instantiateProviderCompat(java.lang.ClassLoader, java.lang.String) throws java.lang.ClassNotFoundException, java.lang.IllegalAccessException, java.lang.InstantiationException;
+ method public final android.content.BroadcastReceiver instantiateReceiver(java.lang.ClassLoader, java.lang.String, android.content.Intent) throws java.lang.ClassNotFoundException, java.lang.IllegalAccessException, java.lang.InstantiationException;
+ method public android.content.BroadcastReceiver instantiateReceiverCompat(java.lang.ClassLoader, java.lang.String, android.content.Intent) throws java.lang.ClassNotFoundException, java.lang.IllegalAccessException, java.lang.InstantiationException;
+ method public final android.app.Service instantiateService(java.lang.ClassLoader, java.lang.String, android.content.Intent) throws java.lang.ClassNotFoundException, java.lang.IllegalAccessException, java.lang.InstantiationException;
+ method public android.app.Service instantiateServiceCompat(java.lang.ClassLoader, java.lang.String, android.content.Intent) throws java.lang.ClassNotFoundException, java.lang.IllegalAccessException, java.lang.InstantiationException;
+ }
+
public class AppLaunchChecker {
ctor public deprecated AppLaunchChecker();
method public static boolean hasStartedFromLauncher(android.content.Context);
@@ -163,6 +177,7 @@
method public static int getBadgeIconType(android.app.Notification);
method public static java.lang.String getCategory(android.app.Notification);
method public static java.lang.String getChannelId(android.app.Notification);
+ method public static java.lang.CharSequence getContentTitle(android.app.Notification);
method public static android.os.Bundle getExtras(android.app.Notification);
method public static java.lang.String getGroup(android.app.Notification);
method public static int getGroupAlertBehavior(android.app.Notification);
@@ -206,6 +221,7 @@
field public static final java.lang.String EXTRA_LARGE_ICON_BIG = "android.largeIcon.big";
field public static final java.lang.String EXTRA_MEDIA_SESSION = "android.mediaSession";
field public static final java.lang.String EXTRA_MESSAGES = "android.messages";
+ field public static final java.lang.String EXTRA_MESSAGING_STYLE_USER = "android.messagingStyleUser";
field public static final java.lang.String EXTRA_PEOPLE = "android.people";
field public static final java.lang.String EXTRA_PICTURE = "android.picture";
field public static final java.lang.String EXTRA_PROGRESS = "android.progress";
@@ -442,7 +458,8 @@
}
public static class NotificationCompat.MessagingStyle extends androidx.core.app.NotificationCompat.Style {
- ctor public NotificationCompat.MessagingStyle(java.lang.CharSequence);
+ ctor public deprecated NotificationCompat.MessagingStyle(java.lang.CharSequence);
+ ctor public NotificationCompat.MessagingStyle(androidx.core.app.Person);
method public void addCompatExtras(android.os.Bundle);
method public deprecated androidx.core.app.NotificationCompat.MessagingStyle addMessage(java.lang.CharSequence, long, java.lang.CharSequence);
method public androidx.core.app.NotificationCompat.MessagingStyle addMessage(java.lang.CharSequence, long, androidx.core.app.Person);
@@ -450,7 +467,8 @@
method public static androidx.core.app.NotificationCompat.MessagingStyle extractMessagingStyleFromNotification(android.app.Notification);
method public java.lang.CharSequence getConversationTitle();
method public java.util.List<androidx.core.app.NotificationCompat.MessagingStyle.Message> getMessages();
- method public java.lang.CharSequence getUserDisplayName();
+ method public androidx.core.app.Person getUser();
+ method public deprecated java.lang.CharSequence getUserDisplayName();
method public boolean isGroupConversation();
method public androidx.core.app.NotificationCompat.MessagingStyle setConversationTitle(java.lang.CharSequence);
method public androidx.core.app.NotificationCompat.MessagingStyle setGroupConversation(boolean);
@@ -809,6 +827,11 @@
method public static long getLongVersionCode(android.content.pm.PackageInfo);
}
+ public final class PermissionInfoCompat {
+ method public static int getProtection(android.content.pm.PermissionInfo);
+ method public static int getProtectionFlags(android.content.pm.PermissionInfo);
+ }
+
public class ShortcutInfoCompat {
method public android.content.ComponentName getActivity();
method public java.lang.CharSequence getDisabledMessage();
@@ -866,6 +889,10 @@
package androidx.core.database {
+ public final class CursorWindowCompat {
+ method public android.database.CursorWindow create(java.lang.String, long);
+ }
+
public final deprecated class DatabaseUtilsCompat {
method public static deprecated java.lang.String[] appendSelectionArgs(java.lang.String[], java.lang.String[]);
method public static deprecated java.lang.String concatenateWhere(java.lang.String, java.lang.String);
@@ -873,6 +900,14 @@
}
+package androidx.core.database.sqlite {
+
+ public final class SQLiteCursorCompat {
+ method public void setFillWindowForwardOnly(android.database.sqlite.SQLiteCursor, boolean);
+ }
+
+}
+
package androidx.core.graphics {
public final class BitmapCompat {
@@ -1275,6 +1310,40 @@
method public static java.lang.String maximizeAndGetScript(java.util.Locale);
}
+ public class PrecomputedTextCompat implements android.text.Spannable {
+ method public char charAt(int);
+ method public static androidx.core.text.PrecomputedTextCompat create(java.lang.CharSequence, androidx.core.text.PrecomputedTextCompat.Params);
+ method public int getParagraphCount();
+ method public int getParagraphEnd(int);
+ method public int getParagraphStart(int);
+ method public androidx.core.text.PrecomputedTextCompat.Params getParams();
+ method public int getSpanEnd(java.lang.Object);
+ method public int getSpanFlags(java.lang.Object);
+ method public int getSpanStart(java.lang.Object);
+ method public <T> T[] getSpans(int, int, java.lang.Class<T>);
+ method public int length();
+ method public int nextSpanTransition(int, int, java.lang.Class);
+ method public void removeSpan(java.lang.Object);
+ method public void setSpan(java.lang.Object, int, int, int);
+ method public java.lang.CharSequence subSequence(int, int);
+ }
+
+ public static final class PrecomputedTextCompat.Params {
+ ctor public PrecomputedTextCompat.Params(android.text.PrecomputedText.Params);
+ method public int getBreakStrategy();
+ method public int getHyphenationFrequency();
+ method public android.text.TextDirectionHeuristic getTextDirection();
+ method public android.text.TextPaint getTextPaint();
+ }
+
+ public static class PrecomputedTextCompat.Params.Builder {
+ ctor public PrecomputedTextCompat.Params.Builder(android.text.TextPaint);
+ method public androidx.core.text.PrecomputedTextCompat.Params build();
+ method public androidx.core.text.PrecomputedTextCompat.Params.Builder setBreakStrategy(int);
+ method public androidx.core.text.PrecomputedTextCompat.Params.Builder setHyphenationFrequency(int);
+ method public androidx.core.text.PrecomputedTextCompat.Params.Builder setTextDirection(android.text.TextDirectionHeuristic);
+ }
+
public abstract interface TextDirectionHeuristicCompat {
method public abstract boolean isRtl(char[], int, int);
method public abstract boolean isRtl(java.lang.CharSequence, int, int);
@@ -1401,6 +1470,15 @@
method public abstract void onActionProviderVisibilityChanged(boolean);
}
+ public final class DisplayCutoutCompat {
+ ctor public DisplayCutoutCompat(android.graphics.Rect, java.util.List<android.graphics.Rect>);
+ method public java.util.List<android.graphics.Rect> getBoundingRects();
+ method public int getSafeInsetBottom();
+ method public int getSafeInsetLeft();
+ method public int getSafeInsetRight();
+ method public int getSafeInsetTop();
+ }
+
public final class DragAndDropPermissionsCompat {
method public void release();
}
@@ -1908,6 +1986,7 @@
method public static deprecated int getScaledPagingTouchSlop(android.view.ViewConfiguration);
method public static float getScaledVerticalScrollFactor(android.view.ViewConfiguration, android.content.Context);
method public static deprecated boolean hasPermanentMenuKey(android.view.ViewConfiguration);
+ method public static boolean shouldShowMenuShortcutsWhenKeyboardPresent(android.view.ViewConfiguration, android.content.Context);
}
public final class ViewGroupCompat {
@@ -2005,8 +2084,10 @@
public class WindowInsetsCompat {
ctor public WindowInsetsCompat(androidx.core.view.WindowInsetsCompat);
+ method public androidx.core.view.WindowInsetsCompat consumeDisplayCutout();
method public androidx.core.view.WindowInsetsCompat consumeStableInsets();
method public androidx.core.view.WindowInsetsCompat consumeSystemWindowInsets();
+ method public androidx.core.view.DisplayCutoutCompat getDisplayCutout();
method public int getStableInsetBottom();
method public int getStableInsetLeft();
method public int getStableInsetRight();
@@ -2319,7 +2400,7 @@
method public int getColumnSpan();
method public int getRowIndex();
method public int getRowSpan();
- method public boolean isHeading();
+ method public deprecated boolean isHeading();
method public boolean isSelected();
method public static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat obtain(int, int, int, int, boolean, boolean);
method public static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat obtain(int, int, int, int, boolean);
diff --git a/compat/res/values-as/strings.xml b/compat/res/values-as/strings.xml
new file mode 100644
index 0000000..b9c349e
--- /dev/null
+++ b/compat/res/values-as/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="status_bar_notification_info_overflow" msgid="7988687684186075107">"৯৯৯+"</string>
+</resources>
diff --git a/compat/res/values-or/strings.xml b/compat/res/values-or/strings.xml
new file mode 100644
index 0000000..f544aef
--- /dev/null
+++ b/compat/res/values-or/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="status_bar_notification_info_overflow" msgid="7988687684186075107">"999+"</string>
+</resources>
diff --git a/compat/res/values/dimens.xml b/compat/res/values/dimens.xml
index 1dcff5e..9ece458 100644
--- a/compat/res/values/dimens.xml
+++ b/compat/res/values/dimens.xml
@@ -70,4 +70,10 @@
<!-- the paddingtop on the right side of the notification (for time etc.) -->
<dimen name="notification_right_side_padding_top">2dp</dimen>
+
+ <!-- the maximum width of the large icon, above which it will be downscaled -->
+ <dimen name="compat_notification_large_icon_max_width">320dp</dimen>
+
+ <!-- the maximum height of the large icon, above which it will be downscaled -->
+ <dimen name="compat_notification_large_icon_max_height">320dp</dimen>
</resources>
diff --git a/compat/src/androidTest/java/androidx/core/app/NotificationCompatTest.java b/compat/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
index a882891..32daade 100644
--- a/compat/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
+++ b/compat/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
@@ -30,6 +30,7 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import android.app.Notification;
import android.content.Context;
@@ -528,6 +529,29 @@
}
@Test
+ public void testMessagingStyle_nullPerson() {
+ NotificationCompat.MessagingStyle messagingStyle =
+ new NotificationCompat.MessagingStyle("self name");
+ messagingStyle.addMessage("text", 200, (Person) null);
+
+ Notification notification = new NotificationCompat.Builder(mContext, "test id")
+ .setSmallIcon(1)
+ .setContentTitle("test title")
+ .setStyle(messagingStyle)
+ .build();
+
+ List<Message> result = NotificationCompat.MessagingStyle
+ .extractMessagingStyleFromNotification(notification)
+ .getMessages();
+
+ assertEquals(1, result.size());
+ assertEquals("text", result.get(0).getText());
+ assertEquals(200, result.get(0).getTimestamp());
+ assertNull(result.get(0).getPerson());
+ assertNull(result.get(0).getSender());
+ }
+
+ @Test
public void testMessagingStyle_message() {
NotificationCompat.MessagingStyle messagingStyle =
new NotificationCompat.MessagingStyle("self name");
@@ -560,6 +584,16 @@
}
@Test
+ public void testMessagingStyle_requiresNonEmptyUserName() {
+ try {
+ new NotificationCompat.MessagingStyle(new Person.Builder().build());
+ fail("Expected IllegalArgumentException about a non-empty user name.");
+ } catch (IllegalArgumentException e) {
+ // Expected
+ }
+ }
+
+ @Test
public void testMessagingStyle_isGroupConversation() {
mContext.getApplicationInfo().targetSdkVersion = Build.VERSION_CODES.P;
NotificationCompat.MessagingStyle messagingStyle =
@@ -683,19 +717,113 @@
assertTrue(result.isGroupConversation());
}
+ @SdkSuppress(minSdkVersion = 28)
@Test
- public void testMessagingStyle_extras() {
+ public void testMessagingStyle_applyNoTitleAndNotGroup() {
NotificationCompat.MessagingStyle messagingStyle =
- new NotificationCompat.MessagingStyle("test name")
+ new NotificationCompat.MessagingStyle("self name")
+ .setGroupConversation(false)
+ .addMessage(
+ new Message(
+ "body",
+ 1,
+ new Person.Builder().setName("example name").build()))
+ .addMessage(new Message("body 2", 2, (Person) null));
+
+ Notification resultNotification = new NotificationCompat.Builder(mContext, "test id")
+ .setStyle(messagingStyle)
+ .build();
+ NotificationCompat.MessagingStyle resultCompatMessagingStyle =
+ NotificationCompat.MessagingStyle
+ .extractMessagingStyleFromNotification(resultNotification);
+
+ // SDK >= 28 applies no title when none is provided to MessagingStyle.
+ assertNull(resultCompatMessagingStyle.getConversationTitle());
+ assertFalse(resultCompatMessagingStyle.isGroupConversation());
+ }
+
+ @SdkSuppress(minSdkVersion = 24, maxSdkVersion = 27)
+ @Test
+ public void testMessagingStyle_applyNoTitleAndNotGroup_legacy() {
+ NotificationCompat.MessagingStyle messagingStyle =
+ new NotificationCompat.MessagingStyle("self name")
+ .setGroupConversation(false)
+ .addMessage(
+ new Message(
+ "body",
+ 1,
+ new Person.Builder().setName("example name").build()))
+ .addMessage(new Message("body 2", 2, (Person) null));
+
+ Notification resultNotification = new NotificationCompat.Builder(mContext, "test id")
+ .setStyle(messagingStyle)
+ .build();
+ NotificationCompat.MessagingStyle resultCompatMessagingStyle =
+ NotificationCompat.MessagingStyle
+ .extractMessagingStyleFromNotification(resultNotification);
+
+ // SDK [24, 27] applies first incoming message sender name as Notification content title.
+ assertEquals("example name", NotificationCompat.getContentTitle(resultNotification));
+ assertNull(resultCompatMessagingStyle.getConversationTitle());
+ assertFalse(resultCompatMessagingStyle.isGroupConversation());
+ }
+
+ @SdkSuppress(minSdkVersion = 28)
+ @Test
+ public void testMessagingStyle_applyConversationTitleAndNotGroup() {
+ NotificationCompat.MessagingStyle messagingStyle =
+ new NotificationCompat.MessagingStyle("self name")
+ .setGroupConversation(false)
+ .setConversationTitle("test title");
+
+ Notification resultNotification = new NotificationCompat.Builder(mContext, "test id")
+ .setStyle(messagingStyle)
+ .build();
+ NotificationCompat.MessagingStyle resultMessagingStyle =
+ NotificationCompat.MessagingStyle
+ .extractMessagingStyleFromNotification(resultNotification);
+
+ // SDK >= 28 applies provided title to MessagingStyle.
+ assertEquals("test title", resultMessagingStyle.getConversationTitle());
+ assertFalse(resultMessagingStyle.isGroupConversation());
+ }
+
+ @SdkSuppress(minSdkVersion = 19, maxSdkVersion = 27)
+ @Test
+ public void testMessagingStyle_applyConversationTitleAndNotGroup_legacy() {
+ NotificationCompat.MessagingStyle messagingStyle =
+ new NotificationCompat.MessagingStyle("self name")
+ .setGroupConversation(false)
+ .setConversationTitle("test title");
+
+ Notification resultNotification = new NotificationCompat.Builder(mContext, "test id")
+ .setStyle(messagingStyle)
+ .build();
+ NotificationCompat.MessagingStyle resultMessagingStyle =
+ NotificationCompat.MessagingStyle
+ .extractMessagingStyleFromNotification(resultNotification);
+
+ // SDK <= 27 applies MessagingStyle title as Notification content title.
+ assertEquals("test title", NotificationCompat.getContentTitle(resultNotification));
+ assertEquals("test title", resultMessagingStyle.getConversationTitle());
+ assertFalse(resultMessagingStyle.isGroupConversation());
+ }
+
+ @Test
+ public void testMessagingStyle_restoreFromCompatExtras() {
+ NotificationCompat.MessagingStyle messagingStyle =
+ new NotificationCompat.MessagingStyle(
+ new Person.Builder().setName("test name").build())
.setGroupConversation(true);
Bundle bundle = new Bundle();
messagingStyle.addCompatExtras(bundle);
NotificationCompat.MessagingStyle resultMessagingStyle =
- new NotificationCompat.MessagingStyle("test name");
+ new NotificationCompat.MessagingStyle(new Person.Builder().setName("temp").build());
resultMessagingStyle.restoreFromCompatExtras(bundle);
assertTrue(resultMessagingStyle.isGroupConversation());
+ assertEquals("test name", resultMessagingStyle.getUser().getName());
}
@Test
@@ -763,6 +891,16 @@
verifyInvisibleActionExists(notification);
}
+ @Test
+ @SdkSuppress(minSdkVersion = 19)
+ public void getContentTitle() {
+ Notification notification = new NotificationCompat.Builder(mContext, "test channel")
+ .setContentTitle("example title")
+ .build();
+
+ assertEquals("example title", NotificationCompat.getContentTitle(notification));
+ }
+
private static void verifyInvisibleActionExists(Notification notification) {
List<NotificationCompat.Action> result =
NotificationCompat.getInvisibleActions(notification);
diff --git a/compat/src/androidTest/java/androidx/core/content/pm/PermissionInfoCompatTest.java b/compat/src/androidTest/java/androidx/core/content/pm/PermissionInfoCompatTest.java
new file mode 100644
index 0000000..79613be
--- /dev/null
+++ b/compat/src/androidTest/java/androidx/core/content/pm/PermissionInfoCompatTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.content.pm;
+
+import android.content.pm.PermissionInfo;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PermissionInfoCompatTest {
+ @Test
+ public void testGetProtectionAndFlags() {
+ PermissionInfo pi = new PermissionInfo();
+
+ pi.protectionLevel = PermissionInfo.PROTECTION_DANGEROUS
+ | PermissionInfo.PROTECTION_FLAG_PRIVILEGED;
+
+ Assert.assertEquals(PermissionInfo.PROTECTION_DANGEROUS,
+ PermissionInfoCompat.getProtection(pi));
+
+ Assert.assertEquals(PermissionInfo.PROTECTION_FLAG_PRIVILEGED,
+ PermissionInfoCompat.getProtectionFlags(pi));
+
+ pi.protectionLevel = 0xf | 0xfff0;
+ Assert.assertEquals(0xf, PermissionInfoCompat.getProtection(pi));
+ Assert.assertEquals(0xfff0, PermissionInfoCompat.getProtectionFlags(pi));
+ }
+}
diff --git a/compat/src/androidTest/java/androidx/core/graphics/drawable/IconCompatTest.java b/compat/src/androidTest/java/androidx/core/graphics/drawable/IconCompatTest.java
index 75ad1d5..2ca517f 100644
--- a/compat/src/androidTest/java/androidx/core/graphics/drawable/IconCompatTest.java
+++ b/compat/src/androidTest/java/androidx/core/graphics/drawable/IconCompatTest.java
@@ -32,6 +32,7 @@
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -258,7 +259,7 @@
public void testBitmapIconCompat_getType() {
IconCompat icon = IconCompat.createWithBitmap(Bitmap.createBitmap(16, 16,
Bitmap.Config.ARGB_8888));
- assertEquals(IconCompat.TYPE_BITMAP, icon.getType());
+ assertEquals(Icon.TYPE_BITMAP, icon.getType());
}
@Test
@@ -266,7 +267,7 @@
byte[] data = new byte[4];
data[0] = data[1] = data[2] = data[3] = (byte) 255;
IconCompat icon = IconCompat.createWithData(data, 0, 4);
- assertEquals(IconCompat.TYPE_DATA, icon.getType());
+ assertEquals(Icon.TYPE_DATA, icon.getType());
}
@Test
@@ -278,11 +279,11 @@
String filePath = file.toURI().getPath();
IconCompat icon = IconCompat.createWithContentUri(Uri.fromFile(file));
- assertEquals(IconCompat.TYPE_URI, icon.getType());
+ assertEquals(Icon.TYPE_URI, icon.getType());
assertEquals(filePath, icon.getUri().getPath());
icon = IconCompat.createWithContentUri(file.toURI().toString());
- assertEquals(IconCompat.TYPE_URI, icon.getType());
+ assertEquals(Icon.TYPE_URI, icon.getType());
assertEquals(filePath, icon.getUri().getPath());
} finally {
file.delete();
@@ -292,7 +293,7 @@
@Test
public void testResourceIconCompat_getType() {
IconCompat icon = IconCompat.createWithResource(mContext, R.drawable.bmp_test);
- assertEquals(IconCompat.TYPE_RESOURCE, icon.getType());
+ assertEquals(Icon.TYPE_RESOURCE, icon.getType());
assertEquals("androidx.core.test", icon.getResPackage());
assertEquals(R.drawable.bmp_test, icon.getResId());
}
diff --git a/compat/src/androidTest/java/androidx/core/text/PrecomputedTextCompatTest.java b/compat/src/androidTest/java/androidx/core/text/PrecomputedTextCompatTest.java
new file mode 100644
index 0000000..a822b7c
--- /dev/null
+++ b/compat/src/androidTest/java/androidx/core/text/PrecomputedTextCompatTest.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.text;
+
+import static android.text.TextDirectionHeuristics.LTR;
+import static android.text.TextDirectionHeuristics.RTL;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.graphics.Color;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.text.Layout;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextDirectionHeuristics;
+import android.text.TextPaint;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.TypefaceSpan;
+
+import androidx.core.text.PrecomputedTextCompat.Params;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PrecomputedTextCompatTest {
+
+ private static final CharSequence NULL_CHAR_SEQUENCE = null;
+ private static final String STRING = "Hello, World!";
+ private static final String MULTIPARA_STRING = "Hello,\nWorld!";
+
+ private static final int SPAN_START = 3;
+ private static final int SPAN_END = 7;
+ private static final TypefaceSpan SPAN = new TypefaceSpan("serif");
+ private static final Spanned SPANNED;
+ static {
+ final SpannableStringBuilder ssb = new SpannableStringBuilder(STRING);
+ ssb.setSpan(SPAN, SPAN_START, SPAN_END, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ SPANNED = ssb;
+ }
+
+ private static final TextPaint PAINT = new TextPaint();
+
+ @Test
+ public void testParams_create() {
+ assertNotNull(new Params.Builder(PAINT).build());
+ assertNotNull(new Params.Builder(PAINT)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE).build());
+ assertNotNull(new Params.Builder(PAINT)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL).build());
+ assertNotNull(new Params.Builder(PAINT)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+ .setTextDirection(LTR).build());
+ }
+
+ @Test
+ public void testParams_SetGet() {
+ assertEquals(Layout.BREAK_STRATEGY_SIMPLE, new Params.Builder(PAINT)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE).build().getBreakStrategy());
+ assertEquals(Layout.HYPHENATION_FREQUENCY_NONE, new Params.Builder(PAINT)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE).build()
+ .getHyphenationFrequency());
+ assertEquals(RTL, new Params.Builder(PAINT).setTextDirection(RTL).build()
+ .getTextDirection());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 23)
+ public void testParams_GetDefaultValues() {
+ assertEquals(Layout.BREAK_STRATEGY_HIGH_QUALITY,
+ new Params.Builder(PAINT).build().getBreakStrategy());
+ assertEquals(Layout.HYPHENATION_FREQUENCY_NORMAL,
+ new Params.Builder(PAINT).build().getHyphenationFrequency());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 18)
+ public void testParams_GetDefaultValues2() {
+ assertEquals(TextDirectionHeuristics.FIRSTSTRONG_LTR,
+ new Params.Builder(PAINT).build().getTextDirection());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 23)
+ public void testParams_equals() {
+ final Params base = new Params.Builder(PAINT)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+ .setTextDirection(LTR).build();
+
+ assertFalse(base.equals(null));
+ assertTrue(base.equals(base));
+ assertFalse(base.equals(new Object()));
+
+ Params other = new Params.Builder(PAINT)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+ .setTextDirection(LTR).build();
+ assertTrue(base.equals(other));
+ assertTrue(other.equals(base));
+ assertEquals(base.hashCode(), other.hashCode());
+
+ other = new Params.Builder(PAINT)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+ .setTextDirection(LTR).build();
+ assertFalse(base.equals(other));
+ assertFalse(other.equals(base));
+
+ other = new Params.Builder(PAINT)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
+ .setTextDirection(LTR).build();
+ assertFalse(base.equals(other));
+ assertFalse(other.equals(base));
+
+
+ other = new Params.Builder(PAINT)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+ .setTextDirection(RTL).build();
+ assertFalse(base.equals(other));
+ assertFalse(other.equals(base));
+
+
+ TextPaint anotherPaint = new TextPaint(PAINT);
+ anotherPaint.setTextSize(PAINT.getTextSize() * 2.0f);
+ other = new Params.Builder(anotherPaint)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+ .setTextDirection(LTR).build();
+ assertFalse(base.equals(other));
+ assertFalse(other.equals(base));
+ }
+
+ @Test
+ public void testParams_equals2() {
+ final Params base = new Params.Builder(PAINT).build();
+
+ assertFalse(base.equals(null));
+ assertTrue(base.equals(base));
+ assertFalse(base.equals(new Object()));
+
+ Params other = new Params.Builder(PAINT).build();
+ assertTrue(base.equals(other));
+ assertTrue(other.equals(base));
+ assertEquals(base.hashCode(), other.hashCode());
+
+ TextPaint paint = new TextPaint(PAINT);
+ paint.setTextSize(paint.getTextSize() * 2.0f + 1.0f);
+ other = new Params.Builder(paint).build();
+ assertFalse(base.equals(other));
+ assertFalse(other.equals(base));
+ }
+
+ @Test
+ public void testCreate_withNull() {
+ final Params param = new Params.Builder(PAINT).build();
+ try {
+ PrecomputedTextCompat.create(NULL_CHAR_SEQUENCE, param);
+ fail();
+ } catch (NullPointerException e) {
+ // pass
+ }
+ try {
+ PrecomputedTextCompat.create(STRING, null);
+ fail();
+ } catch (NullPointerException e) {
+ // pass
+ }
+ }
+
+ @Test
+ public void testCharSequenceInteface() {
+ final Params param = new Params.Builder(PAINT).build();
+ final CharSequence s = PrecomputedTextCompat.create(STRING, param);
+ assertEquals(STRING.length(), s.length());
+ assertEquals('H', s.charAt(0));
+ assertEquals('e', s.charAt(1));
+ assertEquals('l', s.charAt(2));
+ assertEquals('l', s.charAt(3));
+ assertEquals('o', s.charAt(4));
+ assertEquals(',', s.charAt(5));
+ assertEquals("Hello, World!", s.toString());
+
+ final CharSequence s3 = s.subSequence(0, 3);
+ assertEquals(3, s3.length());
+ assertEquals('H', s3.charAt(0));
+ assertEquals('e', s3.charAt(1));
+ assertEquals('l', s3.charAt(2));
+
+ }
+
+ @Test
+ public void testSpannedInterface_Spanned() {
+ final Params param = new Params.Builder(PAINT).build();
+ final Spanned s = PrecomputedTextCompat.create(SPANNED, param);
+ final TypefaceSpan[] spans = s.getSpans(0, s.length(), TypefaceSpan.class);
+ assertNotNull(spans);
+ assertEquals(1, spans.length);
+ assertEquals(SPAN, spans[0]);
+
+ assertEquals(SPAN_START, s.getSpanStart(SPAN));
+ assertEquals(SPAN_END, s.getSpanEnd(SPAN));
+ assertTrue((s.getSpanFlags(SPAN) & Spanned.SPAN_INCLUSIVE_EXCLUSIVE) != 0);
+
+ assertEquals(SPAN_START, s.nextSpanTransition(0, s.length(), TypefaceSpan.class));
+ assertEquals(SPAN_END, s.nextSpanTransition(SPAN_START, s.length(), TypefaceSpan.class));
+ }
+
+ @Test
+ public void testSpannedInterface_Spannable() {
+ final BackgroundColorSpan span = new BackgroundColorSpan(Color.RED);
+ final Params param = new Params.Builder(PAINT).build();
+ final Spannable s = PrecomputedTextCompat.create(STRING, param);
+ assertEquals(0, s.getSpans(0, s.length(), BackgroundColorSpan.class).length);
+
+ s.setSpan(span, SPAN_START, SPAN_END, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+
+ final BackgroundColorSpan[] spans = s.getSpans(0, s.length(), BackgroundColorSpan.class);
+ assertEquals(SPAN_START, s.getSpanStart(span));
+ assertEquals(SPAN_END, s.getSpanEnd(span));
+ assertTrue((s.getSpanFlags(span) & Spanned.SPAN_INCLUSIVE_EXCLUSIVE) != 0);
+
+ assertEquals(SPAN_START, s.nextSpanTransition(0, s.length(), BackgroundColorSpan.class));
+ assertEquals(SPAN_END,
+ s.nextSpanTransition(SPAN_START, s.length(), BackgroundColorSpan.class));
+
+ s.removeSpan(span);
+ assertEquals(0, s.getSpans(0, s.length(), BackgroundColorSpan.class).length);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSpannedInterface_Spannable_setSpan_MetricsAffectingSpan() {
+ final Params param = new Params.Builder(PAINT).build();
+ final Spannable s = PrecomputedTextCompat.create(SPANNED, param);
+ s.setSpan(SPAN, SPAN_START, SPAN_END, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSpannedInterface_Spannable_removeSpan_MetricsAffectingSpan() {
+ final Params param = new Params.Builder(PAINT).build();
+ final Spannable s = PrecomputedTextCompat.create(SPANNED, param);
+ s.removeSpan(SPAN);
+ }
+
+ @Test
+ public void testSpannedInterface_String() {
+ final Params param = new Params.Builder(PAINT).build();
+ final Spanned s = PrecomputedTextCompat.create(STRING, param);
+ TypefaceSpan[] spans = s.getSpans(0, s.length(), TypefaceSpan.class);
+ assertNotNull(spans);
+ assertEquals(0, spans.length);
+
+ assertEquals(-1, s.getSpanStart(SPAN));
+ assertEquals(-1, s.getSpanEnd(SPAN));
+ assertEquals(0, s.getSpanFlags(SPAN));
+
+ assertEquals(s.length(), s.nextSpanTransition(0, s.length(), TypefaceSpan.class));
+ }
+
+ @Test
+ public void testGetParagraphCount() {
+ final Params param = new Params.Builder(PAINT).build();
+ final PrecomputedTextCompat pm = PrecomputedTextCompat.create(STRING, param);
+ assertEquals(1, pm.getParagraphCount());
+ assertEquals(0, pm.getParagraphStart(0));
+ assertEquals(STRING.length(), pm.getParagraphEnd(0));
+
+ final PrecomputedTextCompat pm1 = PrecomputedTextCompat.create(MULTIPARA_STRING, param);
+ assertEquals(2, pm1.getParagraphCount());
+ assertEquals(0, pm1.getParagraphStart(0));
+ assertEquals(7, pm1.getParagraphEnd(0));
+ assertEquals(7, pm1.getParagraphStart(1));
+ assertEquals(pm1.length(), pm1.getParagraphEnd(1));
+ }
+
+}
diff --git a/compat/src/androidTest/java/androidx/core/text/util/FindAddressTest.java b/compat/src/androidTest/java/androidx/core/text/util/FindAddressTest.java
index 292f5b1..9cfa78a 100644
--- a/compat/src/androidTest/java/androidx/core/text/util/FindAddressTest.java
+++ b/compat/src/androidTest/java/androidx/core/text/util/FindAddressTest.java
@@ -138,6 +138,15 @@
public void testFullAddressWithoutZipCode() {
assertIsAddress("1600 Amphitheatre Parkway Mountain View, CA");
assertIsAddress("201 S. Division St. Suite 500 Ann Arbor, MI");
+
+ // Check that addresses without a zip code are only accepted at the end of the string.
+ // This isn't implied by the documentation but was the case in the old implementation
+ // and fixing this bug creates a lot of false positives while fixing relatively few
+ // false negatives. In these examples, "one point" is parsed as a street and "as" is a
+ // state abbreviation (this is taken from a false positive reported in a bug).
+ Assert.assertTrue(containsAddress("one point I was as"));
+ Assert.assertTrue(containsAddress("At one point I was as ignorant as"));
+ Assert.assertFalse(containsAddress("At one point I was as ignorant as them"));
}
@Test
diff --git a/compat/src/androidTest/java/androidx/core/text/util/LinkifyCompatTest.java b/compat/src/androidTest/java/androidx/core/text/util/LinkifyCompatTest.java
index 8435df1..4400d22 100644
--- a/compat/src/androidTest/java/androidx/core/text/util/LinkifyCompatTest.java
+++ b/compat/src/androidTest/java/androidx/core/text/util/LinkifyCompatTest.java
@@ -781,12 +781,6 @@
// ADDRESS RELATED TESTS
@Test
- public void testFindAddress_withoutZipcode() {
- final String address = "455 LARKSPUR DRIVE CALIFORNIA SPRINGS CALIFORNIA";
- verifyAddLinksWithMapAddressSucceeds("Should match map address: " + address, address);
- }
-
- @Test
public void testFindAddress_withZipcode() {
final String address = "455 LARKSPUR DRIVE CALIFORNIA SPRINGS CALIFORNIA 92826";
verifyAddLinksWithMapAddressSucceeds("Should match map address: " + address, address);
diff --git a/compat/src/androidTest/java/androidx/core/view/DisplayCutoutCompatTest.java b/compat/src/androidTest/java/androidx/core/view/DisplayCutoutCompatTest.java
new file mode 100644
index 0000000..88a31f6
--- /dev/null
+++ b/compat/src/androidTest/java/androidx/core/view/DisplayCutoutCompatTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.view;
+
+import static android.os.Build.VERSION.SDK_INT;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+
+import android.graphics.Rect;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DisplayCutoutCompatTest {
+
+ DisplayCutoutCompat mCutoutTop;
+ DisplayCutoutCompat mCutoutTopBottom;
+ DisplayCutoutCompat mCutoutTopBottomClone;
+ DisplayCutoutCompat mCutoutLeftRight;
+
+ @Before
+ public void setUp() throws Exception {
+ mCutoutTop = new DisplayCutoutCompat(new Rect(0, 10, 0, 0), Arrays.asList(
+ new Rect(50, 0, 60, 10)));
+ mCutoutTopBottom = new DisplayCutoutCompat(new Rect(0, 10, 0, 20), Arrays.asList(
+ new Rect(50, 0, 60, 10),
+ new Rect(50, 100, 60, 120)));
+ mCutoutTopBottomClone = new DisplayCutoutCompat(new Rect(0, 10, 0, 20), Arrays.asList(
+ new Rect(50, 0, 60, 10),
+ new Rect(50, 100, 60, 120)));
+ mCutoutLeftRight = new DisplayCutoutCompat(new Rect(30, 0, 40, 0), Arrays.asList(
+ new Rect(0, 50, 30, 60),
+ new Rect(100, 60, 140, 50)));
+ }
+
+ @Test
+ public void testSafeInsets() {
+ if (SDK_INT >= 28) {
+ assertEquals("left", 30, mCutoutLeftRight.getSafeInsetLeft());
+ assertEquals("top", 10, mCutoutTopBottom.getSafeInsetTop());
+ assertEquals("right", 40, mCutoutLeftRight.getSafeInsetRight());
+ assertEquals("bottom", 20, mCutoutTopBottom.getSafeInsetBottom());
+ } else {
+ assertEquals("left", 0, mCutoutLeftRight.getSafeInsetLeft());
+ assertEquals("top", 0, mCutoutTopBottom.getSafeInsetTop());
+ assertEquals("right", 0, mCutoutLeftRight.getSafeInsetRight());
+ assertEquals("bottom", 0, mCutoutTopBottom.getSafeInsetBottom());
+ }
+ }
+
+ @Test
+ public void testBoundingRects() {
+ if (SDK_INT >= 28) {
+ assertEquals(Arrays.asList(new Rect(50, 0, 60, 10)), mCutoutTop.getBoundingRects());
+ } else {
+ assertNull(mCutoutTop.getBoundingRects());
+ }
+ }
+
+ @Test
+ public void testEquals() {
+ assertEquals(mCutoutTopBottomClone, mCutoutTopBottom);
+
+ if (SDK_INT >= 28) {
+ assertNotEquals(mCutoutTopBottom, mCutoutLeftRight);
+ }
+ }
+ @Test
+ public void testHashCode() {
+ assertEquals(mCutoutTopBottomClone.hashCode(), mCutoutTopBottom.hashCode());
+ }
+}
diff --git a/compat/src/androidTest/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompatTest.java b/compat/src/androidTest/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompatTest.java
index 7b862c2..3e668b4 100644
--- a/compat/src/androidTest/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompatTest.java
+++ b/compat/src/androidTest/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompatTest.java
@@ -19,9 +19,9 @@
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertThat;
-import static org.junit.Assume.assumeTrue;
import android.os.Build;
+import android.support.test.filters.SdkSuppress;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.view.accessibility.AccessibilityNodeInfo;
@@ -68,9 +68,9 @@
assertThat(nodeCompat.getTooltipText(), equalTo(tooltipText));
}
+ @SdkSuppress(minSdkVersion = 19)
@Test
public void testGetSetShowingHintText() {
- assumeTrue(Build.VERSION.SDK_INT >= 19);
AccessibilityNodeInfoCompat nodeCompat = obtainedWrappedNodeCompat();
nodeCompat.setShowingHintText(true);
assertThat(nodeCompat.isShowingHintText(), is(true));
@@ -78,9 +78,9 @@
assertThat(nodeCompat.isShowingHintText(), is(false));
}
+ @SdkSuppress(minSdkVersion = 19)
@Test
public void testGetSetScreenReaderFocusable() {
- assumeTrue(Build.VERSION.SDK_INT >= 19);
AccessibilityNodeInfoCompat nodeCompat = obtainedWrappedNodeCompat();
nodeCompat.setScreenReaderFocusable(true);
assertThat(nodeCompat.isScreenReaderFocusable(), is(true));
@@ -88,14 +88,18 @@
assertThat(nodeCompat.isScreenReaderFocusable(), is(false));
}
+ @SdkSuppress(minSdkVersion = 19)
@Test
public void testGetSetHeading() {
- assumeTrue(Build.VERSION.SDK_INT >= 19);
AccessibilityNodeInfoCompat nodeCompat = obtainedWrappedNodeCompat();
nodeCompat.setHeading(true);
assertThat(nodeCompat.isHeading(), is(true));
nodeCompat.setHeading(false);
assertThat(nodeCompat.isHeading(), is(false));
+ AccessibilityNodeInfoCompat.CollectionItemInfoCompat collectionItemInfo =
+ AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(0, 1, 0, 1, true);
+ nodeCompat.setCollectionItemInfo(collectionItemInfo);
+ assertThat(nodeCompat.isHeading(), is(true));
}
private AccessibilityNodeInfoCompat obtainedWrappedNodeCompat() {
diff --git a/compat/src/main/AndroidManifest.xml b/compat/src/main/AndroidManifest.xml
index d18ee92..6c4338d 100644
--- a/compat/src/main/AndroidManifest.xml
+++ b/compat/src/main/AndroidManifest.xml
@@ -13,4 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<manifest package="androidx.core"/>
+<manifest package="androidx.core" xmlns:android="http://schemas.android.com/apk/res/android">
+ <application
+ android:appComponentFactory="androidx.core.app.CoreComponentFactory" />
+</manifest>
diff --git a/compat/src/main/java/androidx/core/app/AppComponentFactory.java b/compat/src/main/java/androidx/core/app/AppComponentFactory.java
new file mode 100644
index 0000000..b6383da
--- /dev/null
+++ b/compat/src/main/java/androidx/core/app/AppComponentFactory.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.app;
+
+import static androidx.core.app.CoreComponentFactory.checkCompatWrapper;
+
+import android.app.Activity;
+import android.app.Application;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.ContentProvider;
+import android.content.Intent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * Version of {@link android.app.AppComponentFactory} that works with androidx libraries.
+ *
+ * Note: This will only work on API 28+ and does not backport AppComponentFactory functionality.
+ */
+@RequiresApi(28)
+public class AppComponentFactory extends android.app.AppComponentFactory {
+
+ /**
+ * @see #instantiateActivityCompat
+ */
+ @Override
+ public final Activity instantiateActivity(ClassLoader cl, String className, Intent intent)
+ throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+ return checkCompatWrapper(instantiateActivityCompat(cl, className, intent));
+ }
+
+ /**
+ * @see #instantiateApplicationCompat
+ */
+ @Override
+ public final Application instantiateApplication(ClassLoader cl, String className)
+ throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+ return checkCompatWrapper(instantiateApplicationCompat(cl, className));
+ }
+
+ /**
+ * @see #instantiateReceiverCompat
+ */
+ @Override
+ public final BroadcastReceiver instantiateReceiver(ClassLoader cl, String className,
+ Intent intent)
+ throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+ return checkCompatWrapper(instantiateReceiverCompat(cl, className, intent));
+ }
+
+ /**
+ * @see #instantiateProviderCompat
+ */
+ @Override
+ public final ContentProvider instantiateProvider(ClassLoader cl, String className)
+ throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+ return checkCompatWrapper(instantiateProviderCompat(cl, className));
+ }
+
+ /**
+ * @see #instantiateServiceCompat
+ */
+ @Override
+ public final Service instantiateService(ClassLoader cl, String className, Intent intent)
+ throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+ return checkCompatWrapper(instantiateServiceCompat(cl, className, intent));
+ }
+
+ /**
+ * Allows application to override the creation of the application object. This can be used to
+ * perform things such as dependency injection or class loader changes to these
+ * classes.
+ * <p>
+ * This method is only intended to provide a hook for instantiation. It does not provide
+ * earlier access to the Application object. The returned object will not be initialized
+ * as a Context yet and should not be used to interact with other android APIs.
+ *
+ * @param cl The default classloader to use for instantiation.
+ * @param className The class to be instantiated.
+ */
+ public @NonNull Application instantiateApplicationCompat(@NonNull ClassLoader cl,
+ @NonNull String className)
+ throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+ try {
+ return (Application) cl.loadClass(className).getDeclaredConstructor().newInstance();
+ } catch (InvocationTargetException | NoSuchMethodException e) {
+ throw new RuntimeException("Couldn't call constructor", e);
+ }
+ }
+
+ /**
+ * Allows application to override the creation of activities. This can be used to
+ * perform things such as dependency injection or class loader changes to these
+ * classes.
+ * <p>
+ * This method is only intended to provide a hook for instantiation. It does not provide
+ * earlier access to the Activity object. The returned object will not be initialized
+ * as a Context yet and should not be used to interact with other android APIs.
+ *
+ * @param cl The default classloader to use for instantiation.
+ * @param className The class to be instantiated.
+ * @param intent Intent creating the class.
+ */
+ public @NonNull Activity instantiateActivityCompat(@NonNull ClassLoader cl,
+ @NonNull String className, @Nullable Intent intent)
+ throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+ try {
+ return (Activity) cl.loadClass(className).getDeclaredConstructor().newInstance();
+ } catch (InvocationTargetException | NoSuchMethodException e) {
+ throw new RuntimeException("Couldn't call constructor", e);
+ }
+ }
+
+ /**
+ * Allows application to override the creation of receivers. This can be used to
+ * perform things such as dependency injection or class loader changes to these
+ * classes.
+ *
+ * @param cl The default classloader to use for instantiation.
+ * @param className The class to be instantiated.
+ * @param intent Intent creating the class.
+ */
+ public @NonNull BroadcastReceiver instantiateReceiverCompat(@NonNull ClassLoader cl,
+ @NonNull String className, @Nullable Intent intent)
+ throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+ try {
+ return (BroadcastReceiver) cl.loadClass(className).getDeclaredConstructor()
+ .newInstance();
+ } catch (InvocationTargetException | NoSuchMethodException e) {
+ throw new RuntimeException("Couldn't call constructor", e);
+ }
+ }
+
+ /**
+ * Allows application to override the creation of services. This can be used to
+ * perform things such as dependency injection or class loader changes to these
+ * classes.
+ * <p>
+ * This method is only intended to provide a hook for instantiation. It does not provide
+ * earlier access to the Service object. The returned object will not be initialized
+ * as a Context yet and should not be used to interact with other android APIs.
+ *
+ * @param cl The default classloader to use for instantiation.
+ * @param className The class to be instantiated.
+ * @param intent Intent creating the class.
+ */
+ public @NonNull Service instantiateServiceCompat(@NonNull ClassLoader cl,
+ @NonNull String className, @Nullable Intent intent)
+ throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+ try {
+ return (Service) cl.loadClass(className).getDeclaredConstructor().newInstance();
+ } catch (InvocationTargetException | NoSuchMethodException e) {
+ throw new RuntimeException("Couldn't call constructor", e);
+ }
+ }
+
+ /**
+ * Allows application to override the creation of providers. This can be used to
+ * perform things such as dependency injection or class loader changes to these
+ * classes.
+ * <p>
+ * This method is only intended to provide a hook for instantiation. It does not provide
+ * earlier access to the ContentProvider object. The returned object will not be initialized
+ * with a Context yet and should not be used to interact with other android APIs.
+ *
+ * @param cl The default classloader to use for instantiation.
+ * @param className The class to be instantiated.
+ */
+ public @NonNull ContentProvider instantiateProviderCompat(@NonNull ClassLoader cl,
+ @NonNull String className)
+ throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+ try {
+ return (ContentProvider) cl.loadClass(className).getDeclaredConstructor().newInstance();
+ } catch (InvocationTargetException | NoSuchMethodException e) {
+ throw new RuntimeException("Couldn't call constructor", e);
+ }
+ }
+}
diff --git a/compat/src/main/java/androidx/core/app/CoreComponentFactory.java b/compat/src/main/java/androidx/core/app/CoreComponentFactory.java
new file mode 100644
index 0000000..b55f03f
--- /dev/null
+++ b/compat/src/main/java/androidx/core/app/CoreComponentFactory.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.app;
+
+import android.app.Activity;
+import android.app.AppComponentFactory;
+import android.app.Application;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.ContentProvider;
+import android.content.Intent;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+/**
+ * Instance of AppComponentFactory for support libraries.
+ * @see CompatWrapped
+ * @hide
+ */
+@RequiresApi(api = 28)
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class CoreComponentFactory extends AppComponentFactory {
+ private static final String TAG = "CoreComponentFactory";
+
+ @Override
+ public Activity instantiateActivity(ClassLoader cl, String className, Intent intent)
+ throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+ return checkCompatWrapper(super.instantiateActivity(cl, className, intent));
+ }
+
+ @Override
+ public Application instantiateApplication(ClassLoader cl, String className)
+ throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+ return checkCompatWrapper(super.instantiateApplication(cl, className));
+ }
+
+ @Override
+ public BroadcastReceiver instantiateReceiver(ClassLoader cl, String className,
+ Intent intent)
+ throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+ return checkCompatWrapper(super.instantiateReceiver(cl, className, intent));
+ }
+
+ @Override
+ public ContentProvider instantiateProvider(ClassLoader cl, String className)
+ throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+ return checkCompatWrapper(super.instantiateProvider(cl, className));
+ }
+
+ @Override
+ public Service instantiateService(ClassLoader cl, String className, Intent intent)
+ throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+ return checkCompatWrapper(super.instantiateService(cl, className, intent));
+ }
+
+ static <T> T checkCompatWrapper(T obj) {
+ if (obj instanceof CompatWrapped) {
+ T wrapper = (T) ((CompatWrapped) obj).getWrapper();
+ if (wrapper != null) {
+ return wrapper;
+ }
+ }
+ return obj;
+ }
+
+ /**
+ * Implement this interface to allow a different class to be returned when instantiating
+ * on certain API levels.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public interface CompatWrapped {
+ /**
+ * Called while this class is being instantiated by the OS.
+ *
+ * If an object is returned then it will be used in place of the class.
+ * Note: this will not be called on API <= 27.
+ *
+ * Example:
+ * <pre class="prettyprint">
+ * {@literal
+ * public AndroidXContentProvider extends ContentProvider implements CompatWrapped {
+ * ...
+ *
+ * public Object getWrapper() {
+ * if (SDK_INT >= 29) {
+ * return new AndroidXContentProviderV29(this);
+ * }
+ * return null;
+ * }
+ * }
+ * }
+ * </pre>
+ */
+ Object getWrapper();
+ }
+
+}
diff --git a/compat/src/main/java/androidx/core/app/NotificationCompat.java b/compat/src/main/java/androidx/core/app/NotificationCompat.java
index caf94e6..4d7bf40 100644
--- a/compat/src/main/java/androidx/core/app/NotificationCompat.java
+++ b/compat/src/main/java/androidx/core/app/NotificationCompat.java
@@ -386,13 +386,17 @@
/**
* Notification key: the username to be displayed for all messages sent by the user
- * including
- * direct replies
- * {@link MessagingStyle} notification.
+ * including direct replies {@link MessagingStyle} notification.
*/
public static final String EXTRA_SELF_DISPLAY_NAME = "android.selfDisplayName";
/**
+ * Notification key: the person to display for all messages sent by the user, including direct
+ * replies to {@link MessagingStyle} notifications.
+ */
+ public static final String EXTRA_MESSAGING_STYLE_USER = "android.messagingStyleUser";
+
+ /**
* Notification key: a {@link String} to be displayed as the title to a conversation
* represented by a {@link MessagingStyle}
*/
@@ -963,11 +967,39 @@
* Set the large icon that is shown in the ticker and notification.
*/
public Builder setLargeIcon(Bitmap icon) {
- mLargeIcon = icon;
+ mLargeIcon = reduceLargeIconSize(icon);
return this;
}
/**
+ * Reduce the size of a notification icon if it's overly large. The framework does
+ * this automatically starting from API 27.
+ */
+ private Bitmap reduceLargeIconSize(Bitmap icon) {
+ if (icon == null || Build.VERSION.SDK_INT >= 27) {
+ return icon;
+ }
+
+ Resources res = mContext.getResources();
+ int maxWidth =
+ res.getDimensionPixelSize(R.dimen.compat_notification_large_icon_max_width);
+ int maxHeight =
+ res.getDimensionPixelSize(R.dimen.compat_notification_large_icon_max_height);
+ if (icon.getWidth() <= maxWidth && icon.getHeight() <= maxHeight) {
+ return icon;
+ }
+
+ double scale = Math.min(
+ maxWidth / (double) Math.max(1, icon.getWidth()),
+ maxHeight / (double) Math.max(1, icon.getHeight()));
+ return Bitmap.createScaledBitmap(
+ icon,
+ (int) Math.ceil(icon.getWidth() * scale),
+ (int) Math.ceil(icon.getHeight() * scale),
+ true /* filtered */);
+ }
+
+ /**
* Set the sound to play. It will play on the default stream.
*
* <p>
@@ -1644,6 +1676,9 @@
}
/**
+ * Applies the compat style data to the framework {@link Notification} in a backwards
+ * compatible way. All other data should be stored within the Notification's extras.
+ *
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
@@ -2123,29 +2158,54 @@
*/
public static final int MAXIMUM_RETAINED_MESSAGES = 25;
- CharSequence mUserDisplayName;
- @Nullable CharSequence mConversationTitle;
- List<Message> mMessages = new ArrayList<>();
- @Nullable Boolean mIsGroupConversation;
+ private final List<Message> mMessages = new ArrayList<>();
+ private Person mUser;
+ private @Nullable CharSequence mConversationTitle;
+ private @Nullable Boolean mIsGroupConversation;
- MessagingStyle() {
- }
+ /** Private empty constructor for {@link Style#restoreFromCompatExtras(Bundle)}. */
+ private MessagingStyle() {}
/**
* @param userDisplayName Required - the name to be displayed for any replies sent by the
* user before the posting app reposts the notification with those messages after they've
* been actually sent and in previous messages sent by the user added in
* {@link #addMessage(Message)}
+ * @deprecated Use {@code #MessagingStyle(Person)} instead.
*/
+ @Deprecated
public MessagingStyle(@NonNull CharSequence userDisplayName) {
- mUserDisplayName = userDisplayName;
+ mUser = new Person.Builder().setName(userDisplayName).build();
}
/**
- * Returns the name to be displayed for any replies sent by the user
+ * Creates a new {@link MessagingStyle} object. Note that {@link Person} must have a
+ * non-empty name.
+ *
+ * @param user This {@link Person}'s name will be shown when this app's notification is
+ * being replied to. It's used temporarily so the app has time to process the send request
+ * and repost the notification with updates to the conversation.
*/
+ public MessagingStyle(@NonNull Person user) {
+ if (TextUtils.isEmpty(user.getName())) {
+ throw new IllegalArgumentException("User's name must not be empty.");
+ }
+ mUser = user;
+ }
+
+ /**
+ * Returns the name to be displayed for any replies sent by the user.
+ *
+ * @deprecated Use {@link #getUser()} instead.
+ */
+ @Deprecated
public CharSequence getUserDisplayName() {
- return mUserDisplayName;
+ return mUser.getName();
+ }
+
+ /** Returns the person to be used for any replies sent by the user. */
+ public Person getUser() {
+ return mUser;
}
/**
@@ -2286,19 +2346,20 @@
*/
public static MessagingStyle extractMessagingStyleFromNotification(
Notification notification) {
- MessagingStyle style;
Bundle extras = NotificationCompat.getExtras(notification);
- if (extras != null && !extras.containsKey(EXTRA_SELF_DISPLAY_NAME)) {
- style = null;
- } else {
- try {
- style = new MessagingStyle();
- style.restoreFromCompatExtras(extras);
- } catch (ClassCastException e) {
- style = null;
- }
+ if (extras != null
+ && !extras.containsKey(EXTRA_SELF_DISPLAY_NAME)
+ && !extras.containsKey(EXTRA_MESSAGING_STYLE_USER)) {
+ return null;
}
- return style;
+
+ try {
+ MessagingStyle style = new MessagingStyle();
+ style.restoreFromCompatExtras(extras);
+ return style;
+ } catch (ClassCastException e) {
+ return null;
+ }
}
/**
@@ -2315,14 +2376,36 @@
if (Build.VERSION.SDK_INT >= 24) {
Notification.MessagingStyle style =
- new Notification.MessagingStyle(mUserDisplayName)
- .setConversationTitle(mConversationTitle);
+ new Notification.MessagingStyle(mUser.getName());
+
+ // In SDK < 28, base Android will assume a MessagingStyle notification is a group
+ // chat if the conversation title is set. In compat, this isn't the case as we've
+ // introduced #setGroupConversation. When we apply these settings to base Android
+ // notifications, we should only set base Android's MessagingStyle conversation
+ // title if it's a group conversation OR SDK >= 28. Otherwise we set the
+ // Notification content title so Android won't think it's a group conversation.
+ if (isGroupConversation() || Build.VERSION.SDK_INT >= 28) {
+ // If group or non-legacy, set MessagingStyle#mConversationTitle.
+ style.setConversationTitle(mConversationTitle);
+ } else {
+ // Otherwise set Notification#mContentTitle.
+ builder.getBuilder().setContentTitle(mConversationTitle);
+ }
+
+ // For SDK >= 28, we can simply denote the group conversation status regardless of
+ // if we set the conversation title or not.
+ if (Build.VERSION.SDK_INT >= 28) {
+ style.setGroupConversation(mIsGroupConversation);
+ }
+
for (MessagingStyle.Message message : mMessages) {
+ CharSequence name = null;
+ if (message.getPerson() != null) {
+ name = message.getPerson().getName();
+ }
Notification.MessagingStyle.Message frameworkMessage =
new Notification.MessagingStyle.Message(
- message.getText(),
- message.getTimestamp(),
- message.getSender());
+ message.getText(), message.getTimestamp(), name);
if (message.getDataMimeType() != null) {
frameworkMessage.setData(message.getDataMimeType(), message.getDataUri());
}
@@ -2335,7 +2418,11 @@
if (mConversationTitle != null) {
builder.getBuilder().setContentTitle(mConversationTitle);
} else if (latestIncomingMessage != null) {
- builder.getBuilder().setContentTitle(latestIncomingMessage.getSender());
+ builder.getBuilder().setContentTitle("");
+ if (latestIncomingMessage.getPerson() != null) {
+ builder.getBuilder().setContentTitle(
+ latestIncomingMessage.getPerson().getName());
+ }
}
// Set the text
if (latestIncomingMessage != null) {
@@ -2369,7 +2456,8 @@
for (int i = mMessages.size() - 1; i >= 0; i--) {
MessagingStyle.Message message = mMessages.get(i);
// Incoming messages have a non-empty sender.
- if (!TextUtils.isEmpty(message.getSender())) {
+ if (message.getPerson() != null
+ && !TextUtils.isEmpty(message.getPerson().getName())) {
return message;
}
}
@@ -2383,7 +2471,7 @@
private boolean hasMessagesWithoutSender() {
for (int i = mMessages.size() - 1; i >= 0; i--) {
MessagingStyle.Message message = mMessages.get(i);
- if (message.getSender() == null) {
+ if (message.getPerson() != null && message.getPerson().getName() == null) {
return true;
}
}
@@ -2395,10 +2483,10 @@
SpannableStringBuilder sb = new SpannableStringBuilder();
final boolean afterLollipop = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
int color = afterLollipop ? Color.BLACK : Color.WHITE;
- CharSequence replyName = message.getSender();
- if (TextUtils.isEmpty(message.getSender())) {
- replyName = mUserDisplayName == null
- ? "" : mUserDisplayName;
+ CharSequence replyName =
+ message.getPerson() == null ? "" : message.getPerson().getName();
+ if (TextUtils.isEmpty(replyName)) {
+ replyName = mUser.getName();
color = afterLollipop && mBuilder.getColor() != NotificationCompat.COLOR_DEFAULT
? mBuilder.getColor()
: color;
@@ -2422,9 +2510,9 @@
@Override
public void addCompatExtras(Bundle extras) {
super.addCompatExtras(extras);
- if (mUserDisplayName != null) {
- extras.putCharSequence(EXTRA_SELF_DISPLAY_NAME, mUserDisplayName);
- }
+ extras.putCharSequence(EXTRA_SELF_DISPLAY_NAME, mUser.getName());
+ extras.putBundle(EXTRA_MESSAGING_STYLE_USER, mUser.toBundle());
+
if (mConversationTitle != null) {
extras.putCharSequence(EXTRA_CONVERSATION_TITLE, mConversationTitle);
}
@@ -2444,11 +2532,21 @@
@Override
protected void restoreFromCompatExtras(Bundle extras) {
mMessages.clear();
- mUserDisplayName = extras.getString(EXTRA_SELF_DISPLAY_NAME);
+ // Call to #restore requires that there either be a display name OR a user.
+ if (extras.containsKey(EXTRA_MESSAGING_STYLE_USER)) {
+ // New path simply unpacks Person, but checks if there's a valid name.
+ mUser = Person.fromBundle(extras.getBundle(EXTRA_MESSAGING_STYLE_USER));
+ } else {
+ // Legacy extra simply builds Person with a name.
+ mUser = new Person.Builder()
+ .setName(extras.getString(EXTRA_SELF_DISPLAY_NAME))
+ .build();
+ }
+
mConversationTitle = extras.getString(EXTRA_CONVERSATION_TITLE);
Parcelable[] parcelables = extras.getParcelableArray(EXTRA_MESSAGES);
if (parcelables != null) {
- mMessages = Message.getMessagesFromBundleArray(parcelables);
+ mMessages.addAll(Message.getMessagesFromBundleArray(parcelables));
}
if (extras.containsKey(EXTRA_IS_GROUP_CONVERSATION)) {
mIsGroupConversation = extras.getBoolean(EXTRA_IS_GROUP_CONVERSATION);
@@ -2570,7 +2668,7 @@
@Deprecated
@Nullable
public CharSequence getSender() {
- return mPerson.getName();
+ return mPerson == null ? null : mPerson.getName();
}
/** Returns the {@link Person} sender of this message. */
@@ -2601,6 +2699,9 @@
}
bundle.putLong(KEY_TIMESTAMP, mTimestamp);
if (mPerson != null) {
+ // We must add both as Frameworks depends on this extra directly in order to
+ // render properly.
+ bundle.putCharSequence(KEY_SENDER, mPerson.getName());
bundle.putBundle(KEY_PERSON, mPerson.toBundle());
}
if (mDataMimeType != null) {
@@ -2646,22 +2747,21 @@
return null;
}
- Message message;
- if (bundle.containsKey(KEY_SENDER)) {
- // Legacy sender
- message = new Message(
- bundle.getCharSequence(KEY_TEXT),
- bundle.getLong(KEY_TIMESTAMP),
- new Person.Builder()
- .setName(bundle.getCharSequence(KEY_SENDER))
- .build());
- } else {
- message = new Message(
- bundle.getCharSequence(KEY_TEXT),
- bundle.getLong(KEY_TIMESTAMP),
- Person.fromBundle(bundle.getBundle(KEY_PERSON)));
+ Person person = null;
+ if (bundle.containsKey(KEY_PERSON)) {
+ person = Person.fromBundle(bundle.getBundle(KEY_PERSON));
+ } else if (bundle.containsKey(KEY_SENDER)) {
+ // Legacy person
+ person = new Person.Builder()
+ .setName(bundle.getCharSequence(KEY_SENDER))
+ .build();
}
+ Message message = new Message(
+ bundle.getCharSequence(KEY_TEXT),
+ bundle.getLong(KEY_TIMESTAMP),
+ person);
+
if (bundle.containsKey(KEY_DATA_MIME_TYPE)
&& bundle.containsKey(KEY_DATA_URI)) {
message.setData(bundle.getString(KEY_DATA_MIME_TYPE),
@@ -5005,6 +5105,12 @@
return result;
}
+ /** Returns the content title of a {@link Notification}. **/
+ @RequiresApi(19)
+ public static CharSequence getContentTitle(Notification notification) {
+ return notification.extras.getCharSequence(Notification.EXTRA_TITLE);
+ }
+
/**
* Get the category of this notification in a backwards compatible
* manner.
diff --git a/compat/src/main/java/androidx/core/app/NotificationManagerCompat.java b/compat/src/main/java/androidx/core/app/NotificationManagerCompat.java
index 3660de3..e8b6188 100644
--- a/compat/src/main/java/androidx/core/app/NotificationManagerCompat.java
+++ b/compat/src/main/java/androidx/core/app/NotificationManagerCompat.java
@@ -267,7 +267,7 @@
// Parse the string again if it is different from the last time this method was called.
if (enabledNotificationListeners != null
&& !enabledNotificationListeners.equals(sEnabledNotificationListeners)) {
- final String[] components = enabledNotificationListeners.split(":");
+ final String[] components = enabledNotificationListeners.split(":", -1);
Set<String> packageNames = new HashSet<String>(components.length);
for (String component : components) {
ComponentName componentName = ComponentName.unflattenFromString(component);
diff --git a/compat/src/main/java/androidx/core/content/pm/PermissionInfoCompat.java b/compat/src/main/java/androidx/core/content/pm/PermissionInfoCompat.java
new file mode 100644
index 0000000..df095a6
--- /dev/null
+++ b/compat/src/main/java/androidx/core/content/pm/PermissionInfoCompat.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.content.pm;
+
+import android.annotation.SuppressLint;
+import android.content.pm.PermissionInfo;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.core.os.BuildCompat;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Helper for accessing features in {@link PermissionInfo}.
+ */
+public final class PermissionInfoCompat {
+ private PermissionInfoCompat() {
+ }
+
+ /** @hide */
+ @IntDef(flag = false, value = {
+ PermissionInfo.PROTECTION_NORMAL,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ PermissionInfo.PROTECTION_SIGNATURE,
+ PermissionInfo.PROTECTION_SIGNATURE_OR_SYSTEM,
+ })
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Protection {}
+
+ /** @hide */
+ @SuppressLint("UniqueConstants") // because _SYSTEM and _PRIVILEGED are aliases.
+ @IntDef(flag = true, value = {
+ PermissionInfo.PROTECTION_FLAG_PRIVILEGED,
+ PermissionInfo.PROTECTION_FLAG_SYSTEM,
+ PermissionInfo.PROTECTION_FLAG_DEVELOPMENT,
+ PermissionInfo.PROTECTION_FLAG_APPOP,
+ PermissionInfo.PROTECTION_FLAG_PRE23,
+ PermissionInfo.PROTECTION_FLAG_INSTALLER,
+ PermissionInfo.PROTECTION_FLAG_VERIFIER,
+ PermissionInfo.PROTECTION_FLAG_PREINSTALLED,
+ PermissionInfo.PROTECTION_FLAG_SETUP,
+ PermissionInfo.PROTECTION_FLAG_INSTANT,
+ PermissionInfo.PROTECTION_FLAG_RUNTIME_ONLY,
+ })
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ProtectionFlags {}
+
+ /**
+ * Return the base permission type of a {@link PermissionInfo}.
+ */
+ @SuppressLint("WrongConstant") // for "PermissionInfo.PROTECTION_MASK_BASE"
+ @Protection
+ public static int getProtection(@NonNull PermissionInfo permissionInfo) {
+ if (BuildCompat.isAtLeastP()) {
+ return permissionInfo.getProtection();
+ } else {
+ return permissionInfo.protectionLevel & PermissionInfo.PROTECTION_MASK_BASE;
+ }
+ }
+
+ /**
+ * Return the additional protection flags of a {@link PermissionInfo}.
+ */
+ @SuppressLint("WrongConstant") // for "~PermissionInfo.PROTECTION_MASK_BASE"
+ @ProtectionFlags
+ public static int getProtectionFlags(@NonNull PermissionInfo permissionInfo) {
+ if (BuildCompat.isAtLeastP()) {
+ return permissionInfo.getProtectionFlags();
+ } else {
+ return permissionInfo.protectionLevel & ~PermissionInfo.PROTECTION_MASK_BASE;
+ }
+ }
+}
diff --git a/compat/src/main/java/androidx/core/database/CursorWindowCompat.java b/compat/src/main/java/androidx/core/database/CursorWindowCompat.java
new file mode 100644
index 0000000..c675977
--- /dev/null
+++ b/compat/src/main/java/androidx/core/database/CursorWindowCompat.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.database;
+
+import android.database.CursorWindow;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.os.BuildCompat;
+
+/**
+ * Helper for accessing features in {@link android.database.CursorWindow}
+ */
+public final class CursorWindowCompat {
+
+ private CursorWindowCompat() {
+ /* Hide constructor */
+ }
+
+ /**
+ * Creates a CursorWindow of the specified size.
+ * <p>
+ * Prior to Android P, this method will return a CursorWindow of size defined by the platform.
+ */
+ @NonNull
+ public CursorWindow create(@Nullable String name, long windowSizeBytes) {
+ if (BuildCompat.isAtLeastP()) {
+ return new CursorWindow(name, windowSizeBytes);
+ } else if (Build.VERSION.SDK_INT >= 15) {
+ return new CursorWindow(name);
+ } else {
+ //noinspection deprecation
+ return new CursorWindow(false);
+ }
+ }
+}
diff --git a/compat/src/main/java/androidx/core/database/sqlite/SQLiteCursorCompat.java b/compat/src/main/java/androidx/core/database/sqlite/SQLiteCursorCompat.java
new file mode 100644
index 0000000..ebbc037
--- /dev/null
+++ b/compat/src/main/java/androidx/core/database/sqlite/SQLiteCursorCompat.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.database.sqlite;
+
+import android.database.sqlite.SQLiteCursor;
+
+import androidx.annotation.NonNull;
+import androidx.core.os.BuildCompat;
+
+/**
+ * Helper for accessing features in {@link android.database.AbstractWindowedCursor}
+ */
+public final class SQLiteCursorCompat {
+
+ private SQLiteCursorCompat() {
+ /* Hide constructor */
+ }
+
+ /**
+ * Controls whether the cursor is filled starting at the position passed to
+ * {@link SQLiteCursor#moveToPosition(int)}.
+ * <p>
+ * By default, SQLiteCursor will optimize for accesses around the requested row index by loading
+ * data on either side of it. Pass true to this method to disable that behavior, useful to
+ * optimize multi-window, continuous reads.
+ * <p>
+ * Prior to Android P, this method will do nothing.
+ */
+ public void setFillWindowForwardOnly(
+ @NonNull SQLiteCursor cursor, boolean fillWindowForwardOnly) {
+ if (BuildCompat.isAtLeastP()) {
+ cursor.setFillWindowForwardOnly(fillWindowForwardOnly);
+ }
+ }
+}
diff --git a/compat/src/main/java/androidx/core/graphics/drawable/IconCompat.java b/compat/src/main/java/androidx/core/graphics/drawable/IconCompat.java
index cd9968c..4c296ea 100644
--- a/compat/src/main/java/androidx/core/graphics/drawable/IconCompat.java
+++ b/compat/src/main/java/androidx/core/graphics/drawable/IconCompat.java
@@ -16,6 +16,12 @@
package androidx.core.graphics.drawable;
+import static android.graphics.drawable.Icon.TYPE_ADAPTIVE_BITMAP;
+import static android.graphics.drawable.Icon.TYPE_BITMAP;
+import static android.graphics.drawable.Icon.TYPE_DATA;
+import static android.graphics.drawable.Icon.TYPE_RESOURCE;
+import static android.graphics.drawable.Icon.TYPE_URI;
+
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
@@ -57,6 +63,7 @@
import androidx.annotation.VisibleForTesting;
import androidx.core.content.ContextCompat;
import androidx.core.content.res.ResourcesCompat;
+import androidx.core.os.BuildCompat;
import java.io.File;
import java.io.FileInputStream;
@@ -79,34 +86,6 @@
*/
public static final int TYPE_UNKOWN = -1;
- // TODO: Switch these to the public constants in the beta branch.
- /**
- * @hide
- */
- @RestrictTo(LIBRARY)
- public static final int TYPE_BITMAP = 1;
- /**
- * @hide
- */
- @RestrictTo(LIBRARY)
- public static final int TYPE_RESOURCE = 2;
- /**
- * @hide
- */
- @RestrictTo(LIBRARY)
- public static final int TYPE_DATA = 3;
- /**
- * @hide
- */
- @RestrictTo(LIBRARY)
- public static final int TYPE_URI = 4;
- /**
- * @hide
- */
- @RestrictTo(LIBRARY)
- public static final int TYPE_ADAPTIVE_BITMAP = 5;
-
-
/**
* @hide
*/
@@ -590,6 +569,58 @@
return bundle;
}
+ @Override
+ public String toString() {
+ if (mType == TYPE_UNKOWN) {
+ return String.valueOf(mObj1);
+ }
+ final StringBuilder sb = new StringBuilder("Icon(typ=").append(typeToString(mType));
+ switch (mType) {
+ case TYPE_BITMAP:
+ case TYPE_ADAPTIVE_BITMAP:
+ sb.append(" size=")
+ .append(((Bitmap) mObj1).getWidth())
+ .append("x")
+ .append(((Bitmap) mObj1).getHeight());
+ break;
+ case TYPE_RESOURCE:
+ sb.append(" pkg=")
+ .append(getResPackage())
+ .append(" id=")
+ .append(String.format("0x%08x", getResId()));
+ break;
+ case TYPE_DATA:
+ sb.append(" len=").append(mInt1);
+ if (mInt2 != 0) {
+ sb.append(" off=").append(mInt2);
+ }
+ break;
+ case TYPE_URI:
+ sb.append(" uri=").append(mObj1);
+ break;
+ }
+ if (mTintList != null) {
+ sb.append(" tint=");
+ sb.append(mTintList);
+ }
+ if (mTintMode != DEFAULT_TINT_MODE) {
+ sb.append(" mode=").append(mTintMode);
+ }
+ sb.append(")");
+ return sb.toString();
+ }
+
+ private static String typeToString(int x) {
+ switch (x) {
+ case TYPE_BITMAP: return "BITMAP";
+ case TYPE_ADAPTIVE_BITMAP: return "BITMAP_MASKABLE";
+ case TYPE_DATA: return "DATA";
+ case TYPE_RESOURCE: return "RESOURCE";
+ case TYPE_URI: return "URI";
+ default: return "UNKNOWN";
+ }
+ }
+
/**
* Extracts an icon from a bundle that was added using {@link #toBundle()}.
*/
@@ -645,8 +676,10 @@
*/
@IconType
@RequiresApi(23)
- public static int getType(Icon icon) {
- // TODO: Switch to public APIs on P+ in beta branch.
+ public static int getType(@NonNull Icon icon) {
+ if (BuildCompat.isAtLeastP()) {
+ return icon.getType();
+ }
try {
return (int) icon.getClass().getMethod("getType").invoke(icon);
} catch (IllegalAccessException e) {
@@ -671,8 +704,10 @@
*/
@Nullable
@RequiresApi(23)
- public static String getResPackage(Icon icon) {
- // TODO: Switch to public APIs on P+ in beta branch.
+ public static String getResPackage(@NonNull Icon icon) {
+ if (BuildCompat.isAtLeastP()) {
+ return icon.getResPackage();
+ }
try {
return (String) icon.getClass().getMethod("getResPackage").invoke(icon);
} catch (IllegalAccessException e) {
@@ -697,8 +732,10 @@
*/
@IdRes
@RequiresApi(23)
- public static int getResId(Icon icon) {
- // TODO: Switch to public APIs on P+ in beta branch.
+ public static int getResId(@NonNull Icon icon) {
+ if (BuildCompat.isAtLeastP()) {
+ return icon.getResId();
+ }
try {
return (int) icon.getClass().getMethod("getResId").invoke(icon);
} catch (IllegalAccessException e) {
@@ -723,8 +760,10 @@
*/
@Nullable
@RequiresApi(23)
- public Uri getUri(Icon icon) {
- // TODO: Switch to public APIs on P+ in beta branch.
+ public Uri getUri(@NonNull Icon icon) {
+ if (BuildCompat.isAtLeastP()) {
+ return icon.getUri();
+ }
try {
return (Uri) icon.getClass().getMethod("getUri").invoke(icon);
} catch (IllegalAccessException e) {
diff --git a/compat/src/main/java/androidx/core/os/BuildCompat.java b/compat/src/main/java/androidx/core/os/BuildCompat.java
index 277a724..19f85b5 100644
--- a/compat/src/main/java/androidx/core/os/BuildCompat.java
+++ b/compat/src/main/java/androidx/core/os/BuildCompat.java
@@ -81,14 +81,10 @@
/**
* Checks if the device is running on a pre-release version of Android P or newer.
* <p>
- * <strong>Note:</strong> This method will return {@code false} on devices running release
- * versions of Android. When Android P is finalized for release, this method will be deprecated
- * and all calls should be replaced with {@code Build.SDK_INT >= Build.VERSION_CODES.P}.
- *
* @return {@code true} if P APIs are available for use, {@code false} otherwise
*/
public static boolean isAtLeastP() {
- return VERSION.CODENAME.equals("P") || VERSION.CODENAME.equals("Q");
+ return VERSION.SDK_INT >= 28;
}
/**
@@ -101,6 +97,8 @@
* @return {@code true} if Q APIs are available for use, {@code false} otherwise
*/
public static boolean isAtLeastQ() {
- return VERSION.CODENAME.equals("Q");
+ return VERSION.CODENAME.length() == 1
+ && VERSION.CODENAME.charAt(0) >= 'Q'
+ && VERSION.CODENAME.charAt(0) <= 'Z';
}
}
diff --git a/compat/src/main/java/androidx/core/os/LocaleHelper.java b/compat/src/main/java/androidx/core/os/LocaleHelper.java
index 001d657..5ce4c82 100644
--- a/compat/src/main/java/androidx/core/os/LocaleHelper.java
+++ b/compat/src/main/java/androidx/core/os/LocaleHelper.java
@@ -33,7 +33,7 @@
// Simpleton implementation for Locale.forLanguageTag(...)
static Locale forLanguageTag(String str) {
if (str.contains("-")) {
- String[] args = str.split("-");
+ String[] args = str.split("-", -1);
if (args.length > 2) {
return new Locale(args[0], args[1], args[2]);
} else if (args.length > 1) {
@@ -42,7 +42,7 @@
return new Locale(args[0]);
}
} else if (str.contains("_")) {
- String[] args = str.split("_");
+ String[] args = str.split("_", -1);
if (args.length > 2) {
return new Locale(args[0], args[1], args[2]);
} else if (args.length > 1) {
diff --git a/compat/src/main/java/androidx/core/os/LocaleListCompat.java b/compat/src/main/java/androidx/core/os/LocaleListCompat.java
index a933877..0be1ff3 100644
--- a/compat/src/main/java/androidx/core/os/LocaleListCompat.java
+++ b/compat/src/main/java/androidx/core/os/LocaleListCompat.java
@@ -289,7 +289,7 @@
if (list == null || list.isEmpty()) {
return getEmptyLocaleList();
} else {
- final String[] tags = list.split(",");
+ final String[] tags = list.split(",", -1);
final Locale[] localeArray = new Locale[tags.length];
for (int i = 0; i < localeArray.length; i++) {
localeArray[i] = Build.VERSION.SDK_INT >= 21
diff --git a/compat/src/main/java/androidx/core/os/LocaleListHelper.java b/compat/src/main/java/androidx/core/os/LocaleListHelper.java
index 0a656bc..137d9cd 100644
--- a/compat/src/main/java/androidx/core/os/LocaleListHelper.java
+++ b/compat/src/main/java/androidx/core/os/LocaleListHelper.java
@@ -273,7 +273,7 @@
if (list == null || list.isEmpty()) {
return getEmptyLocaleList();
} else {
- final String[] tags = list.split(",");
+ final String[] tags = list.split(",", -1);
final Locale[] localeArray = new Locale[tags.length];
for (int i = 0; i < localeArray.length; i++) {
localeArray[i] = LocaleHelper.forLanguageTag(tags[i]);
diff --git a/compat/src/main/java/androidx/core/text/HtmlCompat.java b/compat/src/main/java/androidx/core/text/HtmlCompat.java
index b846805..a5b97ea 100644
--- a/compat/src/main/java/androidx/core/text/HtmlCompat.java
+++ b/compat/src/main/java/androidx/core/text/HtmlCompat.java
@@ -18,6 +18,8 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
import android.annotation.SuppressLint;
import android.graphics.Color;
import android.os.Build;
@@ -33,6 +35,8 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import java.lang.annotation.Retention;
+
/**
* Backwards compatible version of {@link Html}.
*/
@@ -119,6 +123,7 @@
FROM_HTML_MODE_LEGACY
}, flag = true)
@RestrictTo(LIBRARY)
+ @Retention(SOURCE)
@interface FromHtmlFlags {
}
@@ -128,6 +133,7 @@
TO_HTML_PARAGRAPH_LINES_INDIVIDUAL
})
@RestrictTo(LIBRARY)
+ @Retention(SOURCE)
@interface ToHtmlOptions {
}
diff --git a/compat/src/main/java/androidx/core/text/PrecomputedTextCompat.java b/compat/src/main/java/androidx/core/text/PrecomputedTextCompat.java
new file mode 100644
index 0000000..dbf9ba3
--- /dev/null
+++ b/compat/src/main/java/androidx/core/text/PrecomputedTextCompat.java
@@ -0,0 +1,622 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.text;
+
+import android.os.Build;
+import android.text.Layout;
+import android.text.PrecomputedText;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.StaticLayout;
+import android.text.TextDirectionHeuristic;
+import android.text.TextDirectionHeuristics;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.style.MetricAffectingSpan;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.core.os.BuildCompat;
+import androidx.core.util.ObjectsCompat;
+import androidx.core.util.Preconditions;
+
+import java.util.ArrayList;
+
+/**
+ * A text which has the character metrics data.
+ *
+ * A text object that contains the character metrics data and can be used to improve the performance
+ * of text layout operations. When a PrecomputedTextCompat is created with a given
+ * {@link CharSequence}, it will measure the text metrics during the creation. This PrecomputedText
+ * instance can be set on {@link android.widget.TextView} or {@link StaticLayout}. Since the text
+ * layout information will be included in this instance, {@link android.widget.TextView} or
+ * {@link StaticLayout} will not have to recalculate this information.
+ *
+ * On API 28 or later, there is full PrecomputedText support by framework. From API 21 to API 27,
+ * PrecomputedTextCompat relies on internal text layout cache. PrecomputedTextCompat immediately
+ * computes the text layout in the constuctor to warm up the internal text layout cache. On API 20
+ * or before, PrecomputedTextCompat does nothing.
+ *
+ * Note that any {@link android.text.NoCopySpan} attached to the original text won't be passed to
+ * PrecomputedText.
+ */
+public class PrecomputedTextCompat implements Spannable {
+ private static final char LINE_FEED = '\n';
+
+ /**
+ * The information required for building {@link PrecomputedTextCompat}.
+ *
+ * Contains information required for precomputing text measurement metadata, so it can be done
+ * in isolation of a {@link android.widget.TextView} or {@link StaticLayout}, when final layout
+ * constraints are not known.
+ */
+ public static final class Params {
+ private final @NonNull TextPaint mPaint;
+
+ // null on API 17 or before, non null on API 18 or later.
+ private final @Nullable TextDirectionHeuristic mTextDir;
+
+ private final int mBreakStrategy;
+
+ private final int mHyphenationFrequency;
+
+ private final PrecomputedText.Params mWrapped;
+
+ /**
+ * A builder for creating {@link Params}.
+ */
+ public static class Builder {
+ // The TextPaint used for measurement.
+ private final @NonNull TextPaint mPaint;
+
+ // The requested text direction.
+ private TextDirectionHeuristic mTextDir;
+
+ // The break strategy for this measured text.
+ private int mBreakStrategy;
+
+ // The hyphenation frequency for this measured text.
+ private int mHyphenationFrequency;
+
+ /**
+ * Builder constructor.
+ *
+ * @param paint the paint to be used for drawing
+ */
+ public Builder(@NonNull TextPaint paint) {
+ mPaint = paint;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ mBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
+ mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NORMAL;
+ } else {
+ mBreakStrategy = mHyphenationFrequency = 0;
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR;
+ } else {
+ mTextDir = null;
+ }
+ }
+
+ /**
+ * Set the line break strategy.
+ *
+ * The default value is {@link Layout#BREAK_STRATEGY_HIGH_QUALITY}.
+ *
+ * On API 22 and below, this has no effect as there is no line break strategy.
+ *
+ * @param strategy the break strategy
+ * @return PrecomputedTextCompat.Builder instance
+ * @see StaticLayout.Builder#setBreakStrategy
+ * @see android.widget.TextView#setBreakStrategy
+ */
+ @RequiresApi(23)
+ public Builder setBreakStrategy(int strategy) {
+ mBreakStrategy = strategy;
+ return this;
+ }
+
+ /**
+ * Set the hyphenation frequency.
+ *
+ * The default value is {@link Layout#HYPHENATION_FREQUENCY_NORMAL}.
+ *
+ * On API 22 and below, this has no effect as there is no hyphenation frequency.
+ *
+ * @param frequency the hyphenation frequency
+ * @return PrecomputedTextCompat.Builder instance
+ * @see StaticLayout.Builder#setHyphenationFrequency
+ * @see android.widget.TextView#setHyphenationFrequency
+ */
+ @RequiresApi(23)
+ public Builder setHyphenationFrequency(int frequency) {
+ mHyphenationFrequency = frequency;
+ return this;
+ }
+
+ /**
+ * Set the text direction heuristic.
+ *
+ * The default value is {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}.
+ *
+ * On API 17 or before, text direction heuristics cannot be modified, so this method
+ * does nothing.
+ *
+ * @param textDir the text direction heuristic for resolving bidi behavior
+ * @return PrecomputedTextCompat.Builder instance
+ * @see StaticLayout.Builder#setTextDirection
+ */
+ @RequiresApi(18)
+ public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) {
+ mTextDir = textDir;
+ return this;
+ }
+
+ /**
+ * Build the {@link Params}.
+ *
+ * @return the layout parameter
+ */
+ public @NonNull Params build() {
+ return new Params(mPaint, mTextDir, mBreakStrategy, mHyphenationFrequency);
+ }
+ }
+
+ private Params(@NonNull TextPaint paint, @NonNull TextDirectionHeuristic textDir,
+ int strategy, int frequency) {
+ if (BuildCompat.isAtLeastP()) {
+ mWrapped = new PrecomputedText.Params.Builder(paint).setBreakStrategy(strategy)
+ .setHyphenationFrequency(frequency).setTextDirection(textDir).build();
+ } else {
+ mWrapped = null;
+ }
+ mPaint = paint;
+ mTextDir = textDir;
+ mBreakStrategy = strategy;
+ mHyphenationFrequency = frequency;
+ }
+
+ @RequiresApi(28)
+ public Params(@NonNull PrecomputedText.Params wrapped) {
+ mPaint = wrapped.getTextPaint();
+ mTextDir = wrapped.getTextDirection();
+ mBreakStrategy = wrapped.getBreakStrategy();
+ mHyphenationFrequency = wrapped.getHyphenationFrequency();
+ mWrapped = wrapped;
+
+ }
+
+ /**
+ * Returns the {@link TextPaint} for this text.
+ *
+ * @return A {@link TextPaint}
+ */
+ public @NonNull TextPaint getTextPaint() {
+ return mPaint;
+ }
+
+ /**
+ * Returns the {@link TextDirectionHeuristic} for this text.
+ *
+ * On API 17 and below, this returns null, otherwise returns non-null
+ * TextDirectionHeuristic.
+ *
+ * @return the {@link TextDirectionHeuristic}
+ */
+ @RequiresApi(18)
+ public @Nullable TextDirectionHeuristic getTextDirection() {
+ return mTextDir;
+ }
+
+ /**
+ * Returns the break strategy for this text.
+ *
+ * On API 22 and below, this returns 0.
+ *
+ * @return the line break strategy
+ */
+ @RequiresApi(23)
+ public int getBreakStrategy() {
+ return mBreakStrategy;
+ }
+
+ /**
+ * Returns the hyphenation frequency for this text.
+ *
+ * On API 22 and below, this returns 0.
+ *
+ * @return the hyphenation frequency
+ */
+ @RequiresApi(23)
+ public int getHyphenationFrequency() {
+ return mHyphenationFrequency;
+ }
+
+ /**
+ * Check if the same text layout.
+ *
+ * @return true if this and the given param result in the same text layout
+ */
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o == null || !(o instanceof Params)) {
+ return false;
+ }
+ Params other = (Params) o;
+ if (mWrapped != null) {
+ return mWrapped.equals(other);
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (mBreakStrategy != other.getBreakStrategy()) {
+ return false;
+ }
+ if (mHyphenationFrequency != other.getHyphenationFrequency()) {
+ return false;
+ }
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ if (mTextDir != other.getTextDirection()) {
+ return false;
+ }
+ }
+
+ if (mPaint.getTextSize() != other.getTextPaint().getTextSize()) {
+ return false;
+ }
+ if (mPaint.getTextScaleX() != other.getTextPaint().getTextScaleX()) {
+ return false;
+ }
+ if (mPaint.getTextSkewX() != other.getTextPaint().getTextSkewX()) {
+ return false;
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ if (mPaint.getLetterSpacing() != other.getTextPaint().getLetterSpacing()) {
+ return false;
+ }
+ if (!TextUtils.equals(mPaint.getFontFeatureSettings(),
+ other.getTextPaint().getFontFeatureSettings())) {
+ return false;
+ }
+ }
+ if (mPaint.getFlags() != other.getTextPaint().getFlags()) {
+ return false;
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ if (!mPaint.getTextLocales().equals(other.getTextPaint().getTextLocales())) {
+ return false;
+ }
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ if (!mPaint.getTextLocale().equals(other.getTextPaint().getTextLocale())) {
+ return false;
+ }
+ }
+ if (mPaint.getTypeface() == null) {
+ if (other.getTextPaint().getTypeface() != null) {
+ return false;
+ }
+ } else if (!mPaint.getTypeface().equals(other.getTextPaint().getTypeface())) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ return ObjectsCompat.hash(mPaint.getTextSize(), mPaint.getTextScaleX(),
+ mPaint.getTextSkewX(), mPaint.getLetterSpacing(), mPaint.getFlags(),
+ mPaint.getTextLocales(), mPaint.getTypeface(), mPaint.isElegantTextHeight(),
+ mTextDir, mBreakStrategy, mHyphenationFrequency);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ return ObjectsCompat.hash(mPaint.getTextSize(), mPaint.getTextScaleX(),
+ mPaint.getTextSkewX(), mPaint.getLetterSpacing(), mPaint.getFlags(),
+ mPaint.getTextLocale(), mPaint.getTypeface(), mPaint.isElegantTextHeight(),
+ mTextDir, mBreakStrategy, mHyphenationFrequency);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ return ObjectsCompat.hash(mPaint.getTextSize(), mPaint.getTextScaleX(),
+ mPaint.getTextSkewX(), mPaint.getFlags(), mPaint.getTextLocale(),
+ mPaint.getTypeface(), mTextDir, mBreakStrategy, mHyphenationFrequency);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ return ObjectsCompat.hash(mPaint.getTextSize(), mPaint.getTextScaleX(),
+ mPaint.getTextSkewX(), mPaint.getFlags(), mPaint.getTextLocale(),
+ mPaint.getTypeface(), mTextDir, mBreakStrategy, mHyphenationFrequency);
+ } else {
+ return ObjectsCompat.hash(mPaint.getTextSize(), mPaint.getTextScaleX(),
+ mPaint.getTextSkewX(), mPaint.getFlags(), mPaint.getTypeface(), mTextDir,
+ mBreakStrategy, mHyphenationFrequency);
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder("{");
+ sb.append("textSize=" + mPaint.getTextSize());
+ sb.append(", textScaleX=" + mPaint.getTextScaleX());
+ sb.append(", textSkewX=" + mPaint.getTextSkewX());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ sb.append(", letterSpacing=" + mPaint.getLetterSpacing());
+ sb.append(", elegantTextHeight=" + mPaint.isElegantTextHeight());
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ sb.append(", textLocale=" + mPaint.getTextLocales());
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ sb.append(", textLocale=" + mPaint.getTextLocale());
+ }
+ sb.append(", typeface=" + mPaint.getTypeface());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ sb.append(", variationSettings=" + mPaint.getFontVariationSettings());
+ }
+ sb.append(", textDir=" + mTextDir);
+ sb.append(", breakStrategy=" + mBreakStrategy);
+ sb.append(", hyphenationFrequency=" + mHyphenationFrequency);
+ sb.append("}");
+ return sb.toString();
+ }
+ };
+
+ // The original text.
+ private final @NonNull Spannable mText;
+
+ private final @NonNull Params mParams;
+
+ // The list of measured paragraph info.
+ private final @NonNull int[] mParagraphEnds;
+
+ // null on API 27 or before. Non-null on API 28 or later
+ private final @Nullable PrecomputedText mWrapped;
+
+ /**
+ * Create a new {@link PrecomputedText} which will pre-compute text measurement and glyph
+ * positioning information.
+ * <p>
+ * This can be expensive, so computing this on a background thread before your text will be
+ * presented can save work on the UI thread.
+ * </p>
+ *
+ * Note that any {@link android.text.NoCopySpan} attached to the text won't be passed to the
+ * created PrecomputedText.
+ *
+ * @param text the text to be measured
+ * @param params parameters that define how text will be precomputed
+ * @return A {@link PrecomputedText}
+ */
+ public static PrecomputedTextCompat create(@NonNull CharSequence text, @NonNull Params params) {
+ Preconditions.checkNotNull(text);
+ Preconditions.checkNotNull(params);
+
+ if (BuildCompat.isAtLeastP() && params.mWrapped != null) {
+ return new PrecomputedTextCompat(PrecomputedText.create(text, params.mWrapped), params);
+ }
+
+ ArrayList<Integer> ends = new ArrayList<>();
+
+ int paraEnd = 0;
+ int end = text.length();
+ for (int paraStart = 0; paraStart < end; paraStart = paraEnd) {
+ paraEnd = TextUtils.indexOf(text, LINE_FEED, paraStart, end);
+ if (paraEnd < 0) {
+ // No LINE_FEED(U+000A) character found. Use end of the text as the paragraph
+ // end.
+ paraEnd = end;
+ } else {
+ paraEnd++; // Includes LINE_FEED(U+000A) to the prev paragraph.
+ }
+
+ ends.add(paraEnd);
+ }
+ int[] result = new int[ends.size()];
+ for (int i = 0; i < ends.size(); ++i) {
+ result[i] = ends.get(i);
+ }
+
+ // No framework support for PrecomputedText
+ // Compute text layout and throw away StaticLayout for the purpose of warming up the
+ // internal text layout cache.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ StaticLayout.Builder.obtain(text, 0, text.length(), params.getTextPaint(),
+ Integer.MAX_VALUE)
+ .setBreakStrategy(params.getBreakStrategy())
+ .setHyphenationFrequency(params.getHyphenationFrequency())
+ .setTextDirection(params.getTextDirection())
+ .build();
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ new StaticLayout(text, params.getTextPaint(), Integer.MAX_VALUE,
+ Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
+ } else {
+ // There is no way of precomputing text layout on API 20 or before
+ // Do nothing
+ }
+
+ return new PrecomputedTextCompat(text, params, result);
+ }
+
+ // Use PrecomputedText.create instead.
+ private PrecomputedTextCompat(@NonNull CharSequence text, @NonNull Params params,
+ @NonNull int[] paraEnds) {
+ mText = new SpannableString(text);
+ mParams = params;
+ mParagraphEnds = paraEnds;
+ mWrapped = null;
+ }
+
+ @RequiresApi(28)
+ private PrecomputedTextCompat(@NonNull PrecomputedText precomputed, @NonNull Params params) {
+ mText = precomputed;
+ mParams = params;
+ mParagraphEnds = null;
+ mWrapped = precomputed;
+ }
+
+ /**
+ * Returns the layout parameters used to measure this text.
+ */
+ public @NonNull Params getParams() {
+ return mParams;
+ }
+
+ /**
+ * Returns the count of paragraphs.
+ */
+ public @IntRange(from = 0) int getParagraphCount() {
+ if (BuildCompat.isAtLeastP()) {
+ return mWrapped.getParagraphCount();
+ } else {
+ return mParagraphEnds.length;
+ }
+ }
+
+ /**
+ * Returns the paragraph start offset of the text.
+ */
+ public @IntRange(from = 0) int getParagraphStart(@IntRange(from = 0) int paraIndex) {
+ Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
+ if (BuildCompat.isAtLeastP()) {
+ return mWrapped.getParagraphStart(paraIndex);
+ } else {
+ return paraIndex == 0 ? 0 : mParagraphEnds[paraIndex - 1];
+ }
+ }
+
+ /**
+ * Returns the paragraph end offset of the text.
+ */
+ public @IntRange(from = 0) int getParagraphEnd(@IntRange(from = 0) int paraIndex) {
+ Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
+ if (BuildCompat.isAtLeastP()) {
+ return mWrapped.getParagraphStart(paraIndex);
+ } else {
+ return mParagraphEnds[paraIndex];
+ }
+ }
+
+
+ private int findParaIndex(@IntRange(from = 0) int pos) {
+ for (int i = 0; i < mParagraphEnds.length; ++i) {
+ if (pos < mParagraphEnds[i]) {
+ return i;
+ }
+ }
+ throw new IndexOutOfBoundsException(
+ "pos must be less than " + mParagraphEnds[mParagraphEnds.length - 1]
+ + ", gave " + pos);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Spannable overrides
+ //
+ // Do not allow to modify MetricAffectingSpan
+
+ /**
+ * @throws IllegalArgumentException if {@link MetricAffectingSpan} is specified.
+ */
+ @Override
+ public void setSpan(Object what, int start, int end, int flags) {
+ if (what instanceof MetricAffectingSpan) {
+ throw new IllegalArgumentException(
+ "MetricAffectingSpan can not be set to PrecomputedText.");
+ }
+ if (BuildCompat.isAtLeastP()) {
+ mWrapped.setSpan(what, start, end, flags);
+ } else {
+ mText.setSpan(what, start, end, flags);
+ }
+ }
+
+ /**
+ * @throws IllegalArgumentException if {@link MetricAffectingSpan} is specified.
+ */
+ @Override
+ public void removeSpan(Object what) {
+ if (what instanceof MetricAffectingSpan) {
+ throw new IllegalArgumentException(
+ "MetricAffectingSpan can not be removed from PrecomputedText.");
+ }
+ if (BuildCompat.isAtLeastP()) {
+ mWrapped.removeSpan(what);
+ } else {
+ mText.removeSpan(what);
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Spanned overrides
+ //
+ // Just proxy for underlying mText if appropriate.
+
+ @Override
+ public <T> T[] getSpans(int start, int end, Class<T> type) {
+ if (BuildCompat.isAtLeastP()) {
+ return mWrapped.getSpans(start, end, type);
+ } else {
+ return mText.getSpans(start, end, type);
+ }
+
+ }
+
+ @Override
+ public int getSpanStart(Object tag) {
+ return mText.getSpanStart(tag);
+ }
+
+ @Override
+ public int getSpanEnd(Object tag) {
+ return mText.getSpanEnd(tag);
+ }
+
+ @Override
+ public int getSpanFlags(Object tag) {
+ return mText.getSpanFlags(tag);
+ }
+
+ @Override
+ public int nextSpanTransition(int start, int limit, Class type) {
+ return mText.nextSpanTransition(start, limit, type);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // CharSequence overrides.
+ //
+ // Just proxy for underlying mText.
+
+ @Override
+ public int length() {
+ return mText.length();
+ }
+
+ @Override
+ public char charAt(int index) {
+ return mText.charAt(index);
+ }
+
+ @Override
+ public CharSequence subSequence(int start, int end) {
+ return mText.subSequence(start, end);
+ }
+
+ @Override
+ public String toString() {
+ return mText.toString();
+ }
+}
diff --git a/compat/src/main/java/androidx/core/text/util/FindAddress.java b/compat/src/main/java/androidx/core/text/util/FindAddress.java
index 0602428..916db2b 100644
--- a/compat/src/main/java/androidx/core/text/util/FindAddress.java
+++ b/compat/src/main/java/androidx/core/text/util/FindAddress.java
@@ -466,20 +466,21 @@
// At this point we've matched a state; try to match a zip code after it.
Matcher zipMatcher = sWordRe.matcher(content);
- if (zipMatcher.find(stateMatch.end())
- && isValidZipCode(zipMatcher.group(0), stateMatch)) {
- return zipMatcher.end();
+ if (zipMatcher.find(stateMatch.end())) {
+ if (isValidZipCode(zipMatcher.group(0), stateMatch)) {
+ return zipMatcher.end();
+ }
+ } else {
+ // The content ends with a state but no zip
+ // code. This is a legal match according to the
+ // documentation. N.B. This is equivalent to the
+ // original c++ implementation, which only allowed
+ // the zip code to be optional at the end of the
+ // string, which presumably is a bug. We tried
+ // relaxing this to work in other places but it
+ // caused too many false positives.
+ nonZipMatch = stateMatch.end();
}
- // The content ends with a state but no zip
- // code. This is a legal match according to the
- // documentation. N.B. This differs from the
- // original c++ implementation, which only allowed
- // the zip code to be optional at the end of the
- // string, which presumably is a bug. Now we
- // prefer to find a match with a zip code, but
- // remember non-zip matches and return them if
- // necessary.
- nonZipMatch = stateMatch.end();
}
}
}
diff --git a/compat/src/main/java/androidx/core/view/DisplayCutoutCompat.java b/compat/src/main/java/androidx/core/view/DisplayCutoutCompat.java
new file mode 100644
index 0000000..0ce108c
--- /dev/null
+++ b/compat/src/main/java/androidx/core/view/DisplayCutoutCompat.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.view;
+
+import static android.os.Build.VERSION.SDK_INT;
+
+import android.graphics.Rect;
+import android.view.DisplayCutout;
+
+import java.util.List;
+
+
+/**
+ * Represents the area of the display that is not functional for displaying content.
+ *
+ * <p>{@code DisplayCutoutCompat} instances are immutable.
+ */
+public final class DisplayCutoutCompat {
+
+ private final Object mDisplayCutout;
+
+ /**
+ * Creates a DisplayCutout instance.
+ *
+ * @param safeInsets the insets from each edge which avoid the display cutout as returned by
+ * {@link #getSafeInsetTop()} etc.
+ * @param boundingRects the bounding rects of the display cutouts as returned by
+ * {@link #getBoundingRects()} ()}.
+ */
+ // TODO(b/73953958): @VisibleForTesting(visibility = PRIVATE)
+ public DisplayCutoutCompat(Rect safeInsets, List<Rect> boundingRects) {
+ this(SDK_INT >= 28 ? new DisplayCutout(safeInsets, boundingRects) : null);
+ }
+
+ private DisplayCutoutCompat(Object displayCutout) {
+ mDisplayCutout = displayCutout;
+ }
+
+ /** Returns the inset from the top which avoids the display cutout in pixels. */
+ public int getSafeInsetTop() {
+ if (SDK_INT >= 28) {
+ return ((DisplayCutout) mDisplayCutout).getSafeInsetTop();
+ } else {
+ return 0;
+ }
+ }
+
+ /** Returns the inset from the bottom which avoids the display cutout in pixels. */
+ public int getSafeInsetBottom() {
+ if (SDK_INT >= 28) {
+ return ((DisplayCutout) mDisplayCutout).getSafeInsetBottom();
+ } else {
+ return 0;
+ }
+ }
+
+ /** Returns the inset from the left which avoids the display cutout in pixels. */
+ public int getSafeInsetLeft() {
+ if (SDK_INT >= 28) {
+ return ((DisplayCutout) mDisplayCutout).getSafeInsetLeft();
+ } else {
+ return 0;
+ }
+ }
+
+ /** Returns the inset from the right which avoids the display cutout in pixels. */
+ public int getSafeInsetRight() {
+ if (SDK_INT >= 28) {
+ return ((DisplayCutout) mDisplayCutout).getSafeInsetRight();
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Returns a list of {@code Rect}s, each of which is the bounding rectangle for a non-functional
+ * area on the display.
+ *
+ * There will be at most one non-functional area per short edge of the device, and none on
+ * the long edges.
+ *
+ * @return a list of bounding {@code Rect}s, one for each display cutout area.
+ */
+ public List<Rect> getBoundingRects() {
+ if (SDK_INT >= 28) {
+ return ((DisplayCutout) mDisplayCutout).getBoundingRects();
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ DisplayCutoutCompat other = (DisplayCutoutCompat) o;
+ return mDisplayCutout == null ? other.mDisplayCutout == null
+ : mDisplayCutout.equals(other.mDisplayCutout);
+ }
+
+ @Override
+ public int hashCode() {
+ return mDisplayCutout == null ? 0 : mDisplayCutout.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return "DisplayCutoutCompat{" + mDisplayCutout + "}";
+ }
+
+ static DisplayCutoutCompat wrap(Object displayCutout) {
+ return displayCutout == null ? null : new DisplayCutoutCompat(displayCutout);
+ }
+}
diff --git a/compat/src/main/java/androidx/core/view/ViewConfigurationCompat.java b/compat/src/main/java/androidx/core/view/ViewConfigurationCompat.java
index 89c3ca5..2e47e55 100644
--- a/compat/src/main/java/androidx/core/view/ViewConfigurationCompat.java
+++ b/compat/src/main/java/androidx/core/view/ViewConfigurationCompat.java
@@ -17,6 +17,7 @@
package androidx.core.view;
import android.content.Context;
+import android.content.res.Resources;
import android.os.Build;
import android.util.Log;
import android.util.TypedValue;
@@ -130,5 +131,21 @@
return config.getScaledTouchSlop() / 2;
}
+ /**
+ * Check if shortcuts should be displayed in menus.
+ *
+ * @return {@code True} if shortcuts should be displayed in menus.
+ */
+ public static boolean shouldShowMenuShortcutsWhenKeyboardPresent(ViewConfiguration config,
+ @NonNull Context context) {
+ if (android.os.Build.VERSION.SDK_INT >= 28) {
+ return config.shouldShowMenuShortcutsWhenKeyboardPresent();
+ }
+ final Resources res = context.getResources();
+ final int platformResId = res.getIdentifier(
+ "config_showMenuShortcutsWhenKeyboardPresent", "bool", "android");
+ return platformResId != 0 && res.getBoolean(platformResId);
+ }
+
private ViewConfigurationCompat() {}
}
diff --git a/compat/src/main/java/androidx/core/view/WindowInsetsCompat.java b/compat/src/main/java/androidx/core/view/WindowInsetsCompat.java
index eef9b12..003044a 100644
--- a/compat/src/main/java/androidx/core/view/WindowInsetsCompat.java
+++ b/compat/src/main/java/androidx/core/view/WindowInsetsCompat.java
@@ -20,6 +20,7 @@
import android.graphics.Rect;
import android.view.WindowInsets;
+import androidx.annotation.Nullable;
/**
* Describes a set of insets for window content.
@@ -343,6 +344,34 @@
}
}
+ /**
+ * Returns the display cutout if there is one.
+ *
+ * @return the display cutout or null if there is none
+ * @see DisplayCutoutCompat
+ */
+ @Nullable
+ public DisplayCutoutCompat getDisplayCutout() {
+ if (SDK_INT >= 28) {
+ return DisplayCutoutCompat.wrap(((WindowInsets) mInsets).getDisplayCutout());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns a copy of this WindowInsets with the cutout fully consumed.
+ *
+ * @return A modified copy of this WindowInsets
+ */
+ public WindowInsetsCompat consumeDisplayCutout() {
+ if (SDK_INT >= 28) {
+ return new WindowInsetsCompat(((WindowInsets) mInsets).consumeDisplayCutout());
+ } else {
+ return null;
+ }
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
diff --git a/compat/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java b/compat/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
index 16b570f..015a77f 100644
--- a/compat/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
+++ b/compat/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java
@@ -642,7 +642,9 @@
* @param rowSpan The number of rows the item spans.
* @param columnIndex The column index at which the item is located.
* @param columnSpan The number of columns the item spans.
- * @param heading Whether the item is a heading.
+ * @param heading Whether the item is a heading. This should be set to false and the newer
+ * {@link AccessibilityNodeInfoCompat#setHeading(boolean)} used to identify
+ * headings.
* @param selected Whether the item is selected.
* @return An instance.
*/
@@ -666,7 +668,9 @@
* @param rowSpan The number of rows the item spans.
* @param columnIndex The column index at which the item is located.
* @param columnSpan The number of columns the item spans.
- * @param heading Whether the item is a heading.
+ * @param heading Whether the item is a heading. This should be set to false and the newer
+ * {@link AccessibilityNodeInfoCompat#setHeading(boolean)} used to identify
+ * headings.
* @return An instance.
*/
public static CollectionItemInfoCompat obtain(int rowIndex, int rowSpan,
@@ -740,6 +744,7 @@
* heading, table header, etc.
*
* @return If the item is a heading.
+ * @deprecated Use {@link AccessibilityNodeInfoCompat#isHeading()}
*/
public boolean isHeading() {
if (Build.VERSION.SDK_INT >= 19) {
@@ -3313,11 +3318,16 @@
/**
* Returns whether node represents a heading.
+ * <p><strong>Note:</strong> Returns {@code true} if either {@link #setHeading(boolean)}
+ * marks this node as a heading or if the node has a {@link CollectionItemInfoCompat} that marks
+ * it as such, to accomodate apps that use the now-deprecated API.</p>
*
* @return {@code true} if the node is a heading, {@code false} otherwise.
*/
public boolean isHeading() {
- return getBooleanProperty(BOOLEAN_PROPERTY_IS_HEADING);
+ if (getBooleanProperty(BOOLEAN_PROPERTY_IS_HEADING)) return true;
+ CollectionItemInfoCompat collectionItemInfo = getCollectionItemInfo();
+ return (collectionItemInfo != null) && (collectionItemInfo.isHeading());
}
/**
diff --git a/core/ktx/OWNERS b/core/ktx/OWNERS
new file mode 100644
index 0000000..e450f4c
--- /dev/null
+++ b/core/ktx/OWNERS
@@ -0,0 +1 @@
+jakew@google.com
diff --git a/core/ktx/api/0.1.txt b/core/ktx/api/0.1.txt
new file mode 100644
index 0000000..e324352
--- /dev/null
+++ b/core/ktx/api/0.1.txt
@@ -0,0 +1,673 @@
+package androidx.animation {
+
+ public final class AnimatorKt {
+ ctor public AnimatorKt();
+ method public static final android.animation.Animator.AnimatorListener addListener(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onEnd = "null", kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onStart = "null", kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onCancel = "null", kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onRepeat = "null");
+ method @RequiresApi(19) public static final android.animation.Animator.AnimatorPauseListener addPauseListener(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onResume = "null", kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onPause = "null");
+ method public static final android.animation.Animator.AnimatorListener doOnCancel(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+ method public static final android.animation.Animator.AnimatorListener doOnEnd(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+ method @RequiresApi(19) public static final android.animation.Animator.AnimatorPauseListener doOnPause(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+ method public static final android.animation.Animator.AnimatorListener doOnRepeat(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+ method @RequiresApi(19) public static final android.animation.Animator.AnimatorPauseListener doOnResume(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+ method public static final android.animation.Animator.AnimatorListener doOnStart(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+ }
+
+}
+
+package androidx.content {
+
+ public final class ContentValuesKt {
+ ctor public ContentValuesKt();
+ method public static final error.NonExistentClass contentValuesOf(kotlin.Pair<String,?>... pairs);
+ }
+
+ public final class ContextKt {
+ ctor public ContextKt();
+ method public static final void withStyledAttributes(android.content.Context, android.util.AttributeSet? set = "null", int[] attrs, @AttrRes int defStyleAttr = "0", @StyleRes int defStyleRes = "0", kotlin.jvm.functions.Function1<? super android.content.res.TypedArray,kotlin.Unit> block);
+ method public static final void withStyledAttributes(android.content.Context, @StyleRes int resourceId, int[] attrs, kotlin.jvm.functions.Function1<? super android.content.res.TypedArray,kotlin.Unit> block);
+ }
+
+ public final class SharedPreferencesKt {
+ ctor public SharedPreferencesKt();
+ method public static final void edit(android.content.SharedPreferences, kotlin.jvm.functions.Function1<? super android.content.SharedPreferences.Editor,kotlin.Unit> action);
+ }
+
+}
+
+package androidx.content.res {
+
+ public final class TypedArrayKt {
+ ctor public TypedArrayKt();
+ method public static final boolean getBooleanOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method @ColorInt public static final int getColorOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static final android.content.res.ColorStateList getColorStateListOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static final float getDimensionOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method @Dimension public static final int getDimensionPixelOffsetOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method @Dimension public static final int getDimensionPixelSizeOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static final android.graphics.drawable.Drawable getDrawableOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static final float getFloatOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method @RequiresApi(26) public static final android.graphics.Typeface getFontOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static final int getIntOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static final int getIntegerOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static final String getStringOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static final CharSequence[] getTextArrayOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static final CharSequence getTextOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ }
+
+}
+
+package androidx.database {
+
+ public final class CursorKt {
+ ctor public CursorKt();
+ method public static final byte[] getBlob(android.database.Cursor, String columnName);
+ method public static final byte[]? getBlobOrNull(android.database.Cursor, int index);
+ method public static final error.NonExistentClass getBlobOrNull(android.database.Cursor, String columnName);
+ method public static final double getDouble(android.database.Cursor, String columnName);
+ method public static final Double? getDoubleOrNull(android.database.Cursor, int index);
+ method public static final error.NonExistentClass getDoubleOrNull(android.database.Cursor, String columnName);
+ method public static final float getFloat(android.database.Cursor, String columnName);
+ method public static final Float? getFloatOrNull(android.database.Cursor, int index);
+ method public static final error.NonExistentClass getFloatOrNull(android.database.Cursor, String columnName);
+ method public static final int getInt(android.database.Cursor, String columnName);
+ method public static final Integer? getIntOrNull(android.database.Cursor, int index);
+ method public static final error.NonExistentClass getIntOrNull(android.database.Cursor, String columnName);
+ method public static final long getLong(android.database.Cursor, String columnName);
+ method public static final Long? getLongOrNull(android.database.Cursor, int index);
+ method public static final error.NonExistentClass getLongOrNull(android.database.Cursor, String columnName);
+ method public static final short getShort(android.database.Cursor, String columnName);
+ method public static final Short? getShortOrNull(android.database.Cursor, int index);
+ method public static final error.NonExistentClass getShortOrNull(android.database.Cursor, String columnName);
+ method public static final String getString(android.database.Cursor, String columnName);
+ method public static final String? getStringOrNull(android.database.Cursor, int index);
+ method public static final error.NonExistentClass getStringOrNull(android.database.Cursor, String columnName);
+ }
+
+}
+
+package androidx.database.sqlite {
+
+ public final class SQLiteDatabaseKt {
+ ctor public SQLiteDatabaseKt();
+ method public static final <T> T! transaction(android.database.sqlite.SQLiteDatabase, boolean exclusive = "true", kotlin.jvm.functions.Function1<? super android.database.sqlite.SQLiteDatabase,? extends T> body);
+ }
+
+}
+
+package androidx.graphics {
+
+ public final class BitmapKt {
+ ctor public BitmapKt();
+ method public static final android.graphics.Bitmap applyCanvas(android.graphics.Bitmap, kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+ method public static final android.graphics.Bitmap createBitmap(int width, int height, android.graphics.Bitmap.Config config = "Bitmap.Config.ARGB_8888");
+ method @RequiresApi(26) public static final android.graphics.Bitmap createBitmap(int width, int height, android.graphics.Bitmap.Config config = "Bitmap.Config.ARGB_8888", boolean hasAlpha = "true", android.graphics.ColorSpace colorSpace = "ColorSpace.get(ColorSpace.Named.SRGB)");
+ method public static final operator int get(android.graphics.Bitmap, int x, int y);
+ method public static final android.graphics.Bitmap scale(android.graphics.Bitmap, int width, int height, boolean filter = "true");
+ method public static final operator void set(android.graphics.Bitmap, int x, int y, @ColorInt int color);
+ }
+
+ public final class CanvasKt {
+ ctor public CanvasKt();
+ method public static final void withRotation(android.graphics.Canvas, float degrees = "0.0f", float pivotX = "0.0f", float pivotY = "0.0f", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+ method public static final void withSave(android.graphics.Canvas, kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+ method public static final void withScale(android.graphics.Canvas, float x = "1.0f", float y = "1.0f", float pivotX = "0.0f", float pivotY = "0.0f", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+ method public static final void withSkew(android.graphics.Canvas, float x = "0.0f", float y = "0.0f", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+ method public static final void withTranslation(android.graphics.Canvas, float x = "0.0f", float y = "0.0f", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+ }
+
+ public final class ColorKt {
+ ctor public ColorKt();
+ method @RequiresApi(26) public static final operator float component1(android.graphics.Color);
+ method public static final operator int component1(int);
+ method @RequiresApi(26) public static final operator float component1(long);
+ method @RequiresApi(26) public static final operator float component2(android.graphics.Color);
+ method public static final operator int component2(int);
+ method @RequiresApi(26) public static final operator float component2(long);
+ method @RequiresApi(26) public static final operator float component3(android.graphics.Color);
+ method public static final operator int component3(int);
+ method @RequiresApi(26) public static final operator float component3(long);
+ method @RequiresApi(26) public static final operator float component4(android.graphics.Color);
+ method public static final operator int component4(int);
+ method @RequiresApi(26) public static final operator float component4(long);
+ method public static final int getAlpha(int);
+ method @RequiresApi(26) public static final float getAlpha(long);
+ method public static final int getBlue(int);
+ method @RequiresApi(26) public static final float getBlue(long);
+ method @RequiresApi(26) public static final android.graphics.ColorSpace getColorSpace(long);
+ method public static final int getGreen(int);
+ method @RequiresApi(26) public static final float getGreen(long);
+ method @RequiresApi(26) public static final float getLuminance(int);
+ method @RequiresApi(26) public static final float getLuminance(long);
+ method public static final int getRed(int);
+ method @RequiresApi(26) public static final float getRed(long);
+ method @RequiresApi(26) public static final boolean isSrgb(long);
+ method @RequiresApi(26) public static final boolean isWideGamut(long);
+ method @RequiresApi(26) public static final operator android.graphics.Color plus(android.graphics.Color, android.graphics.Color c);
+ method @RequiresApi(26) public static final android.graphics.Color toColor(int);
+ method @RequiresApi(26) public static final android.graphics.Color toColor(long);
+ method @RequiresApi(26) @ColorInt public static final int toColorInt(long);
+ method @RequiresApi(26) @ColorLong public static final long toColorLong(int);
+ }
+
+ public final class MatrixKt {
+ ctor public MatrixKt();
+ method public static final error.NonExistentClass rotationMatrix(float degrees, float px = "0.0f", float py = "0.0f");
+ method public static final error.NonExistentClass scaleMatrix(float sx = "1.0f", float sy = "1.0f");
+ method public static final operator error.NonExistentClass times(android.graphics.Matrix, android.graphics.Matrix m);
+ method public static final error.NonExistentClass translationMatrix(float tx = "0.0f", float ty = "0.0f");
+ method public static final error.NonExistentClass values(android.graphics.Matrix);
+ }
+
+ public final class PathKt {
+ ctor public PathKt();
+ method @RequiresApi(19) public static final infix android.graphics.Path and(android.graphics.Path, android.graphics.Path p);
+ method @RequiresApi(26) public static final Iterable<androidx.graphics.PathSegment> flatten(android.graphics.Path, float error = "0.5f");
+ method @RequiresApi(19) public static final operator android.graphics.Path minus(android.graphics.Path, android.graphics.Path p);
+ method @RequiresApi(19) public static final infix android.graphics.Path or(android.graphics.Path, android.graphics.Path p);
+ method @RequiresApi(19) public static final operator android.graphics.Path plus(android.graphics.Path, android.graphics.Path p);
+ method @RequiresApi(19) public static final infix android.graphics.Path xor(android.graphics.Path, android.graphics.Path p);
+ }
+
+ public final class PathSegment {
+ ctor public PathSegment(android.graphics.PointF start, float startFraction, android.graphics.PointF end, float endFraction);
+ method public final android.graphics.PointF component1();
+ method public final float component2();
+ method public final android.graphics.PointF component3();
+ method public final float component4();
+ method public final androidx.graphics.PathSegment copy(android.graphics.PointF start, float startFraction, android.graphics.PointF end, float endFraction);
+ method public final android.graphics.PointF getEnd();
+ method public final float getEndFraction();
+ method public final android.graphics.PointF getStart();
+ method public final float getStartFraction();
+ }
+
+ public final class PictureKt {
+ ctor public PictureKt();
+ method public static final android.graphics.Picture record(android.graphics.Picture, int width, int height, kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+ }
+
+ public final class PointKt {
+ ctor public PointKt();
+ method public static final operator int component1(android.graphics.Point);
+ method public static final operator float component1(android.graphics.PointF);
+ method public static final operator int component2(android.graphics.Point);
+ method public static final operator float component2(android.graphics.PointF);
+ method public static final operator android.graphics.Point minus(android.graphics.Point, android.graphics.Point p);
+ method public static final operator android.graphics.PointF minus(android.graphics.PointF, android.graphics.PointF p);
+ method public static final operator android.graphics.Point minus(android.graphics.Point, int xy);
+ method public static final operator android.graphics.PointF minus(android.graphics.PointF, float xy);
+ method public static final operator android.graphics.Point plus(android.graphics.Point, android.graphics.Point p);
+ method public static final operator android.graphics.PointF plus(android.graphics.PointF, android.graphics.PointF p);
+ method public static final operator android.graphics.Point plus(android.graphics.Point, int xy);
+ method public static final operator android.graphics.PointF plus(android.graphics.PointF, float xy);
+ method public static final android.graphics.Point toPoint(android.graphics.PointF);
+ method public static final android.graphics.PointF toPointF(android.graphics.Point);
+ method public static final operator android.graphics.Point unaryMinus(android.graphics.Point);
+ method public static final operator android.graphics.PointF unaryMinus(android.graphics.PointF);
+ }
+
+ public final class PorterDuffKt {
+ ctor public PorterDuffKt();
+ method public static final android.graphics.PorterDuffColorFilter toColorFilter(android.graphics.PorterDuff.Mode, int color);
+ method public static final android.graphics.PorterDuffXfermode toXfermode(android.graphics.PorterDuff.Mode);
+ }
+
+ public final class RectKt {
+ ctor public RectKt();
+ method public static final infix android.graphics.Rect and(android.graphics.Rect, android.graphics.Rect r);
+ method public static final infix android.graphics.RectF and(android.graphics.RectF, android.graphics.RectF r);
+ method public static final operator int component1(android.graphics.Rect);
+ method public static final operator float component1(android.graphics.RectF);
+ method public static final operator int component2(android.graphics.Rect);
+ method public static final operator float component2(android.graphics.RectF);
+ method public static final operator int component3(android.graphics.Rect);
+ method public static final operator float component3(android.graphics.RectF);
+ method public static final operator int component4(android.graphics.Rect);
+ method public static final operator float component4(android.graphics.RectF);
+ method public static final operator boolean contains(android.graphics.Rect, android.graphics.Point p);
+ method public static final operator boolean contains(android.graphics.RectF, android.graphics.PointF p);
+ method public static final operator android.graphics.Region minus(android.graphics.Rect, android.graphics.Rect r);
+ method public static final operator android.graphics.Region minus(android.graphics.RectF, android.graphics.RectF r);
+ method public static final operator android.graphics.Rect minus(android.graphics.Rect, int xy);
+ method public static final operator android.graphics.RectF minus(android.graphics.RectF, float xy);
+ method public static final operator android.graphics.Rect minus(android.graphics.Rect, android.graphics.Point xy);
+ method public static final operator android.graphics.RectF minus(android.graphics.RectF, android.graphics.PointF xy);
+ method public static final infix android.graphics.Rect or(android.graphics.Rect, android.graphics.Rect r);
+ method public static final infix android.graphics.RectF or(android.graphics.RectF, android.graphics.RectF r);
+ method public static final operator android.graphics.Rect plus(android.graphics.Rect, android.graphics.Rect r);
+ method public static final operator android.graphics.RectF plus(android.graphics.RectF, android.graphics.RectF r);
+ method public static final operator android.graphics.Rect plus(android.graphics.Rect, int xy);
+ method public static final operator android.graphics.RectF plus(android.graphics.RectF, float xy);
+ method public static final operator android.graphics.Rect plus(android.graphics.Rect, android.graphics.Point xy);
+ method public static final operator android.graphics.RectF plus(android.graphics.RectF, android.graphics.PointF xy);
+ method public static final android.graphics.Rect toRect(android.graphics.RectF);
+ method public static final android.graphics.RectF toRectF(android.graphics.Rect);
+ method public static final android.graphics.Region toRegion(android.graphics.Rect);
+ method public static final android.graphics.Region toRegion(android.graphics.RectF);
+ method public static final error.NonExistentClass transform(android.graphics.RectF, android.graphics.Matrix m);
+ method public static final infix android.graphics.Region xor(android.graphics.Rect, android.graphics.Rect r);
+ method public static final infix android.graphics.Region xor(android.graphics.RectF, android.graphics.RectF r);
+ }
+
+ public final class RegionKt {
+ ctor public RegionKt();
+ method public static final infix android.graphics.Region and(android.graphics.Region, android.graphics.Rect r);
+ method public static final infix android.graphics.Region and(android.graphics.Region, android.graphics.Region r);
+ method public static final operator boolean contains(android.graphics.Region, android.graphics.Point p);
+ method public static final void forEach(android.graphics.Region, kotlin.jvm.functions.Function1<? super android.graphics.Rect,kotlin.Unit> action);
+ method public static final operator java.util.Iterator<android.graphics.Rect> iterator(android.graphics.Region);
+ method public static final operator android.graphics.Region minus(android.graphics.Region, android.graphics.Rect r);
+ method public static final operator android.graphics.Region minus(android.graphics.Region, android.graphics.Region r);
+ method public static final operator android.graphics.Region not(android.graphics.Region);
+ method public static final infix android.graphics.Region or(android.graphics.Region, android.graphics.Rect r);
+ method public static final infix android.graphics.Region or(android.graphics.Region, android.graphics.Region r);
+ method public static final operator android.graphics.Region plus(android.graphics.Region, android.graphics.Rect r);
+ method public static final operator android.graphics.Region plus(android.graphics.Region, android.graphics.Region r);
+ method public static final operator android.graphics.Region unaryMinus(android.graphics.Region);
+ method public static final infix android.graphics.Region xor(android.graphics.Region, android.graphics.Rect r);
+ method public static final infix android.graphics.Region xor(android.graphics.Region, android.graphics.Region r);
+ }
+
+ public final class ShaderKt {
+ ctor public ShaderKt();
+ method public static final void transform(android.graphics.Shader, kotlin.jvm.functions.Function1<? super android.graphics.Matrix,kotlin.Unit> block);
+ }
+
+}
+
+package androidx.graphics.drawable {
+
+ public final class BitmapDrawableKt {
+ ctor public BitmapDrawableKt();
+ method public static final android.graphics.drawable.BitmapDrawable toDrawable(android.graphics.Bitmap, android.content.res.Resources resources);
+ }
+
+ public final class ColorDrawableKt {
+ ctor public ColorDrawableKt();
+ method public static final android.graphics.drawable.ColorDrawable toDrawable(int);
+ method @RequiresApi(26) public static final android.graphics.drawable.ColorDrawable toDrawable(android.graphics.Color);
+ }
+
+ public final class DrawableKt {
+ ctor public DrawableKt();
+ method public static final android.graphics.Bitmap toBitmap(android.graphics.drawable.Drawable, int width = "intrinsicWidth", int height = "intrinsicHeight", android.graphics.Bitmap.Config? config = "null");
+ method public static final void updateBounds(android.graphics.drawable.Drawable, int left = "bounds.left", int top = "bounds.top", int right = "bounds.right", int bottom = "bounds.bottom");
+ }
+
+ public final class IconKt {
+ ctor public IconKt();
+ method @RequiresApi(26) public static final android.graphics.drawable.Icon toAdaptiveIcon(android.graphics.Bitmap);
+ method @RequiresApi(26) public static final android.graphics.drawable.Icon toIcon(android.graphics.Bitmap);
+ method @RequiresApi(26) public static final android.graphics.drawable.Icon toIcon(android.net.Uri);
+ method @RequiresApi(26) public static final android.graphics.drawable.Icon toIcon(byte[]);
+ }
+
+}
+
+package androidx.net {
+
+ public final class UriKt {
+ ctor public UriKt();
+ method public static final android.net.Uri toUri(String);
+ }
+
+}
+
+package androidx.os {
+
+ public final class BundleKt {
+ ctor public BundleKt();
+ method public static final error.NonExistentClass bundleOf(kotlin.Pair<String,?>... pairs);
+ }
+
+ public final class HandlerKt {
+ ctor public HandlerKt();
+ method public static final error.NonExistentClass postAtTime(android.os.Handler, long uptimeMillis, Object? token = "null", kotlin.jvm.functions.Function0<kotlin.Unit> action);
+ method public static final void postDelayed(android.os.Handler, Runnable runnable, Object? token, long delayInMillis);
+ method public static final error.NonExistentClass postDelayed(android.os.Handler, long delayInMillis, Object? token = "null", kotlin.jvm.functions.Function0<kotlin.Unit> action);
+ method public static final error.NonExistentClass postDelayed(android.os.Handler, long amount, java.util.concurrent.TimeUnit unit, Object? token = "null", kotlin.jvm.functions.Function0<kotlin.Unit> action);
+ method @RequiresApi(26) public static final error.NonExistentClass postDelayed(android.os.Handler, java.time.Duration duration, Object? token = "null", kotlin.jvm.functions.Function0<kotlin.Unit> action);
+ }
+
+ public final class PersistableBundleKt {
+ ctor public PersistableBundleKt();
+ method @RequiresApi(21) public static final error.NonExistentClass persistableBundleOf(kotlin.Pair<String,?>... pairs);
+ }
+
+ public final class TraceKt {
+ ctor public TraceKt();
+ method public static final <T> T! trace(String sectionName, kotlin.jvm.functions.Function0<? extends T> block);
+ }
+
+}
+
+package androidx.text {
+
+ public final class SpannableStringBuilderKt {
+ ctor public SpannableStringBuilderKt();
+ method public static final android.text.SpannableStringBuilder backgroundColor(android.text.SpannableStringBuilder, @ColorInt int color, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static final android.text.SpannableStringBuilder bold(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static final android.text.SpannedString buildSpannedString(kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static final android.text.SpannableStringBuilder color(android.text.SpannableStringBuilder, @ColorInt int color, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static final android.text.SpannableStringBuilder inSpans(android.text.SpannableStringBuilder, Object[] spans, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static final android.text.SpannableStringBuilder italic(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static final android.text.SpannableStringBuilder underline(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ }
+
+}
+
+package androidx.time {
+
+ public final class DayOfWeekKt {
+ ctor public DayOfWeekKt();
+ method @RequiresApi(26) public static final java.time.DayOfWeek asDayOfWeek(int);
+ method @RequiresApi(26) public static final int asInt(java.time.DayOfWeek);
+ }
+
+ public final class DurationKt {
+ ctor public DurationKt();
+ method @RequiresApi(26) public static final operator long component1(java.time.Duration);
+ method @RequiresApi(26) public static final operator int component2(java.time.Duration);
+ method @RequiresApi(26) public static final operator java.time.Duration div(java.time.Duration, long divisor);
+ method @RequiresApi(260) public static final java.time.Duration hours(int);
+ method @RequiresApi(260) public static final java.time.Duration hours(long);
+ method @RequiresApi(26) public static final java.time.Duration millis(int);
+ method @RequiresApi(26) public static final java.time.Duration millis(long);
+ method @RequiresApi(260) public static final java.time.Duration minutes(int);
+ method @RequiresApi(260) public static final java.time.Duration minutes(long);
+ method @RequiresApi(26) public static final java.time.Duration nanos(int);
+ method @RequiresApi(26) public static final java.time.Duration nanos(long);
+ method @RequiresApi(26) public static final java.time.Duration seconds(int);
+ method @RequiresApi(26) public static final java.time.Duration seconds(long);
+ method @RequiresApi(26) public static final operator java.time.Duration times(java.time.Duration, long multiplicand);
+ method @RequiresApi(26) public static final operator java.time.Duration unaryMinus(java.time.Duration);
+ }
+
+ public final class InstantKt {
+ ctor public InstantKt();
+ method @RequiresApi(26) public static final java.time.Instant asEpochMillis(long);
+ method @RequiresApi(26) public static final java.time.Instant asEpochSeconds(long);
+ method @RequiresApi(26) public static final operator long component1(java.time.Instant);
+ method @RequiresApi(26) public static final operator int component2(java.time.Instant);
+ }
+
+ public final class LocalDateKt {
+ ctor public LocalDateKt();
+ method @RequiresApi(26) public static final operator int component1(java.time.LocalDate);
+ method @RequiresApi(26) public static final operator java.time.Month component2(java.time.LocalDate);
+ method @RequiresApi(26) public static final operator int component3(java.time.LocalDate);
+ }
+
+ public final class LocalDateTimeKt {
+ ctor public LocalDateTimeKt();
+ method @RequiresApi(26) public static final operator java.time.LocalDate component1(java.time.LocalDateTime);
+ method @RequiresApi(26) public static final operator java.time.LocalTime component2(java.time.LocalDateTime);
+ }
+
+ public final class LocalTimeKt {
+ ctor public LocalTimeKt();
+ method @RequiresApi(26) public static final operator int component1(java.time.LocalTime);
+ method @RequiresApi(26) public static final operator int component2(java.time.LocalTime);
+ method @RequiresApi(26) public static final operator int component3(java.time.LocalTime);
+ method @RequiresApi(26) public static final operator int component4(java.time.LocalTime);
+ }
+
+ public final class MonthDayKt {
+ ctor public MonthDayKt();
+ method @RequiresApi(26) public static final operator java.time.Month component1(java.time.MonthDay);
+ method @RequiresApi(26) public static final operator int component2(java.time.MonthDay);
+ }
+
+ public final class MonthKt {
+ ctor public MonthKt();
+ method @RequiresApi(26) public static final int asInt(java.time.Month);
+ method @RequiresApi(26) public static final java.time.Month asMonth(int);
+ }
+
+ public final class OffsetDateTimeKt {
+ ctor public OffsetDateTimeKt();
+ method @RequiresApi(26) public static final operator java.time.LocalDateTime component1(java.time.OffsetDateTime);
+ method @RequiresApi(26) public static final operator java.time.ZoneOffset component2(java.time.OffsetDateTime);
+ }
+
+ public final class OffsetTimeKt {
+ ctor public OffsetTimeKt();
+ method @RequiresApi(26) public static final operator java.time.LocalTime component1(java.time.OffsetTime);
+ method @RequiresApi(26) public static final operator java.time.ZoneOffset component2(java.time.OffsetTime);
+ }
+
+ public final class PeriodKt {
+ ctor public PeriodKt();
+ method @RequiresApi(26) public static final operator int component1(java.time.Period);
+ method @RequiresApi(26) public static final operator int component2(java.time.Period);
+ method @RequiresApi(26) public static final operator int component3(java.time.Period);
+ method @RequiresApi(26) public static final java.time.Period days(int);
+ method @RequiresApi(26) public static final java.time.Period months(int);
+ method @RequiresApi(26) public static final operator java.time.Period times(java.time.Period, int multiplicand);
+ method @RequiresApi(26) public static final operator java.time.Period unaryMinus(java.time.Period);
+ method @RequiresApi(26) public static final java.time.Period years(int);
+ }
+
+ public final class YearKt {
+ ctor public YearKt();
+ method @RequiresApi(26) public static final int asInt(java.time.Year);
+ method @RequiresApi(26) public static final java.time.Year asYear(int);
+ }
+
+ public final class YearMonthKt {
+ ctor public YearMonthKt();
+ method @RequiresApi(26) public static final operator int component1(java.time.YearMonth);
+ method @RequiresApi(26) public static final operator java.time.Month component2(java.time.YearMonth);
+ }
+
+ public final class ZonedDateTimeKt {
+ ctor public ZonedDateTimeKt();
+ method @RequiresApi(26) public static final operator java.time.LocalDateTime component1(java.time.ZonedDateTime);
+ method @RequiresApi(26) public static final operator java.time.ZoneId component2(java.time.ZonedDateTime);
+ }
+
+}
+
+package androidx.transition {
+
+ public final class TransitionKt {
+ ctor public TransitionKt();
+ method @RequiresApi(19) public static final void addListener(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onEnd = "null", kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onStart = "null", kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onCancel = "null", kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onResume = "null", kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onPause = "null");
+ method @RequiresApi(19) public static final void doOnCancel(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+ method @RequiresApi(19) public static final void doOnEnd(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+ method @RequiresApi(19) public static final void doOnPause(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+ method @RequiresApi(19) public static final void doOnResume(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+ method @RequiresApi(19) public static final void doOnStart(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+ }
+
+}
+
+package androidx.util {
+
+ public final class ArrayMapKt {
+ ctor public ArrayMapKt();
+ method @RequiresApi(19) public static final <K, V> android.util.ArrayMap<K,V> arrayMapOf();
+ method @RequiresApi(19) public static final <K, V> android.util.ArrayMap<K,V> arrayMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
+ }
+
+ public final class ArraySetKt {
+ ctor public ArraySetKt();
+ method @RequiresApi(23) public static final <T> android.util.ArraySet<T> arraySetOf();
+ method @RequiresApi(23) public static final <T> android.util.ArraySet<T> arraySetOf(T... values);
+ }
+
+ public final class AtomicFileKt {
+ ctor public AtomicFileKt();
+ method @RequiresApi(17) public static final byte[] readBytes(android.util.AtomicFile);
+ method @RequiresApi(17) public static final String readText(android.util.AtomicFile, java.nio.charset.Charset charset = "Charsets.UTF_8");
+ method @RequiresApi(17) public static final void tryWrite(android.util.AtomicFile, kotlin.jvm.functions.Function1<? super java.io.FileOutputStream,kotlin.Unit> block);
+ method @RequiresApi(17) public static final void writeBytes(android.util.AtomicFile, byte[] array);
+ method @RequiresApi(17) public static final void writeText(android.util.AtomicFile, String text, java.nio.charset.Charset charset = "Charsets.UTF_8");
+ }
+
+ public final class HalfKt {
+ ctor public HalfKt();
+ method @RequiresApi(26) public static final android.util.Half toHalf(short);
+ method @RequiresApi(26) public static final android.util.Half toHalf(float);
+ method @RequiresApi(26) public static final android.util.Half toHalf(double);
+ method @RequiresApi(26) public static final android.util.Half toHalf(String);
+ }
+
+ public final class LongSparseArrayKt {
+ ctor public LongSparseArrayKt();
+ method @RequiresApi(16) public static final operator <T> boolean contains(android.util.LongSparseArray<T>, long key);
+ method @RequiresApi(16) public static final <T> boolean containsKey(android.util.LongSparseArray<T>, long key);
+ method @RequiresApi(16) public static final <T> boolean containsValue(android.util.LongSparseArray<T>, T! value);
+ method @RequiresApi(16) public static final <T> void forEach(android.util.LongSparseArray<T>, kotlin.jvm.functions.Function2<? super Long,? super T,kotlin.Unit> action);
+ method @RequiresApi(16) public static final <T> T! getOrDefault(android.util.LongSparseArray<T>, long key, T! defaultValue);
+ method @RequiresApi(16) public static final <T> T! getOrElse(android.util.LongSparseArray<T>, long key, kotlin.jvm.functions.Function0<? extends T> defaultValue);
+ method @RequiresApi(16) public static final <T> int getSize(android.util.LongSparseArray<T>);
+ method @RequiresApi(16) public static final <T> boolean isEmpty(android.util.LongSparseArray<T>);
+ method @RequiresApi(16) public static final <T> boolean isNotEmpty(android.util.LongSparseArray<T>);
+ method @RequiresApi(16) public static final <T> kotlin.collections.LongIterator keyIterator(android.util.LongSparseArray<T>);
+ method @RequiresApi(16) public static final operator <T> android.util.LongSparseArray<T> plus(android.util.LongSparseArray<T>, android.util.LongSparseArray<T> other);
+ method @RequiresApi(16) public static final <T> void putAll(android.util.LongSparseArray<T>, android.util.LongSparseArray<T> other);
+ method @RequiresApi(16) public static final <T> boolean remove(android.util.LongSparseArray<T>, long key, T! value);
+ method @RequiresApi(16) public static final operator <T> void set(android.util.LongSparseArray<T>, long key, T! value);
+ method @RequiresApi(16) public static final <T> java.util.Iterator<T> valueIterator(android.util.LongSparseArray<T>);
+ }
+
+ public final class PairKt {
+ ctor public PairKt();
+ method public static final operator <F, S> F! component1(android.util.Pair<F,S>);
+ method public static final operator <F, S> S! component2(android.util.Pair<F,S>);
+ method public static final <F, S> android.util.Pair<F,S> toAndroidPair(kotlin.Pair<? extends F,? extends S>);
+ method public static final <F, S> kotlin.Pair<F,S> toKotlinPair(android.util.Pair<F,S>);
+ }
+
+ public final class RangeKt {
+ ctor public RangeKt();
+ method @RequiresApi(21) public static final infix <T extends java.lang.Comparable<? super T>> android.util.Range<T> and(android.util.Range<T>, android.util.Range<T> other);
+ method @RequiresApi(21) public static final operator <T extends java.lang.Comparable<? super T>> android.util.Range<T> plus(android.util.Range<T>, T value);
+ method @RequiresApi(21) public static final operator <T extends java.lang.Comparable<? super T>> android.util.Range<T> plus(android.util.Range<T>, android.util.Range<T> other);
+ method @RequiresApi(21) public static final infix <T extends java.lang.Comparable<? super T>> android.util.Range<T> rangeTo(T, T that);
+ method @RequiresApi(21) public static final <T extends java.lang.Comparable<? super T>> kotlin.ranges.ClosedRange<T> toClosedRange(android.util.Range<T>);
+ method @RequiresApi(21) public static final <T extends java.lang.Comparable<? super T>> android.util.Range<T> toRange(kotlin.ranges.ClosedRange<T>);
+ }
+
+ public final class SizeKt {
+ ctor public SizeKt();
+ method @RequiresApi(21) public static final operator int component1(android.util.Size);
+ method @RequiresApi(21) public static final operator float component1(android.util.SizeF);
+ method @RequiresApi(21) public static final operator int component2(android.util.Size);
+ method @RequiresApi(21) public static final operator float component2(android.util.SizeF);
+ }
+
+ public final class SparseArrayKt {
+ ctor public SparseArrayKt();
+ method public static final operator <T> boolean contains(android.util.SparseArray<T>, int key);
+ method public static final <T> boolean containsKey(android.util.SparseArray<T>, int key);
+ method public static final <T> boolean containsValue(android.util.SparseArray<T>, T! value);
+ method public static final <T> void forEach(android.util.SparseArray<T>, kotlin.jvm.functions.Function2<? super Integer,? super T,kotlin.Unit> action);
+ method public static final <T> T! getOrDefault(android.util.SparseArray<T>, int key, T! defaultValue);
+ method public static final <T> T! getOrElse(android.util.SparseArray<T>, int key, kotlin.jvm.functions.Function0<? extends T> defaultValue);
+ method public static final <T> int getSize(android.util.SparseArray<T>);
+ method public static final <T> boolean isEmpty(android.util.SparseArray<T>);
+ method public static final <T> boolean isNotEmpty(android.util.SparseArray<T>);
+ method public static final <T> kotlin.collections.IntIterator keyIterator(android.util.SparseArray<T>);
+ method public static final operator <T> android.util.SparseArray<T> plus(android.util.SparseArray<T>, android.util.SparseArray<T> other);
+ method public static final <T> void putAll(android.util.SparseArray<T>, android.util.SparseArray<T> other);
+ method public static final <T> boolean remove(android.util.SparseArray<T>, int key, T! value);
+ method public static final operator <T> void set(android.util.SparseArray<T>, int key, T! value);
+ method public static final <T> java.util.Iterator<T> valueIterator(android.util.SparseArray<T>);
+ }
+
+ public final class SparseBooleanArrayKt {
+ ctor public SparseBooleanArrayKt();
+ method public static final operator boolean contains(android.util.SparseBooleanArray, int key);
+ method public static final boolean containsKey(android.util.SparseBooleanArray, int key);
+ method public static final boolean containsValue(android.util.SparseBooleanArray, boolean value);
+ method public static final void forEach(android.util.SparseBooleanArray, kotlin.jvm.functions.Function2<? super Integer,? super Boolean,kotlin.Unit> action);
+ method public static final boolean getOrDefault(android.util.SparseBooleanArray, int key, boolean defaultValue);
+ method public static final error.NonExistentClass getOrElse(android.util.SparseBooleanArray, int key, kotlin.jvm.functions.Function0<Boolean> defaultValue);
+ method public static final int getSize(android.util.SparseBooleanArray);
+ method public static final boolean isEmpty(android.util.SparseBooleanArray);
+ method public static final boolean isNotEmpty(android.util.SparseBooleanArray);
+ method public static final kotlin.collections.IntIterator keyIterator(android.util.SparseBooleanArray);
+ method public static final operator android.util.SparseBooleanArray plus(android.util.SparseBooleanArray, android.util.SparseBooleanArray other);
+ method public static final void putAll(android.util.SparseBooleanArray, android.util.SparseBooleanArray other);
+ method public static final boolean remove(android.util.SparseBooleanArray, int key, boolean value);
+ method public static final void removeAt(android.util.SparseBooleanArray, int index);
+ method public static final operator void set(android.util.SparseBooleanArray, int key, boolean value);
+ method public static final kotlin.collections.BooleanIterator valueIterator(android.util.SparseBooleanArray);
+ }
+
+ public final class SparseIntArrayKt {
+ ctor public SparseIntArrayKt();
+ method public static final operator boolean contains(android.util.SparseIntArray, int key);
+ method public static final boolean containsKey(android.util.SparseIntArray, int key);
+ method public static final boolean containsValue(android.util.SparseIntArray, int value);
+ method public static final void forEach(android.util.SparseIntArray, kotlin.jvm.functions.Function2<? super Integer,? super Integer,kotlin.Unit> action);
+ method public static final int getOrDefault(android.util.SparseIntArray, int key, int defaultValue);
+ method public static final error.NonExistentClass getOrElse(android.util.SparseIntArray, int key, kotlin.jvm.functions.Function0<Integer> defaultValue);
+ method public static final int getSize(android.util.SparseIntArray);
+ method public static final boolean isEmpty(android.util.SparseIntArray);
+ method public static final boolean isNotEmpty(android.util.SparseIntArray);
+ method public static final kotlin.collections.IntIterator keyIterator(android.util.SparseIntArray);
+ method public static final operator android.util.SparseIntArray plus(android.util.SparseIntArray, android.util.SparseIntArray other);
+ method public static final void putAll(android.util.SparseIntArray, android.util.SparseIntArray other);
+ method public static final boolean remove(android.util.SparseIntArray, int key, int value);
+ method public static final operator void set(android.util.SparseIntArray, int key, int value);
+ method public static final kotlin.collections.IntIterator valueIterator(android.util.SparseIntArray);
+ }
+
+ public final class SparseLongArrayKt {
+ ctor public SparseLongArrayKt();
+ method @RequiresApi(18) public static final operator boolean contains(android.util.SparseLongArray, int key);
+ method @RequiresApi(18) public static final boolean containsKey(android.util.SparseLongArray, int key);
+ method @RequiresApi(18) public static final boolean containsValue(android.util.SparseLongArray, long value);
+ method @RequiresApi(18) public static final void forEach(android.util.SparseLongArray, kotlin.jvm.functions.Function2<? super Integer,? super Long,kotlin.Unit> action);
+ method @RequiresApi(18) public static final long getOrDefault(android.util.SparseLongArray, int key, long defaultValue);
+ method @RequiresApi(18) public static final error.NonExistentClass getOrElse(android.util.SparseLongArray, int key, kotlin.jvm.functions.Function0<Long> defaultValue);
+ method @RequiresApi(18) public static final int getSize(android.util.SparseLongArray);
+ method @RequiresApi(18) public static final boolean isEmpty(android.util.SparseLongArray);
+ method @RequiresApi(18) public static final boolean isNotEmpty(android.util.SparseLongArray);
+ method @RequiresApi(18) public static final kotlin.collections.IntIterator keyIterator(android.util.SparseLongArray);
+ method @RequiresApi(18) public static final operator android.util.SparseLongArray plus(android.util.SparseLongArray, android.util.SparseLongArray other);
+ method @RequiresApi(18) public static final void putAll(android.util.SparseLongArray, android.util.SparseLongArray other);
+ method @RequiresApi(18) public static final boolean remove(android.util.SparseLongArray, int key, long value);
+ method @RequiresApi(18) public static final operator void set(android.util.SparseLongArray, int key, long value);
+ method @RequiresApi(18) public static final kotlin.collections.LongIterator valueIterator(android.util.SparseLongArray);
+ }
+
+}
+
+package androidx.view {
+
+ public final class ViewGroupKt {
+ ctor public ViewGroupKt();
+ method public static final operator boolean contains(android.view.ViewGroup, android.view.View view);
+ method public static final void forEach(android.view.ViewGroup, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> action);
+ method public static final void forEachIndexed(android.view.ViewGroup, kotlin.jvm.functions.Function2<? super Integer,? super android.view.View,kotlin.Unit> action);
+ method public static final operator android.view.View get(android.view.ViewGroup, int index);
+ method public static final int getSize(android.view.ViewGroup);
+ method public static final boolean isEmpty(android.view.ViewGroup);
+ method public static final boolean isNotEmpty(android.view.ViewGroup);
+ method public static final operator java.util.Iterator<android.view.View> iterator(android.view.ViewGroup);
+ method public static final operator void minusAssign(android.view.ViewGroup, android.view.View view);
+ method public static final operator void plusAssign(android.view.ViewGroup, android.view.View view);
+ method public static final void setMargins(android.view.ViewGroup.MarginLayoutParams, int size);
+ method public static final void updateMargins(android.view.ViewGroup.MarginLayoutParams, int left = "leftMargin", int top = "topMargin", int right = "rightMargin", int bottom = "bottomMargin");
+ method @RequiresApi(17) public static final void updateMarginsRelative(android.view.ViewGroup.MarginLayoutParams, int start = "marginStart", int top = "topMargin", int end = "marginEnd", int bottom = "bottomMargin");
+ }
+
+ public final class ViewKt {
+ ctor public ViewKt();
+ method public static final void doOnLayout(android.view.View, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> action);
+ method public static final void doOnNextLayout(android.view.View, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> action);
+ method public static final void doOnPreDraw(android.view.View, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> action);
+ method public static final Runnable postDelayed(android.view.View, long delayInMillis, kotlin.jvm.functions.Function0<kotlin.Unit> action);
+ method @RequiresApi(16) public static final Runnable postOnAnimationDelayed(android.view.View, long delayInMillis, kotlin.jvm.functions.Function0<kotlin.Unit> action);
+ method public static final void setPadding(android.view.View, int size);
+ method public static final android.graphics.Bitmap toBitmap(android.view.View, android.graphics.Bitmap.Config config = "Bitmap.Config.ARGB_8888");
+ method public static final void updatePadding(android.view.View, int left = "paddingLeft", int top = "paddingTop", int right = "paddingRight", int bottom = "paddingBottom");
+ method @RequiresApi(17) public static final void updatePaddingRelative(android.view.View, int start = "paddingStart", int top = "paddingTop", int end = "paddingEnd", int bottom = "paddingBottom");
+ }
+
+}
+
diff --git a/core/ktx/api/0.2.txt b/core/ktx/api/0.2.txt
new file mode 100644
index 0000000..5fbfe40
--- /dev/null
+++ b/core/ktx/api/0.2.txt
@@ -0,0 +1,742 @@
+package androidx.animation {
+
+ public final class AnimatorKt {
+ ctor public AnimatorKt();
+ method public static android.animation.Animator.AnimatorListener addListener(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onEnd = "null", kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onStart = "null", kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onCancel = "null", kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onRepeat = "null");
+ method @RequiresApi(19) public static android.animation.Animator.AnimatorPauseListener addPauseListener(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onResume = "null", kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onPause = "null");
+ method public static android.animation.Animator.AnimatorListener doOnCancel(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+ method public static android.animation.Animator.AnimatorListener doOnEnd(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+ method @RequiresApi(19) public static android.animation.Animator.AnimatorPauseListener doOnPause(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+ method public static android.animation.Animator.AnimatorListener doOnRepeat(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+ method @RequiresApi(19) public static android.animation.Animator.AnimatorPauseListener doOnResume(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+ method public static android.animation.Animator.AnimatorListener doOnStart(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+ }
+
+}
+
+package androidx.content {
+
+ public final class ContentValuesKt {
+ ctor public ContentValuesKt();
+ method public static error.NonExistentClass contentValuesOf(kotlin.Pair<String,?>... pairs);
+ }
+
+ public final class ContextKt {
+ ctor public ContextKt();
+ method public static void withStyledAttributes(android.content.Context, android.util.AttributeSet? set = "null", int[] attrs, @AttrRes int defStyleAttr = "0", @StyleRes int defStyleRes = "0", kotlin.jvm.functions.Function1<? super android.content.res.TypedArray,kotlin.Unit> block);
+ method public static void withStyledAttributes(android.content.Context, @StyleRes int resourceId, int[] attrs, kotlin.jvm.functions.Function1<? super android.content.res.TypedArray,kotlin.Unit> block);
+ }
+
+ public final class SharedPreferencesKt {
+ ctor public SharedPreferencesKt();
+ method public static void edit(android.content.SharedPreferences, boolean commit = "false", kotlin.jvm.functions.Function1<? super android.content.SharedPreferences.Editor,kotlin.Unit> action);
+ }
+
+}
+
+package androidx.content.res {
+
+ public final class TypedArrayKt {
+ ctor public TypedArrayKt();
+ method public static boolean getBooleanOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method @ColorInt public static int getColorOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static android.content.res.ColorStateList getColorStateListOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static float getDimensionOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method @Dimension public static int getDimensionPixelOffsetOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method @Dimension public static int getDimensionPixelSizeOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static android.graphics.drawable.Drawable getDrawableOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static float getFloatOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method @RequiresApi(26) public static android.graphics.Typeface getFontOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static int getIntOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static int getIntegerOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method @AnyRes public static int getResourceIdOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static String getStringOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static CharSequence[] getTextArrayOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static CharSequence getTextOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static <R> R! use(android.content.res.TypedArray, kotlin.jvm.functions.Function1<? super android.content.res.TypedArray,? extends R> block);
+ }
+
+}
+
+package androidx.database {
+
+ public final class CursorKt {
+ ctor public CursorKt();
+ method public static byte[] getBlob(android.database.Cursor, String columnName);
+ method public static byte[]? getBlobOrNull(android.database.Cursor, int index);
+ method public static error.NonExistentClass getBlobOrNull(android.database.Cursor, String columnName);
+ method public static double getDouble(android.database.Cursor, String columnName);
+ method public static Double? getDoubleOrNull(android.database.Cursor, int index);
+ method public static error.NonExistentClass getDoubleOrNull(android.database.Cursor, String columnName);
+ method public static float getFloat(android.database.Cursor, String columnName);
+ method public static Float? getFloatOrNull(android.database.Cursor, int index);
+ method public static error.NonExistentClass getFloatOrNull(android.database.Cursor, String columnName);
+ method public static int getInt(android.database.Cursor, String columnName);
+ method public static Integer? getIntOrNull(android.database.Cursor, int index);
+ method public static error.NonExistentClass getIntOrNull(android.database.Cursor, String columnName);
+ method public static long getLong(android.database.Cursor, String columnName);
+ method public static Long? getLongOrNull(android.database.Cursor, int index);
+ method public static error.NonExistentClass getLongOrNull(android.database.Cursor, String columnName);
+ method public static short getShort(android.database.Cursor, String columnName);
+ method public static Short? getShortOrNull(android.database.Cursor, int index);
+ method public static error.NonExistentClass getShortOrNull(android.database.Cursor, String columnName);
+ method public static String getString(android.database.Cursor, String columnName);
+ method public static String? getStringOrNull(android.database.Cursor, int index);
+ method public static error.NonExistentClass getStringOrNull(android.database.Cursor, String columnName);
+ }
+
+}
+
+package androidx.database.sqlite {
+
+ public final class SQLiteDatabaseKt {
+ ctor public SQLiteDatabaseKt();
+ method public static <T> T! transaction(android.database.sqlite.SQLiteDatabase, boolean exclusive = "true", kotlin.jvm.functions.Function1<? super android.database.sqlite.SQLiteDatabase,? extends T> body);
+ }
+
+}
+
+package androidx.graphics {
+
+ public final class BitmapKt {
+ ctor public BitmapKt();
+ method public static android.graphics.Bitmap applyCanvas(android.graphics.Bitmap, kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+ method public static android.graphics.Bitmap createBitmap(int width, int height, android.graphics.Bitmap.Config config = "Bitmap.Config.ARGB_8888");
+ method @RequiresApi(26) public static android.graphics.Bitmap createBitmap(int width, int height, android.graphics.Bitmap.Config config = "Bitmap.Config.ARGB_8888", boolean hasAlpha = "true", android.graphics.ColorSpace colorSpace = "ColorSpace.get(ColorSpace.Named.SRGB)");
+ method public static operator int get(android.graphics.Bitmap, int x, int y);
+ method public static android.graphics.Bitmap scale(android.graphics.Bitmap, int width, int height, boolean filter = "true");
+ method public static operator void set(android.graphics.Bitmap, int x, int y, @ColorInt int color);
+ }
+
+ public final class CanvasKt {
+ ctor public CanvasKt();
+ method public static void withRotation(android.graphics.Canvas, float degrees = "0.0f", float pivotX = "0.0f", float pivotY = "0.0f", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+ method public static void withSave(android.graphics.Canvas, kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+ method public static void withScale(android.graphics.Canvas, float x = "1.0f", float y = "1.0f", float pivotX = "0.0f", float pivotY = "0.0f", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+ method public static void withSkew(android.graphics.Canvas, float x = "0.0f", float y = "0.0f", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+ method public static void withTranslation(android.graphics.Canvas, float x = "0.0f", float y = "0.0f", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+ }
+
+ public final class ColorKt {
+ ctor public ColorKt();
+ method @RequiresApi(26) public static operator float component1(android.graphics.Color);
+ method public static operator int component1(int);
+ method @RequiresApi(26) public static operator float component1(long);
+ method @RequiresApi(26) public static operator float component2(android.graphics.Color);
+ method public static operator int component2(int);
+ method @RequiresApi(26) public static operator float component2(long);
+ method @RequiresApi(26) public static operator float component3(android.graphics.Color);
+ method public static operator int component3(int);
+ method @RequiresApi(26) public static operator float component3(long);
+ method @RequiresApi(26) public static operator float component4(android.graphics.Color);
+ method public static operator int component4(int);
+ method @RequiresApi(26) public static operator float component4(long);
+ method public static int getAlpha(int);
+ method @RequiresApi(26) public static float getAlpha(long);
+ method public static int getBlue(int);
+ method @RequiresApi(26) public static float getBlue(long);
+ method @RequiresApi(26) public static android.graphics.ColorSpace getColorSpace(long);
+ method public static int getGreen(int);
+ method @RequiresApi(26) public static float getGreen(long);
+ method @RequiresApi(26) public static float getLuminance(int);
+ method @RequiresApi(26) public static float getLuminance(long);
+ method public static int getRed(int);
+ method @RequiresApi(26) public static float getRed(long);
+ method @RequiresApi(26) public static boolean isSrgb(long);
+ method @RequiresApi(26) public static boolean isWideGamut(long);
+ method @RequiresApi(26) public static operator android.graphics.Color plus(android.graphics.Color, android.graphics.Color c);
+ method @RequiresApi(26) public static android.graphics.Color toColor(int);
+ method @RequiresApi(26) public static android.graphics.Color toColor(long);
+ method @RequiresApi(26) @ColorInt public static int toColorInt(long);
+ method @ColorInt public static int toColorInt(String);
+ method @RequiresApi(26) @ColorLong public static long toColorLong(int);
+ }
+
+ public final class MatrixKt {
+ ctor public MatrixKt();
+ method public static error.NonExistentClass rotationMatrix(float degrees, float px = "0.0f", float py = "0.0f");
+ method public static error.NonExistentClass scaleMatrix(float sx = "1.0f", float sy = "1.0f");
+ method public static operator error.NonExistentClass times(android.graphics.Matrix, android.graphics.Matrix m);
+ method public static error.NonExistentClass translationMatrix(float tx = "0.0f", float ty = "0.0f");
+ method public static error.NonExistentClass values(android.graphics.Matrix);
+ }
+
+ public final class PathKt {
+ ctor public PathKt();
+ method @RequiresApi(19) public static infix android.graphics.Path and(android.graphics.Path, android.graphics.Path p);
+ method @RequiresApi(26) public static Iterable<androidx.graphics.PathSegment> flatten(android.graphics.Path, float error = "0.5f");
+ method @RequiresApi(19) public static operator android.graphics.Path minus(android.graphics.Path, android.graphics.Path p);
+ method @RequiresApi(19) public static infix android.graphics.Path or(android.graphics.Path, android.graphics.Path p);
+ method @RequiresApi(19) public static operator android.graphics.Path plus(android.graphics.Path, android.graphics.Path p);
+ method @RequiresApi(19) public static infix android.graphics.Path xor(android.graphics.Path, android.graphics.Path p);
+ }
+
+ public final class PathSegment {
+ ctor public PathSegment(android.graphics.PointF start, float startFraction, android.graphics.PointF end, float endFraction);
+ method public android.graphics.PointF component1();
+ method public float component2();
+ method public android.graphics.PointF component3();
+ method public float component4();
+ method public androidx.graphics.PathSegment copy(android.graphics.PointF start, float startFraction, android.graphics.PointF end, float endFraction);
+ method public android.graphics.PointF getEnd();
+ method public float getEndFraction();
+ method public android.graphics.PointF getStart();
+ method public float getStartFraction();
+ }
+
+ public final class PictureKt {
+ ctor public PictureKt();
+ method public static android.graphics.Picture record(android.graphics.Picture, int width, int height, kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+ }
+
+ public final class PointKt {
+ ctor public PointKt();
+ method public static operator int component1(android.graphics.Point);
+ method public static operator float component1(android.graphics.PointF);
+ method public static operator int component2(android.graphics.Point);
+ method public static operator float component2(android.graphics.PointF);
+ method public static operator android.graphics.Point minus(android.graphics.Point, android.graphics.Point p);
+ method public static operator android.graphics.PointF minus(android.graphics.PointF, android.graphics.PointF p);
+ method public static operator android.graphics.Point minus(android.graphics.Point, int xy);
+ method public static operator android.graphics.PointF minus(android.graphics.PointF, float xy);
+ method public static operator android.graphics.Point plus(android.graphics.Point, android.graphics.Point p);
+ method public static operator android.graphics.PointF plus(android.graphics.PointF, android.graphics.PointF p);
+ method public static operator android.graphics.Point plus(android.graphics.Point, int xy);
+ method public static operator android.graphics.PointF plus(android.graphics.PointF, float xy);
+ method public static android.graphics.Point toPoint(android.graphics.PointF);
+ method public static android.graphics.PointF toPointF(android.graphics.Point);
+ method public static operator android.graphics.Point unaryMinus(android.graphics.Point);
+ method public static operator android.graphics.PointF unaryMinus(android.graphics.PointF);
+ }
+
+ public final class PorterDuffKt {
+ ctor public PorterDuffKt();
+ method public static android.graphics.PorterDuffColorFilter toColorFilter(android.graphics.PorterDuff.Mode, int color);
+ method public static android.graphics.PorterDuffXfermode toXfermode(android.graphics.PorterDuff.Mode);
+ }
+
+ public final class RectKt {
+ ctor public RectKt();
+ method public static infix android.graphics.Rect and(android.graphics.Rect, android.graphics.Rect r);
+ method public static infix android.graphics.RectF and(android.graphics.RectF, android.graphics.RectF r);
+ method public static operator int component1(android.graphics.Rect);
+ method public static operator float component1(android.graphics.RectF);
+ method public static operator int component2(android.graphics.Rect);
+ method public static operator float component2(android.graphics.RectF);
+ method public static operator int component3(android.graphics.Rect);
+ method public static operator float component3(android.graphics.RectF);
+ method public static operator int component4(android.graphics.Rect);
+ method public static operator float component4(android.graphics.RectF);
+ method public static operator boolean contains(android.graphics.Rect, android.graphics.Point p);
+ method public static operator boolean contains(android.graphics.RectF, android.graphics.PointF p);
+ method public static operator android.graphics.Region minus(android.graphics.Rect, android.graphics.Rect r);
+ method public static operator android.graphics.Region minus(android.graphics.RectF, android.graphics.RectF r);
+ method public static operator android.graphics.Rect minus(android.graphics.Rect, int xy);
+ method public static operator android.graphics.RectF minus(android.graphics.RectF, float xy);
+ method public static operator android.graphics.Rect minus(android.graphics.Rect, android.graphics.Point xy);
+ method public static operator android.graphics.RectF minus(android.graphics.RectF, android.graphics.PointF xy);
+ method public static infix android.graphics.Rect or(android.graphics.Rect, android.graphics.Rect r);
+ method public static infix android.graphics.RectF or(android.graphics.RectF, android.graphics.RectF r);
+ method public static operator android.graphics.Rect plus(android.graphics.Rect, android.graphics.Rect r);
+ method public static operator android.graphics.RectF plus(android.graphics.RectF, android.graphics.RectF r);
+ method public static operator android.graphics.Rect plus(android.graphics.Rect, int xy);
+ method public static operator android.graphics.RectF plus(android.graphics.RectF, float xy);
+ method public static operator android.graphics.Rect plus(android.graphics.Rect, android.graphics.Point xy);
+ method public static operator android.graphics.RectF plus(android.graphics.RectF, android.graphics.PointF xy);
+ method public static android.graphics.Rect toRect(android.graphics.RectF);
+ method public static android.graphics.RectF toRectF(android.graphics.Rect);
+ method public static android.graphics.Region toRegion(android.graphics.Rect);
+ method public static android.graphics.Region toRegion(android.graphics.RectF);
+ method public static error.NonExistentClass transform(android.graphics.RectF, android.graphics.Matrix m);
+ method public static infix android.graphics.Region xor(android.graphics.Rect, android.graphics.Rect r);
+ method public static infix android.graphics.Region xor(android.graphics.RectF, android.graphics.RectF r);
+ }
+
+ public final class RegionKt {
+ ctor public RegionKt();
+ method public static infix android.graphics.Region and(android.graphics.Region, android.graphics.Rect r);
+ method public static infix android.graphics.Region and(android.graphics.Region, android.graphics.Region r);
+ method public static operator boolean contains(android.graphics.Region, android.graphics.Point p);
+ method public static void forEach(android.graphics.Region, kotlin.jvm.functions.Function1<? super android.graphics.Rect,kotlin.Unit> action);
+ method public static operator java.util.Iterator<android.graphics.Rect> iterator(android.graphics.Region);
+ method public static operator android.graphics.Region minus(android.graphics.Region, android.graphics.Rect r);
+ method public static operator android.graphics.Region minus(android.graphics.Region, android.graphics.Region r);
+ method public static operator android.graphics.Region not(android.graphics.Region);
+ method public static infix android.graphics.Region or(android.graphics.Region, android.graphics.Rect r);
+ method public static infix android.graphics.Region or(android.graphics.Region, android.graphics.Region r);
+ method public static operator android.graphics.Region plus(android.graphics.Region, android.graphics.Rect r);
+ method public static operator android.graphics.Region plus(android.graphics.Region, android.graphics.Region r);
+ method public static operator android.graphics.Region unaryMinus(android.graphics.Region);
+ method public static infix android.graphics.Region xor(android.graphics.Region, android.graphics.Rect r);
+ method public static infix android.graphics.Region xor(android.graphics.Region, android.graphics.Region r);
+ }
+
+ public final class ShaderKt {
+ ctor public ShaderKt();
+ method public static void transform(android.graphics.Shader, kotlin.jvm.functions.Function1<? super android.graphics.Matrix,kotlin.Unit> block);
+ }
+
+}
+
+package androidx.graphics.drawable {
+
+ public final class BitmapDrawableKt {
+ ctor public BitmapDrawableKt();
+ method public static android.graphics.drawable.BitmapDrawable toDrawable(android.graphics.Bitmap, android.content.res.Resources resources);
+ }
+
+ public final class ColorDrawableKt {
+ ctor public ColorDrawableKt();
+ method public static android.graphics.drawable.ColorDrawable toDrawable(int);
+ method @RequiresApi(26) public static android.graphics.drawable.ColorDrawable toDrawable(android.graphics.Color);
+ }
+
+ public final class DrawableKt {
+ ctor public DrawableKt();
+ method public static android.graphics.Bitmap toBitmap(android.graphics.drawable.Drawable, @Px int width = "intrinsicWidth", @Px int height = "intrinsicHeight", android.graphics.Bitmap.Config? config = "null");
+ method public static void updateBounds(android.graphics.drawable.Drawable, @Px int left = "bounds.left", @Px int top = "bounds.top", @Px int right = "bounds.right", @Px int bottom = "bounds.bottom");
+ }
+
+ public final class IconKt {
+ ctor public IconKt();
+ method @RequiresApi(26) public static android.graphics.drawable.Icon toAdaptiveIcon(android.graphics.Bitmap);
+ method @RequiresApi(26) public static android.graphics.drawable.Icon toIcon(android.graphics.Bitmap);
+ method @RequiresApi(26) public static android.graphics.drawable.Icon toIcon(android.net.Uri);
+ method @RequiresApi(26) public static android.graphics.drawable.Icon toIcon(byte[]);
+ }
+
+}
+
+package androidx.net {
+
+ public final class UriKt {
+ ctor public UriKt();
+ method public static android.net.Uri toUri(String);
+ }
+
+}
+
+package androidx.os {
+
+ public final class BundleKt {
+ ctor public BundleKt();
+ method public static error.NonExistentClass bundleOf(kotlin.Pair<String,?>... pairs);
+ }
+
+ public final class FileKt {
+ ctor public FileKt();
+ method public static android.net.Uri toUri(java.io.File);
+ }
+
+ public final class HandlerKt {
+ ctor public HandlerKt();
+ method public static error.NonExistentClass postAtTime(android.os.Handler, long uptimeMillis, Object? token = "null", kotlin.jvm.functions.Function0<kotlin.Unit> action);
+ method public static void postDelayed(android.os.Handler, Runnable runnable, Object? token, long delayInMillis);
+ method public static error.NonExistentClass postDelayed(android.os.Handler, long delayInMillis, Object? token = "null", kotlin.jvm.functions.Function0<kotlin.Unit> action);
+ method public static error.NonExistentClass postDelayed(android.os.Handler, long amount, java.util.concurrent.TimeUnit unit, Object? token = "null", kotlin.jvm.functions.Function0<kotlin.Unit> action);
+ method @RequiresApi(26) public static error.NonExistentClass postDelayed(android.os.Handler, java.time.Duration duration, Object? token = "null", kotlin.jvm.functions.Function0<kotlin.Unit> action);
+ }
+
+ public final class PersistableBundleKt {
+ ctor public PersistableBundleKt();
+ method @RequiresApi(21) public static error.NonExistentClass persistableBundleOf(kotlin.Pair<String,?>... pairs);
+ }
+
+ public final class TraceKt {
+ ctor public TraceKt();
+ method public static <T> T! trace(String sectionName, kotlin.jvm.functions.Function0<? extends T> block);
+ }
+
+}
+
+package androidx.text {
+
+ public final class CharSequenceKt {
+ ctor public CharSequenceKt();
+ method public static boolean isDigitsOnly(CharSequence);
+ method public static int trimmedLength(CharSequence);
+ }
+
+ public final class SpannableStringBuilderKt {
+ ctor public SpannableStringBuilderKt();
+ method public static android.text.SpannableStringBuilder backgroundColor(android.text.SpannableStringBuilder, @ColorInt int color, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static android.text.SpannableStringBuilder bold(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static android.text.SpannedString buildSpannedString(kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static android.text.SpannableStringBuilder color(android.text.SpannableStringBuilder, @ColorInt int color, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static android.text.SpannableStringBuilder inSpans(android.text.SpannableStringBuilder, Object[] spans, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static android.text.SpannableStringBuilder inSpans(android.text.SpannableStringBuilder, Object span, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static android.text.SpannableStringBuilder italic(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static android.text.SpannableStringBuilder scale(android.text.SpannableStringBuilder, float proportion, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static android.text.SpannableStringBuilder strikeThrough(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static android.text.SpannableStringBuilder underline(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ }
+
+ public final class SpannableStringKt {
+ ctor public SpannableStringKt();
+ method public static error.NonExistentClass clearSpans(android.text.Spannable);
+ method public static operator void minusAssign(android.text.Spannable, Object span);
+ method public static operator void plusAssign(android.text.Spannable, Object span);
+ method public static android.text.Spannable toSpannable(CharSequence);
+ }
+
+ public final class SpannedStringKt {
+ ctor public SpannedStringKt();
+ method public static android.text.Spanned toSpanned(CharSequence);
+ }
+
+ public final class StringKt {
+ ctor public StringKt();
+ method public static String htmlEncode(String);
+ }
+
+}
+
+package androidx.time {
+
+ public final class DayOfWeekKt {
+ ctor public DayOfWeekKt();
+ method @RequiresApi(26) deprecated public static java.time.DayOfWeek asDayOfWeek(int);
+ method @RequiresApi(26) deprecated public static int asInt(java.time.DayOfWeek);
+ }
+
+ public final class DeprecationKt {
+ ctor public DeprecationKt();
+ }
+
+ public final class DurationKt {
+ ctor public DurationKt();
+ method @RequiresApi(26) deprecated public static operator long component1(java.time.Duration);
+ method @RequiresApi(26) deprecated public static operator int component2(java.time.Duration);
+ method @RequiresApi(26) deprecated public static operator java.time.Duration div(java.time.Duration, long divisor);
+ method @RequiresApi(26) deprecated public static java.time.Duration hours(int);
+ method @RequiresApi(26) deprecated public static java.time.Duration hours(long);
+ method @RequiresApi(26) deprecated public static java.time.Duration millis(int);
+ method @RequiresApi(26) deprecated public static java.time.Duration millis(long);
+ method @RequiresApi(26) deprecated public static java.time.Duration minutes(int);
+ method @RequiresApi(26) deprecated public static java.time.Duration minutes(long);
+ method @RequiresApi(26) deprecated public static java.time.Duration nanos(int);
+ method @RequiresApi(26) deprecated public static java.time.Duration nanos(long);
+ method @RequiresApi(26) deprecated public static java.time.Duration seconds(int);
+ method @RequiresApi(26) deprecated public static java.time.Duration seconds(long);
+ method @RequiresApi(26) deprecated public static operator java.time.Duration times(java.time.Duration, long multiplicand);
+ method @RequiresApi(26) deprecated public static operator java.time.Duration unaryMinus(java.time.Duration);
+ }
+
+ public final class InstantKt {
+ ctor public InstantKt();
+ method @RequiresApi(26) deprecated public static java.time.Instant asEpochMillis(long);
+ method @RequiresApi(26) deprecated public static java.time.Instant asEpochSeconds(long);
+ method @RequiresApi(26) deprecated public static operator long component1(java.time.Instant);
+ method @RequiresApi(26) deprecated public static operator int component2(java.time.Instant);
+ }
+
+ public final class LocalDateKt {
+ ctor public LocalDateKt();
+ method @RequiresApi(26) deprecated public static operator int component1(java.time.LocalDate);
+ method @RequiresApi(26) deprecated public static operator java.time.Month component2(java.time.LocalDate);
+ method @RequiresApi(26) deprecated public static operator int component3(java.time.LocalDate);
+ }
+
+ public final class LocalDateTimeKt {
+ ctor public LocalDateTimeKt();
+ method @RequiresApi(26) deprecated public static operator java.time.LocalDate component1(java.time.LocalDateTime);
+ method @RequiresApi(26) deprecated public static operator java.time.LocalTime component2(java.time.LocalDateTime);
+ }
+
+ public final class LocalTimeKt {
+ ctor public LocalTimeKt();
+ method @RequiresApi(26) deprecated public static operator int component1(java.time.LocalTime);
+ method @RequiresApi(26) deprecated public static operator int component2(java.time.LocalTime);
+ method @RequiresApi(26) deprecated public static operator int component3(java.time.LocalTime);
+ method @RequiresApi(26) deprecated public static operator int component4(java.time.LocalTime);
+ }
+
+ public final class MonthDayKt {
+ ctor public MonthDayKt();
+ method @RequiresApi(26) deprecated public static operator java.time.Month component1(java.time.MonthDay);
+ method @RequiresApi(26) deprecated public static operator int component2(java.time.MonthDay);
+ }
+
+ public final class MonthKt {
+ ctor public MonthKt();
+ method @RequiresApi(26) deprecated public static int asInt(java.time.Month);
+ method @RequiresApi(26) deprecated public static java.time.Month asMonth(int);
+ }
+
+ public final class OffsetDateTimeKt {
+ ctor public OffsetDateTimeKt();
+ method @RequiresApi(26) deprecated public static operator java.time.LocalDateTime component1(java.time.OffsetDateTime);
+ method @RequiresApi(26) deprecated public static operator java.time.ZoneOffset component2(java.time.OffsetDateTime);
+ }
+
+ public final class OffsetTimeKt {
+ ctor public OffsetTimeKt();
+ method @RequiresApi(26) deprecated public static operator java.time.LocalTime component1(java.time.OffsetTime);
+ method @RequiresApi(26) deprecated public static operator java.time.ZoneOffset component2(java.time.OffsetTime);
+ }
+
+ public final class PeriodKt {
+ ctor public PeriodKt();
+ method @RequiresApi(26) deprecated public static operator int component1(java.time.Period);
+ method @RequiresApi(26) deprecated public static operator int component2(java.time.Period);
+ method @RequiresApi(26) deprecated public static operator int component3(java.time.Period);
+ method @RequiresApi(26) deprecated public static java.time.Period days(int);
+ method @RequiresApi(26) deprecated public static java.time.Period months(int);
+ method @RequiresApi(26) deprecated public static operator java.time.Period times(java.time.Period, int multiplicand);
+ method @RequiresApi(26) deprecated public static operator java.time.Period unaryMinus(java.time.Period);
+ method @RequiresApi(26) deprecated public static java.time.Period years(int);
+ }
+
+ public final class YearKt {
+ ctor public YearKt();
+ method @RequiresApi(26) deprecated public static int asInt(java.time.Year);
+ method @RequiresApi(26) deprecated public static java.time.Year asYear(int);
+ }
+
+ public final class YearMonthKt {
+ ctor public YearMonthKt();
+ method @RequiresApi(26) deprecated public static operator int component1(java.time.YearMonth);
+ method @RequiresApi(26) deprecated public static operator java.time.Month component2(java.time.YearMonth);
+ }
+
+ public final class ZonedDateTimeKt {
+ ctor public ZonedDateTimeKt();
+ method @RequiresApi(26) deprecated public static operator java.time.LocalDateTime component1(java.time.ZonedDateTime);
+ method @RequiresApi(26) deprecated public static operator java.time.ZoneId component2(java.time.ZonedDateTime);
+ }
+
+}
+
+package androidx.transition {
+
+ public final class TransitionKt {
+ ctor public TransitionKt();
+ method @RequiresApi(19) public static void addListener(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onEnd = "null", kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onStart = "null", kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onCancel = "null", kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onResume = "null", kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onPause = "null");
+ method @RequiresApi(19) public static void doOnCancel(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+ method @RequiresApi(19) public static void doOnEnd(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+ method @RequiresApi(19) public static void doOnPause(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+ method @RequiresApi(19) public static void doOnResume(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+ method @RequiresApi(19) public static void doOnStart(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+ }
+
+}
+
+package androidx.util {
+
+ public final class ArrayMapKt {
+ ctor public ArrayMapKt();
+ method @RequiresApi(19) public static <K, V> android.util.ArrayMap<K,V> arrayMapOf();
+ method @RequiresApi(19) public static <K, V> android.util.ArrayMap<K,V> arrayMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
+ }
+
+ public final class ArraySetKt {
+ ctor public ArraySetKt();
+ method @RequiresApi(23) public static <T> android.util.ArraySet<T> arraySetOf();
+ method @RequiresApi(23) public static <T> android.util.ArraySet<T> arraySetOf(T... values);
+ }
+
+ public final class AtomicFileKt {
+ ctor public AtomicFileKt();
+ method @RequiresApi(17) public static byte[] readBytes(android.util.AtomicFile);
+ method @RequiresApi(17) public static String readText(android.util.AtomicFile, java.nio.charset.Charset charset = "Charsets.UTF_8");
+ method @RequiresApi(17) public static void tryWrite(android.util.AtomicFile, kotlin.jvm.functions.Function1<? super java.io.FileOutputStream,kotlin.Unit> block);
+ method @RequiresApi(17) public static void writeBytes(android.util.AtomicFile, byte[] array);
+ method @RequiresApi(17) public static void writeText(android.util.AtomicFile, String text, java.nio.charset.Charset charset = "Charsets.UTF_8");
+ }
+
+ public final class HalfKt {
+ ctor public HalfKt();
+ method @RequiresApi(26) public static android.util.Half toHalf(short);
+ method @RequiresApi(26) public static android.util.Half toHalf(float);
+ method @RequiresApi(26) public static android.util.Half toHalf(double);
+ method @RequiresApi(26) public static android.util.Half toHalf(String);
+ }
+
+ public final class LocaleKt {
+ ctor public LocaleKt();
+ method @RequiresApi(17) public static int getLayoutDirection(java.util.Locale);
+ }
+
+ public final class LongSparseArrayKt {
+ ctor public LongSparseArrayKt();
+ method @RequiresApi(16) public static operator <T> boolean contains(android.util.LongSparseArray<T>, long key);
+ method @RequiresApi(16) public static <T> boolean containsKey(android.util.LongSparseArray<T>, long key);
+ method @RequiresApi(16) public static <T> boolean containsValue(android.util.LongSparseArray<T>, T! value);
+ method @RequiresApi(16) public static <T> void forEach(android.util.LongSparseArray<T>, kotlin.jvm.functions.Function2<? super Long,? super T,kotlin.Unit> action);
+ method @RequiresApi(16) public static <T> T! getOrDefault(android.util.LongSparseArray<T>, long key, T! defaultValue);
+ method @RequiresApi(16) public static <T> T! getOrElse(android.util.LongSparseArray<T>, long key, kotlin.jvm.functions.Function0<? extends T> defaultValue);
+ method @RequiresApi(16) public static <T> int getSize(android.util.LongSparseArray<T>);
+ method @RequiresApi(16) public static <T> boolean isEmpty(android.util.LongSparseArray<T>);
+ method @RequiresApi(16) public static <T> boolean isNotEmpty(android.util.LongSparseArray<T>);
+ method @RequiresApi(16) public static <T> kotlin.collections.LongIterator keyIterator(android.util.LongSparseArray<T>);
+ method @RequiresApi(16) public static operator <T> android.util.LongSparseArray<T> plus(android.util.LongSparseArray<T>, android.util.LongSparseArray<T> other);
+ method @RequiresApi(16) public static <T> void putAll(android.util.LongSparseArray<T>, android.util.LongSparseArray<T> other);
+ method @RequiresApi(16) public static <T> boolean remove(android.util.LongSparseArray<T>, long key, T! value);
+ method @RequiresApi(16) public static operator <T> void set(android.util.LongSparseArray<T>, long key, T! value);
+ method @RequiresApi(16) public static <T> java.util.Iterator<T> valueIterator(android.util.LongSparseArray<T>);
+ }
+
+ public final class LruCacheKt {
+ ctor public LruCacheKt();
+ method public static <K, V> android.util.LruCache<K,V> lruCache(int maxSize, kotlin.jvm.functions.Function2<? super K,? super V,Integer> sizeOf = "{ _, _ -> 1 }", kotlin.jvm.functions.Function1<? super K,? extends V> create = "{ null as V? }", kotlin.jvm.functions.Function4<? super Boolean,? super K,? super V,? super V,kotlin.Unit> onEntryRemoved = "{ _, _, _, _ -> }");
+ }
+
+ public final class PairKt {
+ ctor public PairKt();
+ method public static operator <F, S> F! component1(android.util.Pair<F,S>);
+ method public static operator <F, S> S! component2(android.util.Pair<F,S>);
+ method public static <F, S> android.util.Pair<F,S> toAndroidPair(kotlin.Pair<? extends F,? extends S>);
+ method public static <F, S> kotlin.Pair<F,S> toKotlinPair(android.util.Pair<F,S>);
+ }
+
+ public final class RangeKt {
+ ctor public RangeKt();
+ method @RequiresApi(21) public static infix <T extends java.lang.Comparable<? super T>> android.util.Range<T> and(android.util.Range<T>, android.util.Range<T> other);
+ method @RequiresApi(21) public static operator <T extends java.lang.Comparable<? super T>> android.util.Range<T> plus(android.util.Range<T>, T value);
+ method @RequiresApi(21) public static operator <T extends java.lang.Comparable<? super T>> android.util.Range<T> plus(android.util.Range<T>, android.util.Range<T> other);
+ method @RequiresApi(21) public static infix <T extends java.lang.Comparable<? super T>> android.util.Range<T> rangeTo(T, T that);
+ method @RequiresApi(21) public static <T extends java.lang.Comparable<? super T>> kotlin.ranges.ClosedRange<T> toClosedRange(android.util.Range<T>);
+ method @RequiresApi(21) public static <T extends java.lang.Comparable<? super T>> android.util.Range<T> toRange(kotlin.ranges.ClosedRange<T>);
+ }
+
+ public final class SizeKt {
+ ctor public SizeKt();
+ method @RequiresApi(21) public static operator int component1(android.util.Size);
+ method @RequiresApi(21) public static operator float component1(android.util.SizeF);
+ method @RequiresApi(21) public static operator int component2(android.util.Size);
+ method @RequiresApi(21) public static operator float component2(android.util.SizeF);
+ }
+
+ public final class SparseArrayKt {
+ ctor public SparseArrayKt();
+ method public static operator <T> boolean contains(android.util.SparseArray<T>, int key);
+ method public static <T> boolean containsKey(android.util.SparseArray<T>, int key);
+ method public static <T> boolean containsValue(android.util.SparseArray<T>, T! value);
+ method public static <T> void forEach(android.util.SparseArray<T>, kotlin.jvm.functions.Function2<? super Integer,? super T,kotlin.Unit> action);
+ method public static <T> T! getOrDefault(android.util.SparseArray<T>, int key, T! defaultValue);
+ method public static <T> T! getOrElse(android.util.SparseArray<T>, int key, kotlin.jvm.functions.Function0<? extends T> defaultValue);
+ method public static <T> int getSize(android.util.SparseArray<T>);
+ method public static <T> boolean isEmpty(android.util.SparseArray<T>);
+ method public static <T> boolean isNotEmpty(android.util.SparseArray<T>);
+ method public static <T> kotlin.collections.IntIterator keyIterator(android.util.SparseArray<T>);
+ method public static operator <T> android.util.SparseArray<T> plus(android.util.SparseArray<T>, android.util.SparseArray<T> other);
+ method public static <T> void putAll(android.util.SparseArray<T>, android.util.SparseArray<T> other);
+ method public static <T> boolean remove(android.util.SparseArray<T>, int key, T! value);
+ method public static operator <T> void set(android.util.SparseArray<T>, int key, T! value);
+ method public static <T> java.util.Iterator<T> valueIterator(android.util.SparseArray<T>);
+ }
+
+ public final class SparseBooleanArrayKt {
+ ctor public SparseBooleanArrayKt();
+ method public static operator boolean contains(android.util.SparseBooleanArray, int key);
+ method public static boolean containsKey(android.util.SparseBooleanArray, int key);
+ method public static boolean containsValue(android.util.SparseBooleanArray, boolean value);
+ method public static void forEach(android.util.SparseBooleanArray, kotlin.jvm.functions.Function2<? super Integer,? super Boolean,kotlin.Unit> action);
+ method public static boolean getOrDefault(android.util.SparseBooleanArray, int key, boolean defaultValue);
+ method public static error.NonExistentClass getOrElse(android.util.SparseBooleanArray, int key, kotlin.jvm.functions.Function0<Boolean> defaultValue);
+ method public static int getSize(android.util.SparseBooleanArray);
+ method public static boolean isEmpty(android.util.SparseBooleanArray);
+ method public static boolean isNotEmpty(android.util.SparseBooleanArray);
+ method public static kotlin.collections.IntIterator keyIterator(android.util.SparseBooleanArray);
+ method public static operator android.util.SparseBooleanArray plus(android.util.SparseBooleanArray, android.util.SparseBooleanArray other);
+ method public static void putAll(android.util.SparseBooleanArray, android.util.SparseBooleanArray other);
+ method public static boolean remove(android.util.SparseBooleanArray, int key, boolean value);
+ method public static void removeAt(android.util.SparseBooleanArray, int index);
+ method public static operator void set(android.util.SparseBooleanArray, int key, boolean value);
+ method public static kotlin.collections.BooleanIterator valueIterator(android.util.SparseBooleanArray);
+ }
+
+ public final class SparseIntArrayKt {
+ ctor public SparseIntArrayKt();
+ method public static operator boolean contains(android.util.SparseIntArray, int key);
+ method public static boolean containsKey(android.util.SparseIntArray, int key);
+ method public static boolean containsValue(android.util.SparseIntArray, int value);
+ method public static void forEach(android.util.SparseIntArray, kotlin.jvm.functions.Function2<? super Integer,? super Integer,kotlin.Unit> action);
+ method public static int getOrDefault(android.util.SparseIntArray, int key, int defaultValue);
+ method public static error.NonExistentClass getOrElse(android.util.SparseIntArray, int key, kotlin.jvm.functions.Function0<Integer> defaultValue);
+ method public static int getSize(android.util.SparseIntArray);
+ method public static boolean isEmpty(android.util.SparseIntArray);
+ method public static boolean isNotEmpty(android.util.SparseIntArray);
+ method public static kotlin.collections.IntIterator keyIterator(android.util.SparseIntArray);
+ method public static operator android.util.SparseIntArray plus(android.util.SparseIntArray, android.util.SparseIntArray other);
+ method public static void putAll(android.util.SparseIntArray, android.util.SparseIntArray other);
+ method public static boolean remove(android.util.SparseIntArray, int key, int value);
+ method public static operator void set(android.util.SparseIntArray, int key, int value);
+ method public static kotlin.collections.IntIterator valueIterator(android.util.SparseIntArray);
+ }
+
+ public final class SparseLongArrayKt {
+ ctor public SparseLongArrayKt();
+ method @RequiresApi(18) public static operator boolean contains(android.util.SparseLongArray, int key);
+ method @RequiresApi(18) public static boolean containsKey(android.util.SparseLongArray, int key);
+ method @RequiresApi(18) public static boolean containsValue(android.util.SparseLongArray, long value);
+ method @RequiresApi(18) public static void forEach(android.util.SparseLongArray, kotlin.jvm.functions.Function2<? super Integer,? super Long,kotlin.Unit> action);
+ method @RequiresApi(18) public static long getOrDefault(android.util.SparseLongArray, int key, long defaultValue);
+ method @RequiresApi(18) public static error.NonExistentClass getOrElse(android.util.SparseLongArray, int key, kotlin.jvm.functions.Function0<Long> defaultValue);
+ method @RequiresApi(18) public static int getSize(android.util.SparseLongArray);
+ method @RequiresApi(18) public static boolean isEmpty(android.util.SparseLongArray);
+ method @RequiresApi(18) public static boolean isNotEmpty(android.util.SparseLongArray);
+ method @RequiresApi(18) public static kotlin.collections.IntIterator keyIterator(android.util.SparseLongArray);
+ method @RequiresApi(18) public static operator android.util.SparseLongArray plus(android.util.SparseLongArray, android.util.SparseLongArray other);
+ method @RequiresApi(18) public static void putAll(android.util.SparseLongArray, android.util.SparseLongArray other);
+ method @RequiresApi(18) public static boolean remove(android.util.SparseLongArray, int key, long value);
+ method @RequiresApi(18) public static operator void set(android.util.SparseLongArray, int key, long value);
+ method @RequiresApi(18) public static kotlin.collections.LongIterator valueIterator(android.util.SparseLongArray);
+ }
+
+}
+
+package androidx.view {
+
+ public final class MenuKt {
+ ctor public MenuKt();
+ method public static operator boolean contains(android.view.Menu, android.view.MenuItem item);
+ method public static void forEach(android.view.Menu, kotlin.jvm.functions.Function1<? super android.view.MenuItem,kotlin.Unit> action);
+ method public static void forEachIndexed(android.view.Menu, kotlin.jvm.functions.Function2<? super Integer,? super android.view.MenuItem,kotlin.Unit> action);
+ method public static operator android.view.MenuItem get(android.view.Menu, int index);
+ method public static int getSize(android.view.Menu);
+ method public static boolean isEmpty(android.view.Menu);
+ method public static boolean isNotEmpty(android.view.Menu);
+ method public static operator java.util.Iterator<android.view.MenuItem> iterator(android.view.Menu);
+ }
+
+ public final class ViewGroupKt {
+ ctor public ViewGroupKt();
+ method public static operator boolean contains(android.view.ViewGroup, android.view.View view);
+ method public static void forEach(android.view.ViewGroup, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> action);
+ method public static void forEachIndexed(android.view.ViewGroup, kotlin.jvm.functions.Function2<? super Integer,? super android.view.View,kotlin.Unit> action);
+ method public static operator android.view.View get(android.view.ViewGroup, int index);
+ method public static kotlin.sequences.Sequence<android.view.View> getChildren(android.view.ViewGroup);
+ method public static int getSize(android.view.ViewGroup);
+ method public static boolean isEmpty(android.view.ViewGroup);
+ method public static boolean isNotEmpty(android.view.ViewGroup);
+ method public static operator java.util.Iterator<android.view.View> iterator(android.view.ViewGroup);
+ method public static operator void minusAssign(android.view.ViewGroup, android.view.View view);
+ method public static operator void plusAssign(android.view.ViewGroup, android.view.View view);
+ method public static void setMargins(android.view.ViewGroup.MarginLayoutParams, @Px int size);
+ method public static void updateLayoutParams(android.view.ViewGroup, kotlin.jvm.functions.Function1<? super android.view.ViewGroup.LayoutParams,kotlin.Unit> block);
+ method public static void updateMargins(android.view.ViewGroup.MarginLayoutParams, @Px int left = "leftMargin", @Px int top = "topMargin", @Px int right = "rightMargin", @Px int bottom = "bottomMargin");
+ method @RequiresApi(17) public static void updateMarginsRelative(android.view.ViewGroup.MarginLayoutParams, @Px int start = "marginStart", @Px int top = "topMargin", @Px int end = "marginEnd", @Px int bottom = "bottomMargin");
+ }
+
+ public final class ViewKt {
+ ctor public ViewKt();
+ method public static void doOnLayout(android.view.View, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> action);
+ method public static void doOnNextLayout(android.view.View, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> action);
+ method public static void doOnPreDraw(android.view.View, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> action);
+ method public static boolean isGone(android.view.View);
+ method public static boolean isInvisible(android.view.View);
+ method public static boolean isVisible(android.view.View);
+ method public static Runnable postDelayed(android.view.View, long delayInMillis, kotlin.jvm.functions.Function0<kotlin.Unit> action);
+ method @RequiresApi(16) public static Runnable postOnAnimationDelayed(android.view.View, long delayInMillis, kotlin.jvm.functions.Function0<kotlin.Unit> action);
+ method public static void setGone(android.view.View, boolean value);
+ method public static void setInvisible(android.view.View, boolean value);
+ method public static void setPadding(android.view.View, @Px int size);
+ method public static void setVisible(android.view.View, boolean value);
+ method public static android.graphics.Bitmap toBitmap(android.view.View, android.graphics.Bitmap.Config config = "Bitmap.Config.ARGB_8888");
+ method public static void updatePadding(android.view.View, @Px int left = "paddingLeft", @Px int top = "paddingTop", @Px int right = "paddingRight", @Px int bottom = "paddingBottom");
+ method @RequiresApi(17) public static void updatePaddingRelative(android.view.View, @Px int start = "paddingStart", @Px int top = "paddingTop", @Px int end = "paddingEnd", @Px int bottom = "paddingBottom");
+ }
+
+}
+
diff --git a/core/ktx/api/current.txt b/core/ktx/api/current.txt
new file mode 100644
index 0000000..4c9c96a
--- /dev/null
+++ b/core/ktx/api/current.txt
@@ -0,0 +1,673 @@
+package androidx.core.animation {
+
+ public final class AnimatorKt {
+ ctor public AnimatorKt();
+ method public static android.animation.Animator.AnimatorListener addListener(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onEnd = "null", kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onStart = "null", kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onCancel = "null", kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onRepeat = "null");
+ method @RequiresApi(19) public static android.animation.Animator.AnimatorPauseListener addPauseListener(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onResume = "null", kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit>? onPause = "null");
+ method public static android.animation.Animator.AnimatorListener doOnCancel(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+ method public static android.animation.Animator.AnimatorListener doOnEnd(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+ method @RequiresApi(19) public static android.animation.Animator.AnimatorPauseListener doOnPause(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+ method public static android.animation.Animator.AnimatorListener doOnRepeat(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+ method @RequiresApi(19) public static android.animation.Animator.AnimatorPauseListener doOnResume(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+ method public static android.animation.Animator.AnimatorListener doOnStart(android.animation.Animator, kotlin.jvm.functions.Function1<? super android.animation.Animator,kotlin.Unit> action);
+ }
+
+}
+
+package androidx.core.content {
+
+ public final class ContentValuesKt {
+ ctor public ContentValuesKt();
+ method public static error.NonExistentClass contentValuesOf(kotlin.Pair<java.lang.String,?>... pairs);
+ }
+
+ public final class ContextKt {
+ ctor public ContextKt();
+ method public static void withStyledAttributes(android.content.Context, android.util.AttributeSet? set = "null", int[] attrs, @AttrRes int defStyleAttr = "0", @StyleRes int defStyleRes = "0", kotlin.jvm.functions.Function1<? super android.content.res.TypedArray,kotlin.Unit> block);
+ method public static void withStyledAttributes(android.content.Context, @StyleRes int resourceId, int[] attrs, kotlin.jvm.functions.Function1<? super android.content.res.TypedArray,kotlin.Unit> block);
+ }
+
+ public final class SharedPreferencesKt {
+ ctor public SharedPreferencesKt();
+ method public static void edit(android.content.SharedPreferences, boolean commit = "false", kotlin.jvm.functions.Function1<? super android.content.SharedPreferences.Editor,kotlin.Unit> action);
+ }
+
+}
+
+package androidx.core.content.res {
+
+ public final class TypedArrayKt {
+ ctor public TypedArrayKt();
+ method public static boolean getBooleanOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method @ColorInt public static int getColorOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static android.content.res.ColorStateList getColorStateListOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static float getDimensionOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method @Dimension public static int getDimensionPixelOffsetOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method @Dimension public static int getDimensionPixelSizeOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static android.graphics.drawable.Drawable getDrawableOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static float getFloatOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method @RequiresApi(26) public static android.graphics.Typeface getFontOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static int getIntOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static int getIntegerOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method @AnyRes public static int getResourceIdOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static String getStringOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static CharSequence[] getTextArrayOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static CharSequence getTextOrThrow(android.content.res.TypedArray, @StyleableRes int index);
+ method public static <R> R! use(android.content.res.TypedArray, kotlin.jvm.functions.Function1<? super android.content.res.TypedArray,? extends R> block);
+ }
+
+}
+
+package androidx.core.database {
+
+ public final class CursorKt {
+ ctor public CursorKt();
+ method public static byte[] getBlob(android.database.Cursor, String columnName);
+ method public static byte[]? getBlobOrNull(android.database.Cursor, int index);
+ method public static error.NonExistentClass getBlobOrNull(android.database.Cursor, String columnName);
+ method public static double getDouble(android.database.Cursor, String columnName);
+ method public static Double? getDoubleOrNull(android.database.Cursor, int index);
+ method public static error.NonExistentClass getDoubleOrNull(android.database.Cursor, String columnName);
+ method public static float getFloat(android.database.Cursor, String columnName);
+ method public static Float? getFloatOrNull(android.database.Cursor, int index);
+ method public static error.NonExistentClass getFloatOrNull(android.database.Cursor, String columnName);
+ method public static int getInt(android.database.Cursor, String columnName);
+ method public static Integer? getIntOrNull(android.database.Cursor, int index);
+ method public static error.NonExistentClass getIntOrNull(android.database.Cursor, String columnName);
+ method public static long getLong(android.database.Cursor, String columnName);
+ method public static Long? getLongOrNull(android.database.Cursor, int index);
+ method public static error.NonExistentClass getLongOrNull(android.database.Cursor, String columnName);
+ method public static short getShort(android.database.Cursor, String columnName);
+ method public static Short? getShortOrNull(android.database.Cursor, int index);
+ method public static error.NonExistentClass getShortOrNull(android.database.Cursor, String columnName);
+ method public static String getString(android.database.Cursor, String columnName);
+ method public static String? getStringOrNull(android.database.Cursor, int index);
+ method public static error.NonExistentClass getStringOrNull(android.database.Cursor, String columnName);
+ }
+
+}
+
+package androidx.core.database.sqlite {
+
+ public final class SQLiteDatabaseKt {
+ ctor public SQLiteDatabaseKt();
+ method public static <T> T! transaction(android.database.sqlite.SQLiteDatabase, boolean exclusive = "true", kotlin.jvm.functions.Function1<? super android.database.sqlite.SQLiteDatabase,? extends T> body);
+ }
+
+}
+
+package androidx.core.graphics {
+
+ public final class BitmapKt {
+ ctor public BitmapKt();
+ method public static android.graphics.Bitmap applyCanvas(android.graphics.Bitmap, kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+ method public static android.graphics.Bitmap createBitmap(int width, int height, android.graphics.Bitmap.Config config = "Bitmap.Config.ARGB_8888");
+ method @RequiresApi(26) public static android.graphics.Bitmap createBitmap(int width, int height, android.graphics.Bitmap.Config config = "Bitmap.Config.ARGB_8888", boolean hasAlpha = "true", android.graphics.ColorSpace colorSpace = "ColorSpace.get(ColorSpace.Named.SRGB)");
+ method public static operator int get(android.graphics.Bitmap, int x, int y);
+ method public static android.graphics.Bitmap scale(android.graphics.Bitmap, int width, int height, boolean filter = "true");
+ method public static operator void set(android.graphics.Bitmap, int x, int y, @ColorInt int color);
+ }
+
+ public final class CanvasKt {
+ ctor public CanvasKt();
+ method public static void withMatrix(android.graphics.Canvas, android.graphics.Matrix matrix = "Matrix()", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+ method public static void withRotation(android.graphics.Canvas, float degrees = "0.0f", float pivotX = "0.0f", float pivotY = "0.0f", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+ method public static void withSave(android.graphics.Canvas, kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+ method public static void withScale(android.graphics.Canvas, float x = "1.0f", float y = "1.0f", float pivotX = "0.0f", float pivotY = "0.0f", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+ method public static void withSkew(android.graphics.Canvas, float x = "0.0f", float y = "0.0f", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+ method public static void withTranslation(android.graphics.Canvas, float x = "0.0f", float y = "0.0f", kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+ }
+
+ public final class ColorKt {
+ ctor public ColorKt();
+ method @RequiresApi(26) public static operator float component1(android.graphics.Color);
+ method public static operator int component1(int);
+ method @RequiresApi(26) public static operator float component1(long);
+ method @RequiresApi(26) public static operator float component2(android.graphics.Color);
+ method public static operator int component2(int);
+ method @RequiresApi(26) public static operator float component2(long);
+ method @RequiresApi(26) public static operator float component3(android.graphics.Color);
+ method public static operator int component3(int);
+ method @RequiresApi(26) public static operator float component3(long);
+ method @RequiresApi(26) public static operator float component4(android.graphics.Color);
+ method public static operator int component4(int);
+ method @RequiresApi(26) public static operator float component4(long);
+ method public static int getAlpha(int);
+ method @RequiresApi(26) public static float getAlpha(long);
+ method public static int getBlue(int);
+ method @RequiresApi(26) public static float getBlue(long);
+ method @RequiresApi(26) public static android.graphics.ColorSpace getColorSpace(long);
+ method public static int getGreen(int);
+ method @RequiresApi(26) public static float getGreen(long);
+ method @RequiresApi(26) public static float getLuminance(int);
+ method @RequiresApi(26) public static float getLuminance(long);
+ method public static int getRed(int);
+ method @RequiresApi(26) public static float getRed(long);
+ method @RequiresApi(26) public static boolean isSrgb(long);
+ method @RequiresApi(26) public static boolean isWideGamut(long);
+ method @RequiresApi(26) public static operator android.graphics.Color plus(android.graphics.Color, android.graphics.Color c);
+ method @RequiresApi(26) public static android.graphics.Color toColor(int);
+ method @RequiresApi(26) public static android.graphics.Color toColor(long);
+ method @RequiresApi(26) @ColorInt public static int toColorInt(long);
+ method @ColorInt public static int toColorInt(String);
+ method @RequiresApi(26) @ColorLong public static long toColorLong(int);
+ }
+
+ public final class MatrixKt {
+ ctor public MatrixKt();
+ method public static error.NonExistentClass rotationMatrix(float degrees, float px = "0.0f", float py = "0.0f");
+ method public static error.NonExistentClass scaleMatrix(float sx = "1.0f", float sy = "1.0f");
+ method public static operator error.NonExistentClass times(android.graphics.Matrix, android.graphics.Matrix m);
+ method public static error.NonExistentClass translationMatrix(float tx = "0.0f", float ty = "0.0f");
+ method public static error.NonExistentClass values(android.graphics.Matrix);
+ }
+
+ public final class PathKt {
+ ctor public PathKt();
+ method @RequiresApi(19) public static infix android.graphics.Path and(android.graphics.Path, android.graphics.Path p);
+ method @RequiresApi(26) public static Iterable<androidx.core.graphics.PathSegment> flatten(android.graphics.Path, float error = "0.5f");
+ method @RequiresApi(19) public static operator android.graphics.Path minus(android.graphics.Path, android.graphics.Path p);
+ method @RequiresApi(19) public static infix android.graphics.Path or(android.graphics.Path, android.graphics.Path p);
+ method @RequiresApi(19) public static operator android.graphics.Path plus(android.graphics.Path, android.graphics.Path p);
+ method @RequiresApi(19) public static infix android.graphics.Path xor(android.graphics.Path, android.graphics.Path p);
+ }
+
+ public final class PathSegment {
+ ctor public PathSegment(android.graphics.PointF start, float startFraction, android.graphics.PointF end, float endFraction);
+ method public android.graphics.PointF component1();
+ method public float component2();
+ method public android.graphics.PointF component3();
+ method public float component4();
+ method public androidx.core.graphics.PathSegment copy(android.graphics.PointF start, float startFraction, android.graphics.PointF end, float endFraction);
+ method public android.graphics.PointF getEnd();
+ method public float getEndFraction();
+ method public android.graphics.PointF getStart();
+ method public float getStartFraction();
+ }
+
+ public final class PictureKt {
+ ctor public PictureKt();
+ method public static android.graphics.Picture record(android.graphics.Picture, int width, int height, kotlin.jvm.functions.Function1<? super android.graphics.Canvas,kotlin.Unit> block);
+ }
+
+ public final class PointKt {
+ ctor public PointKt();
+ method public static operator int component1(android.graphics.Point);
+ method public static operator float component1(android.graphics.PointF);
+ method public static operator int component2(android.graphics.Point);
+ method public static operator float component2(android.graphics.PointF);
+ method public static operator android.graphics.Point minus(android.graphics.Point, android.graphics.Point p);
+ method public static operator android.graphics.PointF minus(android.graphics.PointF, android.graphics.PointF p);
+ method public static operator android.graphics.Point minus(android.graphics.Point, int xy);
+ method public static operator android.graphics.PointF minus(android.graphics.PointF, float xy);
+ method public static operator android.graphics.Point plus(android.graphics.Point, android.graphics.Point p);
+ method public static operator android.graphics.PointF plus(android.graphics.PointF, android.graphics.PointF p);
+ method public static operator android.graphics.Point plus(android.graphics.Point, int xy);
+ method public static operator android.graphics.PointF plus(android.graphics.PointF, float xy);
+ method public static android.graphics.Point toPoint(android.graphics.PointF);
+ method public static android.graphics.PointF toPointF(android.graphics.Point);
+ method public static operator android.graphics.Point unaryMinus(android.graphics.Point);
+ method public static operator android.graphics.PointF unaryMinus(android.graphics.PointF);
+ }
+
+ public final class PorterDuffKt {
+ ctor public PorterDuffKt();
+ method public static android.graphics.PorterDuffColorFilter toColorFilter(android.graphics.PorterDuff.Mode, int color);
+ method public static android.graphics.PorterDuffXfermode toXfermode(android.graphics.PorterDuff.Mode);
+ }
+
+ public final class RectKt {
+ ctor public RectKt();
+ method public static infix android.graphics.Rect and(android.graphics.Rect, android.graphics.Rect r);
+ method public static infix android.graphics.RectF and(android.graphics.RectF, android.graphics.RectF r);
+ method public static operator int component1(android.graphics.Rect);
+ method public static operator float component1(android.graphics.RectF);
+ method public static operator int component2(android.graphics.Rect);
+ method public static operator float component2(android.graphics.RectF);
+ method public static operator int component3(android.graphics.Rect);
+ method public static operator float component3(android.graphics.RectF);
+ method public static operator int component4(android.graphics.Rect);
+ method public static operator float component4(android.graphics.RectF);
+ method public static operator boolean contains(android.graphics.Rect, android.graphics.Point p);
+ method public static operator boolean contains(android.graphics.RectF, android.graphics.PointF p);
+ method public static operator android.graphics.Region minus(android.graphics.Rect, android.graphics.Rect r);
+ method public static operator android.graphics.Region minus(android.graphics.RectF, android.graphics.RectF r);
+ method public static operator android.graphics.Rect minus(android.graphics.Rect, int xy);
+ method public static operator android.graphics.RectF minus(android.graphics.RectF, float xy);
+ method public static operator android.graphics.Rect minus(android.graphics.Rect, android.graphics.Point xy);
+ method public static operator android.graphics.RectF minus(android.graphics.RectF, android.graphics.PointF xy);
+ method public static infix android.graphics.Rect or(android.graphics.Rect, android.graphics.Rect r);
+ method public static infix android.graphics.RectF or(android.graphics.RectF, android.graphics.RectF r);
+ method public static operator android.graphics.Rect plus(android.graphics.Rect, android.graphics.Rect r);
+ method public static operator android.graphics.RectF plus(android.graphics.RectF, android.graphics.RectF r);
+ method public static operator android.graphics.Rect plus(android.graphics.Rect, int xy);
+ method public static operator android.graphics.RectF plus(android.graphics.RectF, float xy);
+ method public static operator android.graphics.Rect plus(android.graphics.Rect, android.graphics.Point xy);
+ method public static operator android.graphics.RectF plus(android.graphics.RectF, android.graphics.PointF xy);
+ method public static android.graphics.Rect toRect(android.graphics.RectF);
+ method public static android.graphics.RectF toRectF(android.graphics.Rect);
+ method public static android.graphics.Region toRegion(android.graphics.Rect);
+ method public static android.graphics.Region toRegion(android.graphics.RectF);
+ method public static error.NonExistentClass transform(android.graphics.RectF, android.graphics.Matrix m);
+ method public static infix android.graphics.Region xor(android.graphics.Rect, android.graphics.Rect r);
+ method public static infix android.graphics.Region xor(android.graphics.RectF, android.graphics.RectF r);
+ }
+
+ public final class RegionKt {
+ ctor public RegionKt();
+ method public static infix android.graphics.Region and(android.graphics.Region, android.graphics.Rect r);
+ method public static infix android.graphics.Region and(android.graphics.Region, android.graphics.Region r);
+ method public static operator boolean contains(android.graphics.Region, android.graphics.Point p);
+ method public static void forEach(android.graphics.Region, kotlin.jvm.functions.Function1<? super android.graphics.Rect,kotlin.Unit> action);
+ method public static operator java.util.Iterator<android.graphics.Rect> iterator(android.graphics.Region);
+ method public static operator android.graphics.Region minus(android.graphics.Region, android.graphics.Rect r);
+ method public static operator android.graphics.Region minus(android.graphics.Region, android.graphics.Region r);
+ method public static operator android.graphics.Region not(android.graphics.Region);
+ method public static infix android.graphics.Region or(android.graphics.Region, android.graphics.Rect r);
+ method public static infix android.graphics.Region or(android.graphics.Region, android.graphics.Region r);
+ method public static operator android.graphics.Region plus(android.graphics.Region, android.graphics.Rect r);
+ method public static operator android.graphics.Region plus(android.graphics.Region, android.graphics.Region r);
+ method public static operator android.graphics.Region unaryMinus(android.graphics.Region);
+ method public static infix android.graphics.Region xor(android.graphics.Region, android.graphics.Rect r);
+ method public static infix android.graphics.Region xor(android.graphics.Region, android.graphics.Region r);
+ }
+
+ public final class ShaderKt {
+ ctor public ShaderKt();
+ method public static void transform(android.graphics.Shader, kotlin.jvm.functions.Function1<? super android.graphics.Matrix,kotlin.Unit> block);
+ }
+
+}
+
+package androidx.core.graphics.drawable {
+
+ public final class BitmapDrawableKt {
+ ctor public BitmapDrawableKt();
+ method public static android.graphics.drawable.BitmapDrawable toDrawable(android.graphics.Bitmap, android.content.res.Resources resources);
+ }
+
+ public final class ColorDrawableKt {
+ ctor public ColorDrawableKt();
+ method public static android.graphics.drawable.ColorDrawable toDrawable(int);
+ method @RequiresApi(26) public static android.graphics.drawable.ColorDrawable toDrawable(android.graphics.Color);
+ }
+
+ public final class DrawableKt {
+ ctor public DrawableKt();
+ method public static android.graphics.Bitmap toBitmap(android.graphics.drawable.Drawable, @Px int width = "intrinsicWidth", @Px int height = "intrinsicHeight", android.graphics.Bitmap.Config? config = "null");
+ method public static void updateBounds(android.graphics.drawable.Drawable, @Px int left = "bounds.left", @Px int top = "bounds.top", @Px int right = "bounds.right", @Px int bottom = "bounds.bottom");
+ }
+
+ public final class IconKt {
+ ctor public IconKt();
+ method @RequiresApi(26) public static android.graphics.drawable.Icon toAdaptiveIcon(android.graphics.Bitmap);
+ method @RequiresApi(26) public static android.graphics.drawable.Icon toIcon(android.graphics.Bitmap);
+ method @RequiresApi(26) public static android.graphics.drawable.Icon toIcon(android.net.Uri);
+ method @RequiresApi(26) public static android.graphics.drawable.Icon toIcon(byte[]);
+ }
+
+}
+
+package androidx.core.location {
+
+ public final class LocationKt {
+ ctor public LocationKt();
+ method public static operator double component1(android.location.Location);
+ method public static operator double component2(android.location.Location);
+ }
+
+}
+
+package androidx.core.net {
+
+ public final class UriKt {
+ ctor public UriKt();
+ method public static java.io.File toFile(android.net.Uri);
+ method public static android.net.Uri toUri(String);
+ method public static android.net.Uri toUri(java.io.File);
+ }
+
+}
+
+package androidx.core.os {
+
+ public final class BundleKt {
+ ctor public BundleKt();
+ method public static error.NonExistentClass bundleOf(kotlin.Pair<java.lang.String,?>... pairs);
+ }
+
+ public final class HandlerKt {
+ ctor public HandlerKt();
+ method public static Runnable postAtTime(android.os.Handler, long uptimeMillis, Object? token = "null", kotlin.jvm.functions.Function0<kotlin.Unit> action);
+ method public static Runnable postDelayed(android.os.Handler, long delayInMillis, Object? token = "null", kotlin.jvm.functions.Function0<kotlin.Unit> action);
+ }
+
+ public final class PersistableBundleKt {
+ ctor public PersistableBundleKt();
+ method @RequiresApi(21) public static error.NonExistentClass persistableBundleOf(kotlin.Pair<java.lang.String,?>... pairs);
+ }
+
+ public final class TraceKt {
+ ctor public TraceKt();
+ method public static <T> T! trace(String sectionName, kotlin.jvm.functions.Function0<? extends T> block);
+ }
+
+}
+
+package androidx.core.preference {
+
+ public final class PreferenceGroupKt {
+ ctor public PreferenceGroupKt();
+ method public static operator boolean contains(android.preference.PreferenceGroup, android.preference.Preference preference);
+ method public static void forEach(android.preference.PreferenceGroup, kotlin.jvm.functions.Function1<? super android.preference.Preference,kotlin.Unit> action);
+ method public static void forEachIndexed(android.preference.PreferenceGroup, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super android.preference.Preference,kotlin.Unit> action);
+ method public static operator android.preference.Preference get(android.preference.PreferenceGroup, CharSequence key);
+ method public static operator android.preference.Preference get(android.preference.PreferenceGroup, int index);
+ method public static kotlin.sequences.Sequence<android.preference.Preference> getChildren(android.preference.PreferenceGroup);
+ method public static int getSize(android.preference.PreferenceGroup);
+ method public static boolean isEmpty(android.preference.PreferenceGroup);
+ method public static boolean isNotEmpty(android.preference.PreferenceGroup);
+ method public static operator java.util.Iterator<android.preference.Preference> iterator(android.preference.PreferenceGroup);
+ method public static operator void minusAssign(android.preference.PreferenceGroup, android.preference.Preference preference);
+ method public static operator void plusAssign(android.preference.PreferenceGroup, android.preference.Preference preference);
+ }
+
+}
+
+package androidx.core.text {
+
+ public final class CharSequenceKt {
+ ctor public CharSequenceKt();
+ method public static boolean isDigitsOnly(CharSequence);
+ method public static int trimmedLength(CharSequence);
+ }
+
+ public final class HtmlKt {
+ ctor public HtmlKt();
+ method public static android.text.Spanned parseAsHtml(String, int flags = "FROM_HTML_MODE_LEGACY", android.text.Html.ImageGetter? imageGetter = "null", android.text.Html.TagHandler? tagHandler = "null");
+ method public static String toHtml(android.text.Spanned, int option = "TO_HTML_PARAGRAPH_LINES_CONSECUTIVE");
+ }
+
+ public final class SpannableStringBuilderKt {
+ ctor public SpannableStringBuilderKt();
+ method public static android.text.SpannableStringBuilder backgroundColor(android.text.SpannableStringBuilder, @ColorInt int color, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static android.text.SpannableStringBuilder bold(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static android.text.SpannedString buildSpannedString(kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static android.text.SpannableStringBuilder color(android.text.SpannableStringBuilder, @ColorInt int color, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static android.text.SpannableStringBuilder inSpans(android.text.SpannableStringBuilder, Object[] spans, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static android.text.SpannableStringBuilder inSpans(android.text.SpannableStringBuilder, Object span, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static android.text.SpannableStringBuilder italic(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static android.text.SpannableStringBuilder scale(android.text.SpannableStringBuilder, float proportion, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static android.text.SpannableStringBuilder strikeThrough(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static android.text.SpannableStringBuilder subscript(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static android.text.SpannableStringBuilder superscript(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ method public static android.text.SpannableStringBuilder underline(android.text.SpannableStringBuilder, kotlin.jvm.functions.Function1<? super android.text.SpannableStringBuilder,kotlin.Unit> builderAction);
+ }
+
+ public final class SpannableStringKt {
+ ctor public SpannableStringKt();
+ method public static error.NonExistentClass clearSpans(android.text.Spannable);
+ method public static operator void minusAssign(android.text.Spannable, Object span);
+ method public static operator void plusAssign(android.text.Spannable, Object span);
+ method public static operator void set(android.text.Spannable, int start, int end, Object span);
+ method public static operator void set(android.text.Spannable, kotlin.ranges.IntRange range, Object span);
+ method public static android.text.Spannable toSpannable(CharSequence);
+ }
+
+ public final class SpannedStringKt {
+ ctor public SpannedStringKt();
+ method public static android.text.Spanned toSpanned(CharSequence);
+ }
+
+ public final class StringKt {
+ ctor public StringKt();
+ method public static String htmlEncode(String);
+ }
+
+}
+
+package androidx.core.transition {
+
+ public final class TransitionKt {
+ ctor public TransitionKt();
+ method @RequiresApi(19) public static void addListener(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onEnd = "null", kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onStart = "null", kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onCancel = "null", kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onResume = "null", kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit>? onPause = "null");
+ method @RequiresApi(19) public static void doOnCancel(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+ method @RequiresApi(19) public static void doOnEnd(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+ method @RequiresApi(19) public static void doOnPause(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+ method @RequiresApi(19) public static void doOnResume(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+ method @RequiresApi(19) public static void doOnStart(android.transition.Transition, kotlin.jvm.functions.Function1<? super android.transition.Transition,kotlin.Unit> action);
+ }
+
+}
+
+package androidx.core.util {
+
+ public final class ArrayMapKt {
+ ctor public ArrayMapKt();
+ method @RequiresApi(19) public static <K, V> android.util.ArrayMap<K,V> arrayMapOf();
+ method @RequiresApi(19) public static <K, V> android.util.ArrayMap<K,V> arrayMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
+ }
+
+ public final class ArraySetKt {
+ ctor public ArraySetKt();
+ method @RequiresApi(23) public static <T> android.util.ArraySet<T> arraySetOf();
+ method @RequiresApi(23) public static <T> android.util.ArraySet<T> arraySetOf(T... values);
+ }
+
+ public final class AtomicFileKt {
+ ctor public AtomicFileKt();
+ method @RequiresApi(17) public static byte[] readBytes(android.util.AtomicFile);
+ method @RequiresApi(17) public static String readText(android.util.AtomicFile, java.nio.charset.Charset charset = "Charsets.UTF_8");
+ method @RequiresApi(17) public static void tryWrite(android.util.AtomicFile, kotlin.jvm.functions.Function1<? super java.io.FileOutputStream,kotlin.Unit> block);
+ method @RequiresApi(17) public static void writeBytes(android.util.AtomicFile, byte[] array);
+ method @RequiresApi(17) public static void writeText(android.util.AtomicFile, String text, java.nio.charset.Charset charset = "Charsets.UTF_8");
+ }
+
+ public final class HalfKt {
+ ctor public HalfKt();
+ method @RequiresApi(26) public static android.util.Half toHalf(short);
+ method @RequiresApi(26) public static android.util.Half toHalf(float);
+ method @RequiresApi(26) public static android.util.Half toHalf(double);
+ method @RequiresApi(26) public static android.util.Half toHalf(String);
+ }
+
+ public final class LocaleKt {
+ ctor public LocaleKt();
+ method @RequiresApi(17) public static int getLayoutDirection(java.util.Locale);
+ }
+
+ public final class LongSparseArrayKt {
+ ctor public LongSparseArrayKt();
+ method @RequiresApi(16) public static operator <T> boolean contains(android.util.LongSparseArray<T>, long key);
+ method @RequiresApi(16) public static <T> boolean containsKey(android.util.LongSparseArray<T>, long key);
+ method @RequiresApi(16) public static <T> boolean containsValue(android.util.LongSparseArray<T>, T! value);
+ method @RequiresApi(16) public static <T> void forEach(android.util.LongSparseArray<T>, kotlin.jvm.functions.Function2<? super java.lang.Long,? super T,kotlin.Unit> action);
+ method @RequiresApi(16) public static <T> T! getOrDefault(android.util.LongSparseArray<T>, long key, T! defaultValue);
+ method @RequiresApi(16) public static <T> T! getOrElse(android.util.LongSparseArray<T>, long key, kotlin.jvm.functions.Function0<? extends T> defaultValue);
+ method @RequiresApi(16) public static <T> int getSize(android.util.LongSparseArray<T>);
+ method @RequiresApi(16) public static <T> boolean isEmpty(android.util.LongSparseArray<T>);
+ method @RequiresApi(16) public static <T> boolean isNotEmpty(android.util.LongSparseArray<T>);
+ method @RequiresApi(16) public static <T> kotlin.collections.LongIterator keyIterator(android.util.LongSparseArray<T>);
+ method @RequiresApi(16) public static operator <T> android.util.LongSparseArray<T> plus(android.util.LongSparseArray<T>, android.util.LongSparseArray<T> other);
+ method @RequiresApi(16) public static <T> void putAll(android.util.LongSparseArray<T>, android.util.LongSparseArray<T> other);
+ method @RequiresApi(16) public static <T> boolean remove(android.util.LongSparseArray<T>, long key, T! value);
+ method @RequiresApi(16) public static operator <T> void set(android.util.LongSparseArray<T>, long key, T! value);
+ method @RequiresApi(16) public static <T> java.util.Iterator<T> valueIterator(android.util.LongSparseArray<T>);
+ }
+
+ public final class LruCacheKt {
+ ctor public LruCacheKt();
+ method public static <K, V> android.util.LruCache<K,V> lruCache(int maxSize, kotlin.jvm.functions.Function2<? super K,? super V,java.lang.Integer> sizeOf = "{ _, _ -> 1 }", kotlin.jvm.functions.Function1<? super K,? extends V> create = "{ null as V? }", kotlin.jvm.functions.Function4<? super java.lang.Boolean,? super K,? super V,? super V,kotlin.Unit> onEntryRemoved = "{ _, _, _, _ -> }");
+ }
+
+ public final class PairKt {
+ ctor public PairKt();
+ method public static operator <F, S> F! component1(android.util.Pair<F,S>);
+ method public static operator <F, S> S! component2(android.util.Pair<F,S>);
+ method public static <F, S> android.util.Pair<F,S> toAndroidPair(kotlin.Pair<? extends F,? extends S>);
+ method public static <F, S> kotlin.Pair<F,S> toKotlinPair(android.util.Pair<F,S>);
+ }
+
+ public final class RangeKt {
+ ctor public RangeKt();
+ method @RequiresApi(21) public static infix <T extends java.lang.Comparable<? super T>> android.util.Range<T> and(android.util.Range<T>, android.util.Range<T> other);
+ method @RequiresApi(21) public static operator <T extends java.lang.Comparable<? super T>> android.util.Range<T> plus(android.util.Range<T>, T value);
+ method @RequiresApi(21) public static operator <T extends java.lang.Comparable<? super T>> android.util.Range<T> plus(android.util.Range<T>, android.util.Range<T> other);
+ method @RequiresApi(21) public static infix <T extends java.lang.Comparable<? super T>> android.util.Range<T> rangeTo(T, T that);
+ method @RequiresApi(21) public static <T extends java.lang.Comparable<? super T>> kotlin.ranges.ClosedRange<T> toClosedRange(android.util.Range<T>);
+ method @RequiresApi(21) public static <T extends java.lang.Comparable<? super T>> android.util.Range<T> toRange(kotlin.ranges.ClosedRange<T>);
+ }
+
+ public final class SizeKt {
+ ctor public SizeKt();
+ method @RequiresApi(21) public static operator int component1(android.util.Size);
+ method @RequiresApi(21) public static operator float component1(android.util.SizeF);
+ method @RequiresApi(21) public static operator int component2(android.util.Size);
+ method @RequiresApi(21) public static operator float component2(android.util.SizeF);
+ }
+
+ public final class SparseArrayKt {
+ ctor public SparseArrayKt();
+ method public static operator <T> boolean contains(android.util.SparseArray<T>, int key);
+ method public static <T> boolean containsKey(android.util.SparseArray<T>, int key);
+ method public static <T> boolean containsValue(android.util.SparseArray<T>, T! value);
+ method public static <T> void forEach(android.util.SparseArray<T>, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,kotlin.Unit> action);
+ method public static <T> T! getOrDefault(android.util.SparseArray<T>, int key, T! defaultValue);
+ method public static <T> T! getOrElse(android.util.SparseArray<T>, int key, kotlin.jvm.functions.Function0<? extends T> defaultValue);
+ method public static <T> int getSize(android.util.SparseArray<T>);
+ method public static <T> boolean isEmpty(android.util.SparseArray<T>);
+ method public static <T> boolean isNotEmpty(android.util.SparseArray<T>);
+ method public static <T> kotlin.collections.IntIterator keyIterator(android.util.SparseArray<T>);
+ method public static operator <T> android.util.SparseArray<T> plus(android.util.SparseArray<T>, android.util.SparseArray<T> other);
+ method public static <T> void putAll(android.util.SparseArray<T>, android.util.SparseArray<T> other);
+ method public static <T> boolean remove(android.util.SparseArray<T>, int key, T! value);
+ method public static operator <T> void set(android.util.SparseArray<T>, int key, T! value);
+ method public static <T> java.util.Iterator<T> valueIterator(android.util.SparseArray<T>);
+ }
+
+ public final class SparseBooleanArrayKt {
+ ctor public SparseBooleanArrayKt();
+ method public static operator boolean contains(android.util.SparseBooleanArray, int key);
+ method public static boolean containsKey(android.util.SparseBooleanArray, int key);
+ method public static boolean containsValue(android.util.SparseBooleanArray, boolean value);
+ method public static void forEach(android.util.SparseBooleanArray, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Boolean,kotlin.Unit> action);
+ method public static boolean getOrDefault(android.util.SparseBooleanArray, int key, boolean defaultValue);
+ method public static error.NonExistentClass getOrElse(android.util.SparseBooleanArray, int key, kotlin.jvm.functions.Function0<java.lang.Boolean> defaultValue);
+ method public static int getSize(android.util.SparseBooleanArray);
+ method public static boolean isEmpty(android.util.SparseBooleanArray);
+ method public static boolean isNotEmpty(android.util.SparseBooleanArray);
+ method public static kotlin.collections.IntIterator keyIterator(android.util.SparseBooleanArray);
+ method public static operator android.util.SparseBooleanArray plus(android.util.SparseBooleanArray, android.util.SparseBooleanArray other);
+ method public static void putAll(android.util.SparseBooleanArray, android.util.SparseBooleanArray other);
+ method public static boolean remove(android.util.SparseBooleanArray, int key, boolean value);
+ method public static operator void set(android.util.SparseBooleanArray, int key, boolean value);
+ method public static kotlin.collections.BooleanIterator valueIterator(android.util.SparseBooleanArray);
+ }
+
+ public final class SparseIntArrayKt {
+ ctor public SparseIntArrayKt();
+ method public static operator boolean contains(android.util.SparseIntArray, int key);
+ method public static boolean containsKey(android.util.SparseIntArray, int key);
+ method public static boolean containsValue(android.util.SparseIntArray, int value);
+ method public static void forEach(android.util.SparseIntArray, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,kotlin.Unit> action);
+ method public static int getOrDefault(android.util.SparseIntArray, int key, int defaultValue);
+ method public static error.NonExistentClass getOrElse(android.util.SparseIntArray, int key, kotlin.jvm.functions.Function0<java.lang.Integer> defaultValue);
+ method public static int getSize(android.util.SparseIntArray);
+ method public static boolean isEmpty(android.util.SparseIntArray);
+ method public static boolean isNotEmpty(android.util.SparseIntArray);
+ method public static kotlin.collections.IntIterator keyIterator(android.util.SparseIntArray);
+ method public static operator android.util.SparseIntArray plus(android.util.SparseIntArray, android.util.SparseIntArray other);
+ method public static void putAll(android.util.SparseIntArray, android.util.SparseIntArray other);
+ method public static boolean remove(android.util.SparseIntArray, int key, int value);
+ method public static operator void set(android.util.SparseIntArray, int key, int value);
+ method public static kotlin.collections.IntIterator valueIterator(android.util.SparseIntArray);
+ }
+
+ public final class SparseLongArrayKt {
+ ctor public SparseLongArrayKt();
+ method @RequiresApi(18) public static operator boolean contains(android.util.SparseLongArray, int key);
+ method @RequiresApi(18) public static boolean containsKey(android.util.SparseLongArray, int key);
+ method @RequiresApi(18) public static boolean containsValue(android.util.SparseLongArray, long value);
+ method @RequiresApi(18) public static void forEach(android.util.SparseLongArray, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Long,kotlin.Unit> action);
+ method @RequiresApi(18) public static long getOrDefault(android.util.SparseLongArray, int key, long defaultValue);
+ method @RequiresApi(18) public static error.NonExistentClass getOrElse(android.util.SparseLongArray, int key, kotlin.jvm.functions.Function0<java.lang.Long> defaultValue);
+ method @RequiresApi(18) public static int getSize(android.util.SparseLongArray);
+ method @RequiresApi(18) public static boolean isEmpty(android.util.SparseLongArray);
+ method @RequiresApi(18) public static boolean isNotEmpty(android.util.SparseLongArray);
+ method @RequiresApi(18) public static kotlin.collections.IntIterator keyIterator(android.util.SparseLongArray);
+ method @RequiresApi(18) public static operator android.util.SparseLongArray plus(android.util.SparseLongArray, android.util.SparseLongArray other);
+ method @RequiresApi(18) public static void putAll(android.util.SparseLongArray, android.util.SparseLongArray other);
+ method @RequiresApi(18) public static boolean remove(android.util.SparseLongArray, int key, long value);
+ method @RequiresApi(18) public static operator void set(android.util.SparseLongArray, int key, long value);
+ method @RequiresApi(18) public static kotlin.collections.LongIterator valueIterator(android.util.SparseLongArray);
+ }
+
+}
+
+package androidx.core.view {
+
+ public final class MenuKt {
+ ctor public MenuKt();
+ method public static operator boolean contains(android.view.Menu, android.view.MenuItem item);
+ method public static void forEach(android.view.Menu, kotlin.jvm.functions.Function1<? super android.view.MenuItem,kotlin.Unit> action);
+ method public static void forEachIndexed(android.view.Menu, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super android.view.MenuItem,kotlin.Unit> action);
+ method public static operator android.view.MenuItem get(android.view.Menu, int index);
+ method public static kotlin.sequences.Sequence<android.view.MenuItem> getChildren(android.view.Menu);
+ method public static int getSize(android.view.Menu);
+ method public static boolean isEmpty(android.view.Menu);
+ method public static boolean isNotEmpty(android.view.Menu);
+ method public static operator java.util.Iterator<android.view.MenuItem> iterator(android.view.Menu);
+ method public static operator void minusAssign(android.view.Menu, android.view.MenuItem item);
+ }
+
+ public final class ViewGroupKt {
+ ctor public ViewGroupKt();
+ method public static operator boolean contains(android.view.ViewGroup, android.view.View view);
+ method public static void forEach(android.view.ViewGroup, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> action);
+ method public static void forEachIndexed(android.view.ViewGroup, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super android.view.View,kotlin.Unit> action);
+ method public static operator android.view.View get(android.view.ViewGroup, int index);
+ method public static kotlin.sequences.Sequence<android.view.View> getChildren(android.view.ViewGroup);
+ method public static int getSize(android.view.ViewGroup);
+ method public static boolean isEmpty(android.view.ViewGroup);
+ method public static boolean isNotEmpty(android.view.ViewGroup);
+ method public static operator java.util.Iterator<android.view.View> iterator(android.view.ViewGroup);
+ method public static operator void minusAssign(android.view.ViewGroup, android.view.View view);
+ method public static operator void plusAssign(android.view.ViewGroup, android.view.View view);
+ method public static void setMargins(android.view.ViewGroup.MarginLayoutParams, @Px int size);
+ method public static void updateMargins(android.view.ViewGroup.MarginLayoutParams, @Px int left = "leftMargin", @Px int top = "topMargin", @Px int right = "rightMargin", @Px int bottom = "bottomMargin");
+ method @RequiresApi(17) public static void updateMarginsRelative(android.view.ViewGroup.MarginLayoutParams, @Px int start = "marginStart", @Px int top = "topMargin", @Px int end = "marginEnd", @Px int bottom = "bottomMargin");
+ }
+
+ public final class ViewKt {
+ ctor public ViewKt();
+ method @RequiresApi(16) public static void announceForAccessibility(android.view.View, @StringRes int resource);
+ method public static void doOnLayout(android.view.View, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> action);
+ method public static void doOnNextLayout(android.view.View, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> action);
+ method public static void doOnPreDraw(android.view.View, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> action);
+ method public static boolean isGone(android.view.View);
+ method public static boolean isInvisible(android.view.View);
+ method public static boolean isVisible(android.view.View);
+ method public static Runnable postDelayed(android.view.View, long delayInMillis, kotlin.jvm.functions.Function0<kotlin.Unit> action);
+ method @RequiresApi(16) public static Runnable postOnAnimationDelayed(android.view.View, long delayInMillis, kotlin.jvm.functions.Function0<kotlin.Unit> action);
+ method public static void setGone(android.view.View, boolean value);
+ method public static void setInvisible(android.view.View, boolean value);
+ method public static void setPadding(android.view.View, @Px int size);
+ method public static void setVisible(android.view.View, boolean value);
+ method public static android.graphics.Bitmap toBitmap(android.view.View, android.graphics.Bitmap.Config config = "Bitmap.Config.ARGB_8888");
+ method public static void updateLayoutParams(android.view.View, kotlin.jvm.functions.Function1<? super android.view.ViewGroup.LayoutParams,kotlin.Unit> block);
+ method public static void updatePadding(android.view.View, @Px int left = "paddingLeft", @Px int top = "paddingTop", @Px int right = "paddingRight", @Px int bottom = "paddingBottom");
+ method @RequiresApi(17) public static void updatePaddingRelative(android.view.View, @Px int start = "paddingStart", @Px int top = "paddingTop", @Px int end = "paddingEnd", @Px int bottom = "paddingBottom");
+ }
+
+}
+
+package androidx.core.widget {
+
+ public final class ToastKt {
+ ctor public ToastKt();
+ method public static android.widget.Toast toast(android.content.Context, CharSequence text, int duration = "Toast.LENGTH_SHORT");
+ method public static android.widget.Toast toast(android.content.Context, @StringRes int resId, int duration = "Toast.LENGTH_SHORT");
+ }
+
+}
+
diff --git a/core/ktx/build.gradle b/core/ktx/build.gradle
new file mode 100644
index 0000000..bce0757
--- /dev/null
+++ b/core/ktx/build.gradle
@@ -0,0 +1,37 @@
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ buildTypes {
+ debug {
+ testCoverageEnabled = false // Breaks Kotlin compiler.
+ }
+ }
+}
+
+dependencies {
+ api(KOTLIN_STDLIB)
+ api(project(":annotation"))
+ api(project(":core"))
+
+ androidTestImplementation(JUNIT)
+ androidTestImplementation(TEST_RUNNER)
+ androidTestImplementation(TEST_RULES)
+ androidTestImplementation(TRUTH)
+ androidTestImplementation(project(":internal-testutils-ktx"))
+}
+
+supportLibrary {
+ name = "Core Kotlin Extensions"
+ publish = true
+ mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+ mavenGroup = LibraryGroups.CORE
+ inceptionYear = "2018"
+ description = "Kotlin extensions for 'core' artifact"
+}
diff --git a/core/ktx/src/androidTest/AndroidManifest.xml b/core/ktx/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..259b616
--- /dev/null
+++ b/core/ktx/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.core.ktx.test">
+ <application>
+ <activity android:name="androidx.core.TestActivity"/>
+ <activity android:name="androidx.core.TestPreferenceActivity"/>
+ </application>
+</manifest>
diff --git a/core/ktx/src/androidTest/assets/red.png b/core/ktx/src/androidTest/assets/red.png
new file mode 100644
index 0000000..6292f4b
--- /dev/null
+++ b/core/ktx/src/androidTest/assets/red.png
Binary files differ
diff --git a/core/ktx/src/androidTest/font_licenses.txt b/core/ktx/src/androidTest/font_licenses.txt
new file mode 100644
index 0000000..0b83a9a
--- /dev/null
+++ b/core/ktx/src/androidTest/font_licenses.txt
@@ -0,0 +1,93 @@
+Copyright 2006 The Inconsolata Project Authors
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/core/ktx/src/androidTest/java/androidx/core/TestActivity.kt b/core/ktx/src/androidTest/java/androidx/core/TestActivity.kt
new file mode 100644
index 0000000..b0132ab
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/TestActivity.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core
+
+import android.app.Activity
+import android.os.Bundle
+import androidx.core.ktx.test.R
+
+class TestActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.test_activity)
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/TestPreferenceActivity.kt b/core/ktx/src/androidTest/java/androidx/core/TestPreferenceActivity.kt
new file mode 100644
index 0000000..43e3dee
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/TestPreferenceActivity.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core
+
+import android.app.Activity
+import android.os.Bundle
+import android.preference.PreferenceFragment
+import androidx.core.ktx.test.R
+
+class TestPreferenceActivity : Activity() {
+
+ companion object {
+ const val TAG = "TestPreferenceActivity"
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ fragmentManager.beginTransaction()
+ .add(android.R.id.content, TestPreferenceFragment(), TAG)
+ .commitNow()
+ }
+
+ class TestPreferenceFragment : PreferenceFragment() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ addPreferencesFromResource(R.xml.preferences)
+ }
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/animation/AnimatorTest.kt b/core/ktx/src/androidTest/java/androidx/core/animation/AnimatorTest.kt
new file mode 100644
index 0000000..de298a5
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/animation/AnimatorTest.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.animation
+
+import android.animation.Animator
+import android.animation.ObjectAnimator
+import android.support.test.InstrumentationRegistry
+import android.support.test.annotation.UiThreadTest
+import android.support.test.filters.SdkSuppress
+import android.support.test.runner.AndroidJUnit4
+import android.view.View
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class AnimatorTest {
+ private val context = InstrumentationRegistry.getContext()
+ private val view = View(context)
+
+ private lateinit var animator: Animator
+
+ @Before fun before() {
+ animator = ObjectAnimator.ofFloat(view, View.ALPHA, 0f, 1f)
+ }
+
+ @Test fun testDoOnStart() {
+ var called = false
+ animator.doOnStart {
+ called = true
+ }
+
+ animator.listeners.forEach { it.onAnimationStart(animator) }
+ assertTrue(called)
+ }
+
+ @Test fun testDoOnEnd() {
+ var called = false
+ animator.doOnEnd {
+ called = true
+ }
+
+ animator.listeners.forEach { it.onAnimationEnd(animator) }
+ assertTrue(called)
+ }
+
+ @Test fun testDoOnCancel() {
+ var cancelCalled = false
+ animator.doOnCancel {
+ cancelCalled = true
+ }
+
+ animator.listeners.forEach { it.onAnimationCancel(animator) }
+ assertTrue(cancelCalled)
+ }
+
+ @Test fun testDoOnRepeat() {
+ var called = false
+ animator.doOnRepeat {
+ called = true
+ }
+
+ animator.listeners.forEach { it.onAnimationRepeat(animator) }
+ assertTrue(called)
+ }
+
+ @UiThreadTest
+ @SdkSuppress(minSdkVersion = 19)
+ @Test fun testDoOnPause() {
+ var called = false
+ animator.doOnPause {
+ called = true
+ }
+
+ // Start and pause and assert doOnPause was called
+ animator.start()
+ animator.pause()
+ assertTrue(called)
+
+ animator.cancel()
+ }
+
+ @UiThreadTest
+ @SdkSuppress(minSdkVersion = 19)
+ @Test fun testDoOnResume() {
+ var called = false
+ animator.doOnResume {
+ called = true
+ }
+
+ animator.start()
+ animator.pause()
+
+ // Now resume and assert doOnResume was called
+ animator.resume()
+ assertTrue(called)
+
+ animator.cancel()
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/content/ContentValuesTest.kt b/core/ktx/src/androidTest/java/androidx/core/content/ContentValuesTest.kt
new file mode 100644
index 0000000..746a634
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/content/ContentValuesTest.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.content
+
+import androidx.testutils.assertThrows
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import java.util.concurrent.atomic.AtomicInteger
+
+class ContentValuesTest {
+ @Test fun valuesOfValid() {
+ val values = contentValuesOf(
+ "null" to null,
+ "string" to "string",
+ "byte" to 1.toByte(),
+ "short" to 1.toShort(),
+ "int" to 1,
+ "long" to 1L,
+ "float" to 1f,
+ "double" to 1.0,
+ "boolean" to true,
+ "byteArray" to byteArrayOf()
+ )
+ assertEquals(10, values.size())
+ assertNull(values.get("null"))
+ assertEquals("string", values.get("string"))
+ assertEquals(1.toByte(), values.get("byte"))
+ assertEquals(1.toShort(), values.get("short"))
+ assertEquals(1, values.get("int"))
+ assertEquals(1L, values.get("long"))
+ assertEquals(1f, values.get("float"))
+ assertEquals(1.0, values.get("double"))
+ assertEquals(true, values.get("boolean"))
+ assertArrayEquals(byteArrayOf(), values.get("byteArray") as ByteArray)
+ }
+
+ @Test fun valuesOfInvalid() {
+ assertThrows<IllegalArgumentException> {
+ contentValuesOf("nope" to AtomicInteger(1))
+ }.hasMessageThat().isEqualTo(
+ "Illegal value type java.util.concurrent.atomic.AtomicInteger for key \"nope\"")
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/content/ContextTest.kt b/core/ktx/src/androidTest/java/androidx/core/content/ContextTest.kt
new file mode 100644
index 0000000..09d2355
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/content/ContextTest.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.content
+
+import android.content.ContextWrapper
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SdkSuppress
+import androidx.core.ktx.test.R
+import androidx.core.getAttributeSet
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class ContextTest {
+ private val context = InstrumentationRegistry.getContext()
+
+ @SdkSuppress(minSdkVersion = 23)
+ @Test fun systemService() {
+ var lookup: Class<*>? = null
+ val context = object : ContextWrapper(context) {
+ override fun getSystemServiceName(serviceClass: Class<*>): String? {
+ lookup = serviceClass
+ return if (serviceClass == Unit::class.java) "unit" else null
+ }
+
+ override fun getSystemService(name: String): Any? {
+ return if (name == "unit") Unit else null
+ }
+ }
+ val actual = context.systemService<Unit>()
+ assertEquals(Unit::class.java, lookup)
+ assertSame(Unit, actual)
+ }
+
+ @Test fun withStyledAttributes() {
+ context.withStyledAttributes(attrs = intArrayOf(android.R.attr.textColorPrimary)) {
+ val resourceId = getResourceId(0, -1)
+ assertTrue(resourceId != 1)
+ }
+
+ context.withStyledAttributes(
+ android.R.style.Theme_Light,
+ intArrayOf(android.R.attr.textColorPrimary)
+ ) {
+ val resourceId = getResourceId(0, -1)
+ assertTrue(resourceId != 1)
+ }
+
+ val attrs = context.getAttributeSet(R.layout.test_attrs)
+ context.withStyledAttributes(attrs, R.styleable.SampleAttrs) {
+ assertTrue(getInt(R.styleable.SampleAttrs_sample, -1) != -1)
+ }
+
+ context.withStyledAttributes(attrs, R.styleable.SampleAttrs, 0, 0) {
+ assertTrue(getInt(R.styleable.SampleAttrs_sample, -1) != -1)
+ }
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/content/SharedPreferencesTest.kt b/core/ktx/src/androidTest/java/androidx/core/content/SharedPreferencesTest.kt
new file mode 100644
index 0000000..bf1e67d
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/content/SharedPreferencesTest.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.content
+
+import android.support.test.InstrumentationRegistry
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class SharedPreferencesTest {
+ private val context = InstrumentationRegistry.getContext()
+
+ @Test fun editApply() {
+ val preferences = context.getSharedPreferences("prefs", 0)
+
+ preferences.edit {
+ putString("test_key1", "test_value")
+ putInt("test_key2", 100)
+ }
+
+ assertEquals("test_value", preferences.getString("test_key1", null))
+ assertEquals(100, preferences.getInt("test_key2", 0))
+ }
+
+ @Test fun editCommit() {
+ val preferences = context.getSharedPreferences("prefs", 0)
+ preferences.edit(commit = true) {
+ putString("test_key1", "test_value")
+ putInt("test_key2", 100)
+ }
+
+ assertEquals("test_value", preferences.getString("test_key1", null))
+ assertEquals(100, preferences.getInt("test_key2", 0))
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/content/res/TypedArrayTest.kt b/core/ktx/src/androidTest/java/androidx/core/content/res/TypedArrayTest.kt
new file mode 100644
index 0000000..fe576df
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/content/res/TypedArrayTest.kt
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.content.res
+
+import android.graphics.Color
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SdkSuppress
+import androidx.core.ktx.test.R
+import androidx.testutils.assertThrows
+import androidx.core.getAttributeSet
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class TypedArrayTest {
+ private val context = InstrumentationRegistry.getContext()
+
+ @Test fun boolean() {
+ val attrs = context.getAttributeSet(R.layout.typed_array)
+ val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+ assertTrue(array.getBooleanOrThrow(R.styleable.TypedArrayTypes_boolean_present))
+
+ assertThrows<IllegalArgumentException> {
+ array.getBooleanOrThrow(R.styleable.TypedArrayTypes_boolean_absent)
+ }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+ }
+
+ @Test fun color() {
+ val attrs = context.getAttributeSet(R.layout.typed_array)
+ val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+ assertEquals(Color.WHITE, array.getColorOrThrow(R.styleable.TypedArrayTypes_color_present))
+
+ assertThrows<IllegalArgumentException> {
+ array.getColorOrThrow(R.styleable.TypedArrayTypes_color_absent)
+ }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+ }
+
+ @Test fun colorStateList() {
+ val attrs = context.getAttributeSet(R.layout.typed_array)
+ val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+ val stateList = array.getColorStateListOrThrow(R.styleable.TypedArrayTypes_color_present)
+
+ assertEquals(Color.WHITE, stateList.defaultColor)
+
+ assertThrows<IllegalArgumentException> {
+ array.getColorStateListOrThrow(R.styleable.TypedArrayTypes_color_absent)
+ }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+ }
+
+ @Test fun dimension() {
+ val attrs = context.getAttributeSet(R.layout.typed_array)
+ val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+ assertEquals(1f, array.getDimensionOrThrow(R.styleable.TypedArrayTypes_dimension_present))
+
+ assertThrows<IllegalArgumentException> {
+ array.getDimensionOrThrow(R.styleable.TypedArrayTypes_dimension_absent)
+ }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+ }
+
+ @Test fun dimensionPixelSize() {
+ val attrs = context.getAttributeSet(R.layout.typed_array)
+ val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+ assertEquals(1,
+ array.getDimensionPixelSizeOrThrow(R.styleable.TypedArrayTypes_dimension_present))
+
+ assertThrows<IllegalArgumentException> {
+ array.getDimensionPixelSizeOrThrow(R.styleable.TypedArrayTypes_dimension_absent)
+ }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+ }
+
+ @Test fun dimensionPixelOffset() {
+ val attrs = context.getAttributeSet(R.layout.typed_array)
+ val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+ assertEquals(1,
+ array.getDimensionPixelOffsetOrThrow(R.styleable.TypedArrayTypes_dimension_present))
+
+ assertThrows<IllegalArgumentException> {
+ array.getDimensionPixelOffsetOrThrow(R.styleable.TypedArrayTypes_dimension_absent)
+ }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+ }
+
+ @Test fun drawable() {
+ val attrs = context.getAttributeSet(R.layout.typed_array)
+ val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+ assertNotNull(array.getDrawableOrThrow(R.styleable.TypedArrayTypes_drawable_present))
+
+ assertThrows<IllegalArgumentException> {
+ array.getDrawableOrThrow(R.styleable.TypedArrayTypes_drawable_absent)
+ }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+ }
+
+ @Test fun float() {
+ val attrs = context.getAttributeSet(R.layout.typed_array)
+ val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+ assertEquals(0.1f, array.getFloatOrThrow(R.styleable.TypedArrayTypes_float_present))
+
+ assertThrows<IllegalArgumentException> {
+ array.getFloatOrThrow(R.styleable.TypedArrayTypes_float_absent)
+ }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+ }
+
+ @SdkSuppress(minSdkVersion = 26)
+ @Test fun font() {
+ val attrs = context.getAttributeSet(R.layout.typed_array)
+ val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+ assertNotNull(array.getFontOrThrow(R.styleable.TypedArrayTypes_font_present))
+
+ assertThrows<IllegalArgumentException> {
+ array.getFontOrThrow(R.styleable.TypedArrayTypes_font_absent)
+ }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+ }
+
+ @Test fun int() {
+ val attrs = context.getAttributeSet(R.layout.typed_array)
+ val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+ assertEquals(1, array.getIntOrThrow(R.styleable.TypedArrayTypes_integer_present))
+
+ assertThrows<IllegalArgumentException> {
+ array.getIntOrThrow(R.styleable.TypedArrayTypes_integer_absent)
+ }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+ }
+
+ @Test fun integer() {
+ val attrs = context.getAttributeSet(R.layout.typed_array)
+ val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+ assertEquals(1, array.getIntegerOrThrow(R.styleable.TypedArrayTypes_integer_present))
+
+ assertThrows<IllegalArgumentException> {
+ array.getIntegerOrThrow(R.styleable.TypedArrayTypes_integer_absent)
+ }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+ }
+
+ @Test
+ fun resourceId() {
+ val attrs = context.getAttributeSet(R.layout.typed_array)
+ val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+ assertEquals(
+ R.font.inconsolata_regular,
+ array.getResourceIdOrThrow(R.styleable.TypedArrayTypes_resource_present)
+ )
+
+ assertThrows<IllegalArgumentException> {
+ array.getResourceIdOrThrow(R.styleable.TypedArrayTypes_resource_absent)
+ }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+ }
+
+ @Test fun string() {
+ val attrs = context.getAttributeSet(R.layout.typed_array)
+ val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+ assertEquals("Hello", array.getStringOrThrow(R.styleable.TypedArrayTypes_string_present))
+
+ assertThrows<IllegalArgumentException> {
+ array.getStringOrThrow(R.styleable.TypedArrayTypes_string_absent)
+ }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+ }
+
+ @Test fun text() {
+ val attrs = context.getAttributeSet(R.layout.typed_array)
+ val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+ assertEquals("Hello", array.getTextOrThrow(R.styleable.TypedArrayTypes_string_present))
+
+ assertThrows<IllegalArgumentException> {
+ array.getTextOrThrow(R.styleable.TypedArrayTypes_string_absent)
+ }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+ }
+
+ @Test fun textArray() {
+ val attrs = context.getAttributeSet(R.layout.typed_array)
+ val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+ val text = array.getTextArrayOrThrow(R.styleable.TypedArrayTypes_text_array_present)
+ assertEquals("Hello", text[0].toString())
+ assertEquals("World", text[1].toString())
+
+ assertThrows<IllegalArgumentException> {
+ array.getTextOrThrow(R.styleable.TypedArrayTypes_text_array_absent)
+ }.hasMessageThat().isEqualTo("Attribute not defined in set.")
+ }
+
+ @Test fun useRecyclesArray() {
+ val attrs = context.getAttributeSet(R.layout.typed_array)
+ val array = context.obtainStyledAttributes(attrs, R.styleable.TypedArrayTypes)
+
+ val result = array.use {
+ it.getBoolean(R.styleable.TypedArrayTypes_boolean_present, false)
+ }
+ assertTrue(result)
+
+ assertThrows<RuntimeException> {
+ array.recycle()
+ }
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/database/CursorTest.kt b/core/ktx/src/androidTest/java/androidx/core/database/CursorTest.kt
new file mode 100644
index 0000000..86d0731
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/database/CursorTest.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.database
+
+import android.database.Cursor
+import android.database.MatrixCursor
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+class CursorTest {
+ @Test fun blobByName() {
+ val cursor = scalarCursor(byteArrayOf(0x01))
+ val blob = cursor.getBlob("data")
+ assertArrayEquals(byteArrayOf(0x01), blob)
+ }
+
+ @Test fun doubleByName() {
+ val cursor = scalarCursor(1.5)
+ val double = cursor.getDouble("data")
+ assertEquals(1.5, double, 0.0)
+ }
+
+ @Test fun floatByName() {
+ val cursor = scalarCursor(1.5f)
+ val float = cursor.getFloat("data")
+ assertEquals(1.5f, float, 0f)
+ }
+
+ @Test fun intByName() {
+ val cursor = scalarCursor(1)
+ val int = cursor.getInt("data")
+ assertEquals(1, int)
+ }
+
+ @Test fun longByName() {
+ val cursor = scalarCursor(1L)
+ val long = cursor.getLong("data")
+ assertEquals(1L, long)
+ }
+
+ @Test fun shortByName() {
+ val cursor = scalarCursor(1.toShort())
+ val short = cursor.getShort("data")
+ assertEquals(1.toShort(), short)
+ }
+
+ @Test fun stringByName() {
+ val cursor = scalarCursor("hey")
+ val string = cursor.getString("data")
+ assertEquals("hey", string)
+ }
+
+ @Test fun blobOrNullByIndex() {
+ val cursor = scalarCursor(null)
+ val blob = cursor.getBlobOrNull(0)
+ assertNull(blob)
+ }
+
+ @Test fun doubleOrNullByIndex() {
+ val cursor = scalarCursor(null)
+ val double = cursor.getDoubleOrNull(0)
+ assertNull(double)
+ }
+
+ @Test fun floatOrNullByIndex() {
+ val cursor = scalarCursor(null)
+ val float = cursor.getFloatOrNull(0)
+ assertNull(float)
+ }
+
+ @Test fun intOrNullByIndex() {
+ val cursor = scalarCursor(null)
+ val int = cursor.getIntOrNull(0)
+ assertNull(int)
+ }
+
+ @Test fun longOrNullByIndex() {
+ val cursor = scalarCursor(null)
+ val long = cursor.getLongOrNull(0)
+ assertNull(long)
+ }
+
+ @Test fun shortOrNullByIndex() {
+ val cursor = scalarCursor(null)
+ val short = cursor.getShortOrNull(0)
+ assertNull(short)
+ }
+
+ @Test fun stringOrNullByIndex() {
+ val cursor = scalarCursor(null)
+ val string = cursor.getStringOrNull(0)
+ assertNull(string)
+ }
+
+ @Test fun blobOrNullByName() {
+ val cursor = scalarCursor(null)
+ val blob = cursor.getBlobOrNull("data")
+ assertNull(blob)
+ }
+
+ @Test fun doubleOrNullByName() {
+ val cursor = scalarCursor(null)
+ val double = cursor.getDoubleOrNull("data")
+ assertNull(double)
+ }
+
+ @Test fun floatOrNullByName() {
+ val cursor = scalarCursor(null)
+ val float = cursor.getFloatOrNull("data")
+ assertNull(float)
+ }
+
+ @Test fun intOrNullByName() {
+ val cursor = scalarCursor(null)
+ val int = cursor.getIntOrNull("data")
+ assertNull(int)
+ }
+
+ @Test fun longOrNullByName() {
+ val cursor = scalarCursor(null)
+ val long = cursor.getLongOrNull("data")
+ assertNull(long)
+ }
+
+ @Test fun shortOrNullByName() {
+ val cursor = scalarCursor(null)
+ val short = cursor.getShortOrNull("data")
+ assertNull(short)
+ }
+
+ @Test fun stringOrNullByName() {
+ val cursor = scalarCursor(null)
+ val string = cursor.getStringOrNull("data")
+ assertNull(string)
+ }
+
+ private fun scalarCursor(item: Any?): Cursor = MatrixCursor(arrayOf("data")).apply {
+ addRow(arrayOf(item))
+ moveToFirst() // Prepare for consumers to read.
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/database/sqlite/SQLiteDatabaseTest.kt b/core/ktx/src/androidTest/java/androidx/core/database/sqlite/SQLiteDatabaseTest.kt
new file mode 100644
index 0000000..c8ed67d
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/database/sqlite/SQLiteDatabaseTest.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.database.sqlite
+
+import android.content.ContentValues
+import android.database.sqlite.SQLiteDatabase
+import android.database.sqlite.SQLiteOpenHelper
+import android.support.test.InstrumentationRegistry
+import androidx.testutils.assertThrows
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class SQLiteDatabaseTest {
+ private val context = InstrumentationRegistry.getContext()
+ private val openHelper = object : SQLiteOpenHelper(context, null, null, 1) {
+ override fun onCreate(db: SQLiteDatabase) {
+ db.execSQL("CREATE TABLE test(name TEXT)")
+ }
+
+ override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
+ }
+ }
+ private val db = openHelper.writableDatabase
+
+ @Test fun throwingBodyNotSuccessful() {
+ val exception = RuntimeException()
+ assertThrows<RuntimeException> {
+ db.transaction {
+ insert("test", null, ContentValues().apply { put("name", "Alice") })
+ throw exception
+ }
+ }.isSameAs(exception)
+
+ val query = db.rawQuery("SELECT COUNT(*) FROM test", emptyArray())
+ query.moveToFirst()
+ assertEquals(0L, query.getLong(0))
+ query.close()
+ }
+
+ @Test fun bodyReturnValue() {
+ val result = db.transaction {
+ "Hey"
+ }
+ assertEquals("Hey", result)
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/extensions.kt b/core/ktx/src/androidTest/java/androidx/core/extensions.kt
new file mode 100644
index 0000000..dc07142
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/extensions.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import android.util.Xml
+import androidx.annotation.LayoutRes
+import org.xmlpull.v1.XmlPullParser
+
+@SuppressLint("ResourceType")
+fun Context.getAttributeSet(@LayoutRes layoutId: Int): AttributeSet {
+ val parser = resources.getXml(layoutId)
+ var type = parser.next()
+ while (type != XmlPullParser.START_TAG) {
+ type = parser.next()
+ }
+ return Xml.asAttributeSet(parser)
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/BitmapTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/BitmapTest.kt
new file mode 100644
index 0000000..25b318b
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/BitmapTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Bitmap
+import android.graphics.ColorSpace
+import android.support.test.filters.SdkSuppress
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class BitmapTest {
+ @Test fun create() {
+ val bitmap = createBitmap(7, 9)
+ assertEquals(7, bitmap.width)
+ assertEquals(9, bitmap.height)
+ assertEquals(Bitmap.Config.ARGB_8888, bitmap.config)
+ }
+
+ @Test fun createWithConfig() {
+ val bitmap = createBitmap(7, 9, config = Bitmap.Config.RGB_565)
+ assertEquals(Bitmap.Config.RGB_565, bitmap.config)
+ }
+
+ @SdkSuppress(minSdkVersion = 26)
+ @Test fun createWithColorSpace() {
+ val colorSpace = ColorSpace.get(ColorSpace.Named.ADOBE_RGB)
+ val bitmap = createBitmap(7, 9, colorSpace = colorSpace)
+ assertEquals(colorSpace, bitmap.colorSpace)
+ }
+
+ @Test fun scale() {
+ val b = createBitmap(7, 9).scale(3, 5)
+ assertEquals(3, b.width)
+ assertEquals(5, b.height)
+ }
+
+ @Test fun applyCanvas() {
+ val p = createBitmap(2, 2).applyCanvas {
+ drawColor(0x40302010)
+ }.getPixel(1, 1)
+
+ assertEquals(0x40302010, p)
+ }
+
+ @Test fun getPixel() {
+ val b = createBitmap(2, 2).applyCanvas {
+ drawColor(0x40302010)
+ }
+ assertEquals(0x40302010, b[1, 1])
+ }
+
+ @Test fun setPixel() {
+ val b = createBitmap(2, 2)
+ b[1, 1] = 0x40302010
+ assertEquals(0x40302010, b[1, 1])
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/CanvasTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/CanvasTest.kt
new file mode 100644
index 0000000..e940aa9
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/CanvasTest.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Canvas
+import android.graphics.Matrix
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class CanvasTest {
+ private val values = FloatArray(9)
+ private val canvas = Canvas(createBitmap(1, 1))
+
+ @Suppress("DEPRECATION")
+ @Test fun withSave() {
+ val beforeCount = canvas.saveCount
+
+ canvas.matrix.getValues(values)
+ val x = values[Matrix.MTRANS_X]
+ val y = values[Matrix.MTRANS_Y]
+
+ canvas.withSave {
+ assertThat(beforeCount).isLessThan(saveCount)
+ translate(10.0f, 10.0f)
+ }
+
+ canvas.matrix.getValues(values)
+ assertEquals(x, values[Matrix.MTRANS_X])
+ assertEquals(y, values[Matrix.MTRANS_Y])
+
+ assertEquals(beforeCount, canvas.saveCount)
+ }
+
+ @Test fun withTranslation() {
+ val beforeCount = canvas.saveCount
+ canvas.withTranslation(x = 16.0f, y = 32.0f) {
+ assertThat(beforeCount).isLessThan(saveCount)
+
+ @Suppress("DEPRECATION")
+ matrix.getValues(values) // will work for a software canvas
+
+ assertEquals(16.0f, values[Matrix.MTRANS_X])
+ assertEquals(32.0f, values[Matrix.MTRANS_Y])
+ }
+ assertEquals(beforeCount, canvas.saveCount)
+ }
+
+ @Test fun withRotation() {
+ val beforeCount = canvas.saveCount
+ canvas.withRotation(degrees = 90.0f, pivotX = 16.0f, pivotY = 32.0f) {
+ assertThat(beforeCount).isLessThan(saveCount)
+
+ @Suppress("DEPRECATION")
+ matrix.getValues(values) // will work for a software canvas
+
+ assertEquals(48.0f, values[Matrix.MTRANS_X])
+ assertEquals(16.0f, values[Matrix.MTRANS_Y])
+ assertEquals(-1.0f, values[Matrix.MSKEW_X])
+ assertEquals(1.0f, values[Matrix.MSKEW_Y])
+ }
+ assertEquals(beforeCount, canvas.saveCount)
+ }
+
+ @Test fun withScale() {
+ val beforeCount = canvas.saveCount
+ canvas.withScale(x = 2.0f, y = 4.0f, pivotX = 16.0f, pivotY = 32.0f) {
+ assertThat(beforeCount).isLessThan(saveCount)
+
+ @Suppress("DEPRECATION")
+ matrix.getValues(values) // will work for a software canvas
+
+ assertEquals(-16.0f, values[Matrix.MTRANS_X])
+ assertEquals(-96.0f, values[Matrix.MTRANS_Y])
+ assertEquals(2.0f, values[Matrix.MSCALE_X])
+ assertEquals(4.0f, values[Matrix.MSCALE_Y])
+ }
+ assertEquals(beforeCount, canvas.saveCount)
+ }
+
+ @Test fun withSkew() {
+ val beforeCount = canvas.saveCount
+ canvas.withSkew(x = 2.0f, y = 4.0f) {
+ assertThat(beforeCount).isLessThan(saveCount)
+
+ @Suppress("DEPRECATION")
+ matrix.getValues(values) // will work for a software canvas
+
+ assertEquals(2.0f, values[Matrix.MSKEW_X])
+ assertEquals(4.0f, values[Matrix.MSKEW_Y])
+ }
+ assertEquals(beforeCount, canvas.saveCount)
+ }
+
+ @Suppress("DEPRECATION")
+ @Test fun withMatrix() {
+ val originMatrix = canvas.matrix
+
+ val inputMatrix = Matrix()
+ inputMatrix.postTranslate(16.0f, 32.0f)
+ inputMatrix.postRotate(90.0f, 16.0f, 32.0f)
+ inputMatrix.postScale(2.0f, 4.0f, 16.0f, 32.0f)
+
+ val beforeCount = canvas.saveCount
+ canvas.withMatrix(inputMatrix) {
+ assertThat(beforeCount).isLessThan(saveCount)
+ assertEquals(inputMatrix, matrix)
+ }
+
+ assertEquals(originMatrix, canvas.matrix)
+ assertEquals(beforeCount, canvas.saveCount)
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/ColorTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/ColorTest.kt
new file mode 100644
index 0000000..5c647d1
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/ColorTest.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Color
+import android.graphics.ColorSpace
+import android.support.test.filters.SdkSuppress
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class ColorTest {
+ @SdkSuppress(minSdkVersion = 26)
+ @Test fun destructuringColor() {
+ val (r, g, b, a) = 0x337f3010.toColor()
+ assertEquals(0.5f, r, 1e-2f)
+ assertEquals(0.19f, g, 1e-2f)
+ assertEquals(0.06f, b, 1e-2f)
+ assertEquals(0.2f, a, 1e-2f)
+ }
+
+ @Test fun destructuringInt() {
+ val (a, r, g, b) = 0x337f3010
+ assertEquals(0x33, a)
+ assertEquals(0x7f, r)
+ assertEquals(0x30, g)
+ assertEquals(0x10, b)
+ }
+
+ @SdkSuppress(minSdkVersion = 26)
+ @Test fun intToColor() = assertEquals(Color.valueOf(0x337f3010), 0x337f3010.toColor())
+
+ @SdkSuppress(minSdkVersion = 26)
+ @Test fun intToColorLong() = assertEquals(Color.pack(0x337f3010), 0x337f3010.toColorLong())
+
+ @Test fun alpha() = assertEquals(0x33, 0x337f3010.alpha)
+ @Test fun red() = assertEquals(0x7f, 0x337f3010.red)
+ @Test fun green() = assertEquals(0x30, 0x337f3010.green)
+ @Test fun blue() = assertEquals(0x10, 0x337f3010.blue)
+
+ @SdkSuppress(minSdkVersion = 26)
+ @Test fun luminance() = assertEquals(0.212f, 0xff7f7f7f.toInt().luminance, 1e-3f)
+
+ @SdkSuppress(minSdkVersion = 26)
+ @Test fun longToColor() {
+ assertEquals(Color.valueOf(0x337f3010), Color.pack(0x337f3010).toColor())
+ }
+
+ @SdkSuppress(minSdkVersion = 26)
+ @Test fun longToColorInt() = assertEquals(0x337f3010, Color.pack(0x337f3010).toColorInt())
+
+ @SdkSuppress(minSdkVersion = 26)
+ @Test fun destructuringLong() {
+ val (r, g, b, a) = Color.pack(0x337f3010)
+ assertEquals(0.20f, a, 1e-2f)
+ assertEquals(0.50f, r, 1e-2f)
+ assertEquals(0.19f, g, 1e-2f)
+ assertEquals(0.06f, b, 1e-2f)
+ }
+
+ @SdkSuppress(minSdkVersion = 26)
+ @Test fun alphaLong() = assertEquals(0.20f, Color.pack(0x337f3010).alpha, 1e-2f)
+
+ @SdkSuppress(minSdkVersion = 26)
+ @Test fun redLong() = assertEquals(0.50f, Color.pack(0x337f3010).red, 1e-2f)
+
+ @SdkSuppress(minSdkVersion = 26)
+ @Test fun greenLong() = assertEquals(0.19f, Color.pack(0x337f3010).green, 1e-2f)
+
+ @SdkSuppress(minSdkVersion = 26)
+ @Test fun blueLong() = assertEquals(0.06f, Color.pack(0x337f3010).blue, 1e-2f)
+
+ @SdkSuppress(minSdkVersion = 26)
+ @Test fun luminanceLong() {
+ assertEquals(0.212f, Color.pack(0xff7f7f7f.toInt()).luminance, 1e-3f)
+ }
+
+ @SdkSuppress(minSdkVersion = 26)
+ @Test fun isSrgb() {
+ assertTrue(0x337f3010.toColorLong().isSrgb)
+ val c = Color.pack(1.0f, 0.0f, 0.0f, 1.0f, ColorSpace.get(ColorSpace.Named.BT2020))
+ assertFalse(c.isSrgb)
+ }
+
+ @SdkSuppress(minSdkVersion = 26)
+ @Test fun isWideGamut() {
+ assertFalse(0x337f3010.toColorLong().isWideGamut)
+ val c = Color.pack(1.0f, 0.0f, 0.0f, 1.0f, ColorSpace.get(ColorSpace.Named.BT2020))
+ assertTrue(c.isWideGamut)
+ }
+
+ @SdkSuppress(minSdkVersion = 26)
+ @Test fun getColorSpace() {
+ val sRGB = ColorSpace.get(ColorSpace.Named.SRGB)
+ assertEquals(sRGB, 0x337f3010.toColorLong().colorSpace)
+
+ val bt2020 = ColorSpace.get(ColorSpace.Named.BT2020)
+ val c = Color.pack(1.0f, 0.0f, 0.0f, 1.0f, bt2020)
+ assertEquals(bt2020, c.colorSpace)
+ }
+
+ @SdkSuppress(minSdkVersion = 26)
+ @Test fun addColorsSameColorSpace() {
+ val (r, g, b, a) = 0x7f7f0000.toColor() + 0x7f007f00.toColor()
+ assertEquals(0.16f, r, 1e-2f)
+ assertEquals(0.33f, g, 1e-2f)
+ assertEquals(0.00f, b, 1e-2f)
+ assertEquals(0.75f, a, 1e-2f)
+ }
+
+ @Test fun stringToColorInt() = assertEquals(Color.GREEN, "#00ff00".toColorInt())
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/MatrixTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/MatrixTest.kt
new file mode 100644
index 0000000..61fce4f
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/MatrixTest.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Matrix
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class MatrixTest {
+ @Test fun translationMatrix() {
+ val r = translationMatrix(2.0f, 3.0f).values()
+ assertEquals(1.0f, r[Matrix.MSCALE_X])
+ assertEquals(0.0f, r[Matrix.MSKEW_X])
+ assertEquals(2.0f, r[Matrix.MTRANS_X])
+ assertEquals(0.0f, r[Matrix.MSKEW_Y])
+ assertEquals(1.0f, r[Matrix.MSCALE_Y])
+ assertEquals(3.0f, r[Matrix.MTRANS_Y])
+ }
+
+ @Test fun scaleMatrix() {
+ val r = scaleMatrix(2.0f, 3.0f).values()
+ assertEquals(2.0f, r[Matrix.MSCALE_X])
+ assertEquals(0.0f, r[Matrix.MSKEW_X])
+ assertEquals(0.0f, r[Matrix.MTRANS_X])
+ assertEquals(0.0f, r[Matrix.MSKEW_Y])
+ assertEquals(3.0f, r[Matrix.MSCALE_Y])
+ assertEquals(0.0f, r[Matrix.MTRANS_Y])
+ }
+
+ @Test fun rotationMatrix() {
+ val r = rotationMatrix(90.0f, 2.0f, 3.0f).values()
+ assertEquals(0.0f, r[Matrix.MSCALE_X])
+ assertEquals(-1.0f, r[Matrix.MSKEW_X])
+ assertEquals(5.0f, r[Matrix.MTRANS_X])
+ assertEquals(1.0f, r[Matrix.MSKEW_Y])
+ assertEquals(0.0f, r[Matrix.MSCALE_Y])
+ assertEquals(1.0f, r[Matrix.MTRANS_Y])
+ }
+
+ @Test fun multiply() {
+ val t = translationMatrix(2.0f, 3.0f)
+ val s = scaleMatrix(2.0f, 3.0f)
+ val r = (s * t).values()
+
+ assertEquals(4.0f, r[Matrix.MTRANS_X], 1e-4f)
+ assertEquals(9.0f, r[Matrix.MTRANS_Y], 1e-4f)
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/PathTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/PathTest.kt
new file mode 100644
index 0000000..36f2cba
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/PathTest.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Path
+import android.graphics.RectF
+import android.support.test.filters.SdkSuppress
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class PathTest {
+ @SdkSuppress(minSdkVersion = 26)
+ @Test fun testFlatten() {
+ val p = Path()
+
+ // Start with several moves
+ p.moveTo(5.0f, 5.0f)
+ p.moveTo(10.0f, 10.0f)
+ p.lineTo(20.0f, 20.0f)
+ p.lineTo(30.0f, 10.0f)
+ // Several moves in the middle
+ p.moveTo(40.0f, 10.0f)
+ p.moveTo(50.0f, 10.0f)
+ p.lineTo(60.0f, 20.0f)
+ // End with several moves
+ p.moveTo(10.0f, 10.0f)
+ p.moveTo(30.0f, 30.0f)
+
+ var count = 0
+ p.flatten().forEach {
+ count++
+ assertNotEquals(it.startFraction, it.endFraction)
+ }
+ assertEquals(3, count)
+ }
+
+ @SdkSuppress(minSdkVersion = 19)
+ @Test fun testUnion() {
+ val r1 = Path().apply { addRect(0.0f, 0.0f, 10.0f, 10.0f, Path.Direction.CW) }
+ val r2 = Path().apply { addRect(5.0f, 0.0f, 15.0f, 15.0f, Path.Direction.CW) }
+
+ val p = r1 + r2
+ val r = RectF()
+ p.computeBounds(r, true)
+
+ assertEquals(RectF(0.0f, 0.0f, 15.0f, 15.0f), r)
+ }
+
+ @SdkSuppress(minSdkVersion = 19)
+ @Test fun testAnd() {
+ val r1 = Path().apply { addRect(0.0f, 0.0f, 10.0f, 10.0f, Path.Direction.CW) }
+ val r2 = Path().apply { addRect(5.0f, 0.0f, 15.0f, 15.0f, Path.Direction.CW) }
+
+ val p = r1 and r2
+ val r = RectF()
+ p.computeBounds(r, true)
+
+ assertEquals(RectF(0.0f, 0.0f, 15.0f, 15.0f), r)
+ }
+
+ @SdkSuppress(minSdkVersion = 19)
+ @Test fun testDifference() {
+ val r1 = Path().apply { addRect(0.0f, 0.0f, 10.0f, 10.0f, Path.Direction.CW) }
+ val r2 = Path().apply { addRect(5.0f, 0.0f, 15.0f, 15.0f, Path.Direction.CW) }
+
+ val p = r1 - r2
+ val r = RectF()
+ p.computeBounds(r, true)
+
+ assertEquals(RectF(0.0f, 0.0f, 5.0f, 10.0f), r)
+ }
+
+ @SdkSuppress(minSdkVersion = 19)
+ @Test fun testIntersection() {
+ val r1 = Path().apply { addRect(0.0f, 0.0f, 10.0f, 10.0f, Path.Direction.CW) }
+ val r2 = Path().apply { addRect(5.0f, 0.0f, 15.0f, 15.0f, Path.Direction.CW) }
+
+ val p = r1 or r2
+ val r = RectF()
+ p.computeBounds(r, true)
+
+ assertEquals(RectF(5.0f, 0.0f, 10.0f, 10.0f), r)
+ }
+
+ @SdkSuppress(minSdkVersion = 19)
+ @Test fun testEmptyIntersection() {
+ val r1 = Path().apply { addRect(0.0f, 0.0f, 2.0f, 2.0f, Path.Direction.CW) }
+ val r2 = Path().apply { addRect(5.0f, 5.0f, 7.0f, 7.0f, Path.Direction.CW) }
+
+ val p = r1 or r2
+ assertTrue(p.isEmpty)
+ }
+
+ @SdkSuppress(minSdkVersion = 19)
+ @Test fun testXor() {
+ val r1 = Path().apply { addRect(0.0f, 0.0f, 10.0f, 10.0f, Path.Direction.CW) }
+ val r2 = Path().apply { addRect(5.0f, 5.0f, 15.0f, 15.0f, Path.Direction.CW) }
+
+ val p = r1 xor r2
+ val r = RectF()
+ p.computeBounds(r, true)
+
+ assertEquals(RectF(0.0f, 0.0f, 15.0f, 15.0f), r)
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/PictureTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/PictureTest.kt
new file mode 100644
index 0000000..6d0029f
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/PictureTest.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Color
+import android.graphics.Picture
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class PictureTest {
+ @Test fun record() {
+ val p = Picture().record(1, 1) {
+ drawColor(Color.RED)
+ }
+ val v = createBitmap(1, 1).applyCanvas {
+ drawPicture(p)
+ }.getPixel(0, 0)
+ assertEquals(0xffff0000.toInt(), v)
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/PointTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/PointTest.kt
new file mode 100644
index 0000000..791d35f
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/PointTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Point
+import android.graphics.PointF
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class PointTest {
+ @Test fun destructuringInt() {
+ val (x, y) = Point(2, 3)
+ assertEquals(2, x)
+ assertEquals(3, y)
+ }
+
+ @Test fun destructuringFloat() {
+ val (x, y) = PointF(2.0f, 3.0f)
+ assertEquals(2.0f, x)
+ assertEquals(3.0f, y)
+ }
+
+ @Test fun offsetInt() {
+ val (x, y) = Point(2, 3) + 2
+ assertEquals(4, x)
+ assertEquals(5, y)
+ }
+
+ @Test fun offsetFloat() {
+ val (x, y) = PointF(2.0f, 3.0f) + 2.0f
+ assertEquals(4.0f, x)
+ assertEquals(5.0f, y)
+ }
+
+ @Test fun offsetPoint() {
+ val (x, y) = Point(2, 3) + Point(1, 2)
+ assertEquals(3, x)
+ assertEquals(5, y)
+ }
+
+ @Test fun offsetPointF() {
+ val (x, y) = PointF(2.0f, 3.0f) + PointF(1.0f, 2.0f)
+ assertEquals(3.0f, x)
+ assertEquals(5.0f, y)
+ }
+
+ @Test fun negativeOffsetInt() {
+ val (x, y) = Point(2, 3) - 2
+ assertEquals(0, x)
+ assertEquals(1, y)
+ }
+
+ @Test fun negativeOffsetFloat() {
+ val (x, y) = PointF(2.0f, 3.0f) - 2.0f
+ assertEquals(0.0f, x)
+ assertEquals(1.0f, y)
+ }
+
+ @Test fun negativeOffsetPoint() {
+ val (x, y) = Point(2, 3) - Point(1, 2)
+ assertEquals(1, x)
+ assertEquals(1, y)
+ }
+
+ @Test fun negativeOffsetPointF() {
+ val (x, y) = PointF(2.0f, 3.0f) - PointF(1.0f, 2.0f)
+ assertEquals(1.0f, x)
+ assertEquals(1.0f, y)
+ }
+
+ @Test fun negateInt() {
+ val (x, y) = -Point(2, 3)
+ assertEquals(-2, x)
+ assertEquals(-3, y)
+ }
+
+ @Test fun negateFloat() {
+ val (x, y) = -PointF(2.0f, 3.0f)
+ assertEquals(-2.0f, x)
+ assertEquals(-3.0f, y)
+ }
+
+ @Test fun toPointF() {
+ val pointF = Point(1, 2).toPointF()
+ assertEquals(1f, pointF.x, 0f)
+ assertEquals(2f, pointF.y, 0f)
+ }
+
+ @Test fun toPoint() {
+ assertEquals(Point(1, 2), PointF(1.1f, 2.8f).toPoint())
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/PorterDuffTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/PorterDuffTest.kt
new file mode 100644
index 0000000..9b44a50
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/PorterDuffTest.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Paint
+import android.graphics.PorterDuff
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class PorterDuffTest {
+ @Test fun xfermode() {
+ val p = createBitmap(1, 1).applyCanvas {
+ val p = Paint().apply { color = 0xffffffff.toInt() }
+ drawRect(0f, 0f, 1f, 1f, p)
+
+ p.color = 0x7f00ff00
+ p.xfermode = PorterDuff.Mode.SRC.toXfermode()
+ drawRect(0f, 0f, 1f, 1f, p)
+ }.getPixel(0, 0)
+
+ assertEquals(0x7f00ff00, p)
+ }
+
+ @Test fun colorFilter() {
+ val p = createBitmap(1, 1).applyCanvas {
+ val p = Paint().apply { color = 0xffffffff.toInt() }
+ drawRect(0f, 0f, 1f, 1f, p)
+
+ p.color = 0xff000000.toInt()
+ p.colorFilter = PorterDuff.Mode.SRC.toColorFilter(0xff00ff00.toInt())
+ drawRect(0f, 0f, 1f, 1f, p)
+ }.getPixel(0, 0)
+
+ assertEquals(0xff00ff00.toInt(), p)
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/RectTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/RectTest.kt
new file mode 100644
index 0000000..8b2613f
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/RectTest.kt
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Matrix
+import android.graphics.Point
+import android.graphics.PointF
+import android.graphics.Rect
+import android.graphics.RectF
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class RectTest {
+ @Test fun destructuringInt() {
+ val (l, t, r, b) = Rect(4, 8, 16, 24)
+ assertEquals(4, l)
+ assertEquals(8, t)
+ assertEquals(16, r)
+ assertEquals(24, b)
+ }
+
+ @Test fun destructuringFloat() {
+ val (l, t, r, b) = RectF(4.0f, 8.0f, 16.0f, 24.0f)
+ assertEquals(4.0f, l)
+ assertEquals(8.0f, t)
+ assertEquals(16.0f, r)
+ assertEquals(24.0f, b)
+ }
+
+ @Test fun unionInt() {
+ val (l, t, r, b) = Rect(0, 0, 4, 4) + Rect(-1, -1, 6, 6)
+ assertEquals(-1, l)
+ assertEquals(-1, t)
+ assertEquals(6, r)
+ assertEquals(6, b)
+ }
+
+ @Test fun unionAsAndInt() {
+ val (l, t, r, b) = Rect(0, 0, 4, 4) and Rect(-1, -1, 6, 6)
+ assertEquals(-1, l)
+ assertEquals(-1, t)
+ assertEquals(6, r)
+ assertEquals(6, b)
+ }
+
+ @Test fun unionFloat() {
+ val (l, t, r, b) = RectF(0.0f, 0.0f, 4.0f, 4.0f) + RectF(-1.0f, -1.0f, 6.0f, 6.0f)
+ assertEquals(-1.0f, l)
+ assertEquals(-1.0f, t)
+ assertEquals(6.0f, r)
+ assertEquals(6.0f, b)
+ }
+
+ @Test fun unionAsAndFloat() {
+ val (l, t, r, b) = RectF(0.0f, 0.0f, 4.0f, 4.0f) and RectF(-1.0f, -1.0f, 6.0f, 6.0f)
+ assertEquals(-1.0f, l)
+ assertEquals(-1.0f, t)
+ assertEquals(6.0f, r)
+ assertEquals(6.0f, b)
+ }
+
+ @Test fun differenceInt() {
+ val r = Rect(0, 0, 4, 4) - Rect(-1, 2, 6, 6)
+ assertEquals(Rect(0, 0, 4, 2), r.bounds)
+ }
+
+ @Test fun differenceFloat() {
+ val r = RectF(0.0f, 0.0f, 4.0f, 4.0f) - RectF(-1.0f, 2.0f, 6.0f, 6.0f)
+ assertEquals(Rect(0, 0, 4, 2), r.bounds)
+ }
+
+ @Test fun intersectionAsOrInt() {
+ val (l, t, r, b) = Rect(0, 0, 4, 4) or Rect(2, 2, 6, 6)
+ assertEquals(2, l)
+ assertEquals(2, t)
+ assertEquals(4, r)
+ assertEquals(4, b)
+ }
+
+ @Test fun intersectionAsOrFloat() {
+ val (l, t, r, b) = RectF(0.0f, 0.0f, 4.0f, 4.0f) or RectF(2.0f, 2.0f, 6.0f, 6.0f)
+ assertEquals(2.0f, l)
+ assertEquals(2.0f, t)
+ assertEquals(4.0f, r)
+ assertEquals(4.0f, b)
+ }
+
+ @Test fun xorInt() {
+ val r = Rect(0, 0, 4, 4) xor Rect(2, 2, 6, 6)
+ assertEquals(Rect(0, 0, 4, 4) and Rect(2, 2, 6, 6), r.bounds)
+ assertFalse(r.contains(3, 3))
+ }
+
+ @Test fun xorFloat() {
+ val r = RectF(0.0f, 0.0f, 4.0f, 4.0f) xor RectF(2.0f, 2.0f, 6.0f, 6.0f)
+ assertEquals(Rect(0, 0, 4, 4) and Rect(2, 2, 6, 6), r.bounds)
+ assertFalse(r.contains(3, 3))
+ }
+
+ @Test fun offsetInt() {
+ val (l, t, r, b) = Rect(0, 0, 2, 2) + 2
+ assertEquals(l, 2)
+ assertEquals(t, 2)
+ assertEquals(r, 4)
+ assertEquals(b, 4)
+ }
+
+ @Test fun offsetPoint() {
+ val (l, t, r, b) = Rect(0, 0, 2, 2) + Point(1, 2)
+ assertEquals(l, 1)
+ assertEquals(t, 2)
+ assertEquals(r, 3)
+ assertEquals(b, 4)
+ }
+
+ @Test fun offsetFloat() {
+ val (l, t, r, b) = RectF(0.0f, 0.0f, 2.0f, 2.0f) + 2.0f
+ assertEquals(l, 2.0f)
+ assertEquals(t, 2.0f)
+ assertEquals(r, 4.0f)
+ assertEquals(b, 4.0f)
+ }
+
+ @Test fun offsetPointF() {
+ val (l, t, r, b) = RectF(0.0f, 0.0f, 2.0f, 2.0f) + PointF(1.0f, 2.0f)
+ assertEquals(l, 1.0f)
+ assertEquals(t, 2.0f)
+ assertEquals(r, 3.0f)
+ assertEquals(b, 4.0f)
+ }
+
+ @Test fun negativeOffsetInt() {
+ val (l, t, r, b) = Rect(0, 0, 2, 2) - 2
+ assertEquals(l, -2)
+ assertEquals(t, -2)
+ assertEquals(r, 0)
+ assertEquals(b, 0)
+ }
+
+ @Test fun negativeOffsetPoint() {
+ val (l, t, r, b) = Rect(0, 0, 2, 2) - Point(1, 2)
+ assertEquals(l, -1)
+ assertEquals(t, -2)
+ assertEquals(r, 1)
+ assertEquals(b, 0)
+ }
+
+ @Test fun negativeOffsetFloat() {
+ val (l, t, r, b) = RectF(0.0f, 0.0f, 2.0f, 2.0f) - 2.0f
+ assertEquals(l, -2.0f)
+ assertEquals(t, -2.0f)
+ assertEquals(r, 0.0f)
+ assertEquals(b, 0.0f)
+ }
+
+ @Test fun negativeOffsetPointF() {
+ val (l, t, r, b) = RectF(0.0f, 0.0f, 2.0f, 2.0f) - PointF(1.0f, 2.0f)
+ assertEquals(l, -1.0f)
+ assertEquals(t, -2.0f)
+ assertEquals(r, 1.0f)
+ assertEquals(b, 0.0f)
+ }
+
+ @Test fun pointInside() {
+ assertTrue(Point(1, 1) in Rect(0, 0, 2, 2))
+ assertTrue(PointF(1.0f, 1.0f) in RectF(0.0f, 0.0f, 2.0f, 2.0f))
+ }
+
+ @Test fun toRect() {
+ assertEquals(
+ Rect(0, 1, 2, 3),
+ RectF(0f, 1f, 2f, 3f).toRect())
+
+ assertEquals(
+ Rect(0, 1, 2, 3),
+ RectF(0.1f, 1.1f, 1.9f, 2.9f).toRect())
+ }
+
+ @Test fun toRectF() {
+ val rectF = Rect(0, 1, 2, 3).toRectF()
+ assertEquals(0f, rectF.left, 0f)
+ assertEquals(1f, rectF.top, 0f)
+ assertEquals(2f, rectF.right, 0f)
+ assertEquals(3f, rectF.bottom, 0f)
+ }
+
+ @Test fun toRegionInt() {
+ assertEquals(Rect(1, 1, 5, 5), Rect(1, 1, 5, 5).toRegion().bounds)
+ }
+
+ @Test fun toRegionFloat() {
+ assertEquals(Rect(1, 1, 5, 5), RectF(1.1f, 1.1f, 4.8f, 4.8f).toRegion().bounds)
+ }
+
+ @Test fun transformRectToRect() {
+ val m = Matrix()
+ m.setScale(2.0f, 2.0f)
+
+ val r = RectF(2.0f, 2.0f, 5.0f, 7.0f).transform(m)
+ assertEquals(4f, r.left, 0f)
+ assertEquals(4f, r.top, 0f)
+ assertEquals(10f, r.right, 0f)
+ assertEquals(14f, r.bottom, 0f)
+ }
+
+ @Test fun transformRectNotPreserved() {
+ val m = Matrix()
+ m.setRotate(45.0f)
+
+ val (l, t, r, b) = RectF(-1.0f, -1.0f, 1.0f, 1.0f).transform(m)
+ assertEquals(-1.414f, l, 1e-3f)
+ assertEquals(-1.414f, t, 1e-3f)
+ assertEquals( 1.414f, r, 1e-3f)
+ assertEquals( 1.414f, b, 1e-3f)
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/RegionTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/RegionTest.kt
new file mode 100644
index 0000000..21e6443
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/RegionTest.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Point
+import android.graphics.Rect
+import android.graphics.Region
+import androidx.testutils.assertThrows
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class RegionTest {
+ @Test fun containsPoint() {
+ assertFalse(Point(1, 1) in Region())
+ assertTrue(Point(1, 1) in Region(0, 0, 2, 2))
+ }
+
+ @Test fun unionRect() {
+ val r = Region(0, 0, 2, 2) + Rect(4, 4, 6, 6)
+ assertFalse(Point(3, 3) in r)
+ assertTrue(Point(1, 1) in r)
+ assertTrue(Point(5, 5) in r)
+ }
+
+ @Test fun unionRegion() {
+ val r = Region(0, 0, 2, 2) + Region(4, 4, 6, 6)
+ assertFalse(Point(3, 3) in r)
+ assertTrue(Point(1, 1) in r)
+ assertTrue(Point(5, 5) in r)
+ }
+
+ @Test fun unionAsAndRect() {
+ val r = Region(0, 0, 2, 2) and Rect(4, 4, 6, 6)
+ assertFalse(Point(3, 3) in r)
+ assertTrue(Point(1, 1) in r)
+ assertTrue(Point(5, 5) in r)
+ }
+
+ @Test fun unionAsAndRegion() {
+ val r = Region(0, 0, 2, 2) and Region(4, 4, 6, 6)
+ assertFalse(Point(3, 3) in r)
+ assertTrue(Point(1, 1) in r)
+ assertTrue(Point(5, 5) in r)
+ }
+
+ @Test fun differenceRect() {
+ val r = Region(0, 0, 4, 4) - Rect(2, 2, 6, 6)
+ assertFalse(Point(3, 3) in r)
+ assertTrue(Point(1, 1) in r)
+ assertFalse(Point(5, 5) in r)
+ }
+
+ @Test fun differenceRegion() {
+ val r = Region(0, 0, 4, 4) - Region(2, 2, 6, 6)
+ assertFalse(Point(3, 3) in r)
+ assertTrue(Point(1, 1) in r)
+ assertFalse(Point(5, 5) in r)
+ }
+
+ @Test fun unaryMinus() {
+ val r = Rect(0, 0, 10, 10) - Rect(4, 4, 6, 6)
+ assertTrue(Point(1, 1) in r)
+ assertFalse(Point(5, 5) in r)
+
+ val i = -r
+ assertFalse(Point(1, 1) in i)
+ assertTrue(Point(5, 5) in i)
+ }
+
+ @Test fun not() {
+ val r = Rect(0, 0, 10, 10) - Rect(4, 4, 6, 6)
+ assertTrue(Point(1, 1) in r)
+ assertFalse(Point(5, 5) in r)
+
+ val i = !r
+ assertFalse(Point(1, 1) in i)
+ assertTrue(Point(5, 5) in i)
+ }
+
+ @Test fun orRect() {
+ val r = Region(0, 0, 4, 4) or Rect(2, 2, 6, 6)
+ assertFalse(Point(1, 1) in r)
+ assertTrue(Point(3, 3) in r)
+ }
+
+ @Test fun orRegion() {
+ val r = Region(0, 0, 4, 4) or Region(2, 2, 6, 6)
+ assertFalse(Point(1, 1) in r)
+ assertTrue(Point(3, 3) in r)
+ }
+
+ @Test fun xorRect() {
+ val r = Region(0, 0, 4, 4) xor Rect(2, 2, 6, 6)
+ assertFalse(Point(3, 3) in r)
+ assertTrue(Point(1, 1) in r)
+ }
+
+ @Test fun xorRegion() {
+ val r = Region(0, 0, 4, 4) xor Region(2, 2, 6, 6)
+ assertFalse(Point(3, 3) in r)
+ assertTrue(Point(1, 1) in r)
+ }
+
+ @Test fun iteratorForLoop() {
+ val region = Region(0, 0, 4, 4) -
+ Rect(2, 2, 6, 6)
+ var count = 0
+ var r = Rect()
+ for (rect in region) {
+ count++
+ assertNotSame(r, rect)
+ r = rect
+ }
+ assertEquals(2, count)
+ }
+
+ @Test fun iteratorOutOfBounds() {
+ val region = Region(0, 0, 4, 4) -
+ Rect(2, 2, 6, 6)
+ val it = region.iterator()
+ it.next()
+ it.next()
+ assertThrows<IndexOutOfBoundsException> {
+ it.next()
+ }
+ }
+
+ @Test fun iteratorForEach() {
+ val region = Region(0, 0, 4, 4) -
+ Rect(2, 2, 6, 6)
+ var count = 0
+ var r = Rect()
+ region.forEach {
+ count++
+ assertNotSame(r, it)
+ r = it
+ }
+ assertEquals(2, count)
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/ShaderTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/ShaderTest.kt
new file mode 100644
index 0000000..ad2b63c
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/ShaderTest.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Matrix
+import android.graphics.Shader
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class ShaderTest {
+ @Test
+ fun testTransform() {
+ @Suppress("DEPRECATION")
+ val shader = Shader()
+ val values = FloatArray(9)
+ val matrix = Matrix()
+
+ shader.transform {
+ setTranslate(10f, 30f)
+ }
+
+ // Now read matrix from Shader
+ shader.getLocalMatrix(matrix)
+ matrix.getValues(values)
+
+ // Assert that the values are as expected
+ assertEquals(10f, values[Matrix.MTRANS_X])
+ assertEquals(30f, values[Matrix.MTRANS_Y])
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/drawable/BitmapDrawableTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/drawable/BitmapDrawableTest.kt
new file mode 100644
index 0000000..4c4ceb6
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/drawable/BitmapDrawableTest.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics.drawable
+
+import android.support.test.InstrumentationRegistry
+import androidx.core.graphics.createBitmap
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class BitmapDrawableTest {
+ private val context = InstrumentationRegistry.getContext()
+
+ @Test fun fromBitmap() {
+ val b = createBitmap(1, 1)
+ val drawable = b.toDrawable(context.resources)
+ assertEquals(b, drawable.bitmap)
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/drawable/ColorDrawableTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/drawable/ColorDrawableTest.kt
new file mode 100644
index 0000000..ad7a39b
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/drawable/ColorDrawableTest.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics.drawable
+
+import android.graphics.Color
+import android.support.test.filters.SdkSuppress
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class ColorDrawableTest {
+ @Test fun fromInt() {
+ val drawable = Color.CYAN.toDrawable()
+ assertEquals(Color.CYAN, drawable.color)
+ }
+
+ @SdkSuppress(minSdkVersion = 26)
+ @Test fun fromColor() {
+ val drawable = Color.valueOf(Color.CYAN).toDrawable()
+ assertEquals(Color.CYAN, drawable.color)
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/drawable/DrawableTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/drawable/DrawableTest.kt
new file mode 100644
index 0000000..6455d0a
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/drawable/DrawableTest.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics.drawable
+
+import android.graphics.Bitmap.Config
+import android.graphics.Color
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.ColorDrawable
+import android.support.test.InstrumentationRegistry
+import androidx.core.graphics.createBitmap
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class DrawableTest {
+ private val context = InstrumentationRegistry.getContext()
+ private val resources = context.resources
+
+ @Test fun bitmapDrawableNoSizeNoConfig() {
+ val original = createBitmap(10, 10).apply {
+ eraseColor(Color.RED)
+ }
+ val drawable = BitmapDrawable(resources, original)
+
+ val bitmap = drawable.toBitmap()
+ assertEquals(10, bitmap.width)
+ assertEquals(10, bitmap.height)
+ assertEquals(Config.ARGB_8888, bitmap.config)
+ assertEquals(Color.RED, bitmap.getPixel(5, 5))
+ }
+
+ @Test fun bitmapDrawableNoSizeRetainedConfig() {
+ val original = createBitmap(10, 10).apply {
+ eraseColor(Color.RED)
+ }
+ val drawable = BitmapDrawable(resources, original)
+
+ val bitmap = drawable.toBitmap(config = Config.ARGB_8888)
+ assertEquals(10, bitmap.width)
+ assertEquals(10, bitmap.height)
+ assertEquals(Config.ARGB_8888, bitmap.config)
+ assertEquals(Color.RED, bitmap.getPixel(5, 5))
+ }
+
+ @Test fun bitmapDrawableNoSizeDifferentConfig() {
+ val original = createBitmap(10, 10).apply {
+ eraseColor(Color.RED)
+ }
+ val drawable = BitmapDrawable(resources, original)
+
+ val bitmap = drawable.toBitmap(config = Config.ARGB_8888)
+ assertEquals(10, bitmap.width)
+ assertEquals(10, bitmap.height)
+ assertEquals(Config.ARGB_8888, bitmap.config)
+ assertEquals(Color.RED, bitmap.getPixel(5, 5))
+ }
+
+ @Test fun bitmapDrawableDifferentSizeNoConfig() {
+ val original = createBitmap(10, 10).apply {
+ eraseColor(Color.RED)
+ }
+ val drawable = BitmapDrawable(resources, original)
+
+ val bitmap = drawable.toBitmap(20, 20)
+ assertEquals(20, bitmap.width)
+ assertEquals(20, bitmap.height)
+ assertEquals(Config.ARGB_8888, bitmap.config)
+ assertEquals(Color.RED, bitmap.getPixel(10, 10))
+ }
+
+ @Test fun bitmapDrawableDifferentSizeDifferentConfig() {
+ val original = createBitmap(10, 10).apply {
+ eraseColor(Color.RED)
+ }
+ val drawable = BitmapDrawable(resources, original)
+
+ val bitmap = drawable.toBitmap(20, 20, Config.ARGB_8888)
+ assertEquals(20, bitmap.width)
+ assertEquals(20, bitmap.height)
+ assertEquals(Config.ARGB_8888, bitmap.config)
+ assertEquals(Color.RED, bitmap.getPixel(10, 10))
+ }
+
+ @Test fun drawableNoConfig() {
+ val drawable = object : ColorDrawable(Color.RED) {
+ override fun getIntrinsicWidth() = 10
+ override fun getIntrinsicHeight() = 10
+ }
+
+ val bitmap = drawable.toBitmap()
+ assertEquals(10, bitmap.width)
+ assertEquals(10, bitmap.height)
+ assertEquals(Config.ARGB_8888, bitmap.config)
+ assertEquals(Color.RED, bitmap.getPixel(5, 5))
+ }
+
+ @Test fun drawableConfig() {
+ val drawable = object : ColorDrawable(Color.RED) {
+ override fun getIntrinsicWidth() = 10
+ override fun getIntrinsicHeight() = 10
+ }
+
+ val bitmap = drawable.toBitmap(config = Config.RGB_565)
+ assertEquals(10, bitmap.width)
+ assertEquals(10, bitmap.height)
+ assertEquals(Config.RGB_565, bitmap.config)
+ assertEquals(Color.RED, bitmap.getPixel(5, 5))
+ }
+
+ @Test fun drawableSize() {
+ val drawable = object : ColorDrawable(Color.RED) {
+ override fun getIntrinsicWidth() = 10
+ override fun getIntrinsicHeight() = 10
+ }
+
+ val bitmap = drawable.toBitmap(20, 20)
+ assertEquals(20, bitmap.width)
+ assertEquals(20, bitmap.height)
+ assertEquals(Config.ARGB_8888, bitmap.config)
+ assertEquals(Color.RED, bitmap.getPixel(10, 10))
+ }
+
+ @Test fun oldBoundsRestored() {
+ val drawable = object : ColorDrawable(Color.RED) {
+ override fun getIntrinsicWidth() = 10
+ override fun getIntrinsicHeight() = 10
+ }
+ drawable.setBounds(2, 2, 8, 8)
+
+ val bitmap = drawable.toBitmap()
+ assertEquals(10, bitmap.width)
+ assertEquals(10, bitmap.height)
+
+ assertEquals(2, drawable.bounds.left)
+ assertEquals(2, drawable.bounds.top)
+ assertEquals(8, drawable.bounds.right)
+ assertEquals(8, drawable.bounds.bottom)
+ }
+
+ @Test fun updateBoundsTest() {
+ val drawable = object : ColorDrawable(Color.RED) {
+ override fun getIntrinsicWidth() = 10
+ override fun getIntrinsicHeight() = 10
+ }
+ drawable.setBounds(0, 0, 10, 10)
+
+ drawable.updateBounds(1, 2, 3, 4)
+
+ assertEquals(1, drawable.bounds.left)
+ assertEquals(2, drawable.bounds.top)
+ assertEquals(3, drawable.bounds.right)
+ assertEquals(4, drawable.bounds.bottom)
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/graphics/drawable/IconTest.kt b/core/ktx/src/androidTest/java/androidx/core/graphics/drawable/IconTest.kt
new file mode 100644
index 0000000..e2dd84c
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/graphics/drawable/IconTest.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics.drawable
+
+import android.graphics.Bitmap
+import android.graphics.Bitmap.Config.ARGB_8888
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.drawable.Icon
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SdkSuppress
+import androidx.core.graphics.createBitmap
+import androidx.core.net.toUri
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import java.io.File
+
+@SdkSuppress(minSdkVersion = 26)
+class IconTest {
+ private val context = InstrumentationRegistry.getContext()
+
+ @Test fun fromBitmapAdaptive() {
+ val density = context.resources.displayMetrics.density
+
+ val edge = (108.0f * density + 0.5f).toInt()
+ val bitmap = Bitmap.createBitmap(edge, edge, ARGB_8888).apply {
+ eraseColor(Color.RED)
+ }
+ val icon = bitmap.toAdaptiveIcon()
+
+ val rendered = icon.toIntrinsicBitmap()
+ val masked = (72.0f * density + 0.5f).toInt()
+ assertEquals(masked, rendered.width)
+ assertEquals(masked, rendered.height)
+ // Grab a pixel from the middle to ensure we are not being masked.
+ assertEquals(Color.RED, rendered.getPixel(masked / 2, masked / 2))
+ }
+
+ @Test fun fromBitmap() {
+ val bitmap = createBitmap(1, 1).apply {
+ eraseColor(Color.RED)
+ }
+ val icon = bitmap.toIcon()
+
+ val rendered = icon.toIntrinsicBitmap()
+ assertEquals(1, rendered.width)
+ assertEquals(1, rendered.height)
+ assertEquals(Color.RED, rendered.getPixel(0, 0))
+ }
+
+ @Test fun fromUri() {
+ // Icon can't read from file:///android_asset/red.png so copy to a real file.
+ val cacheFile = File(context.cacheDir, "red.png")
+ context.assets.open("red.png").use { cacheFile.writeBytes(it.readBytes()) }
+
+ val uri = cacheFile.toUri()
+ val icon = uri.toIcon()
+
+ val rendered = icon.toIntrinsicBitmap()
+ assertEquals(1, rendered.width)
+ assertEquals(1, rendered.height)
+ assertEquals(Color.RED, rendered.getPixel(0, 0))
+ }
+
+ @Test fun fromByteArray() {
+ val bytes = context.assets.open("red.png").use { it.readBytes() }
+ val icon = bytes.toIcon()
+
+ val rendered = icon.toIntrinsicBitmap()
+ assertEquals(1, rendered.width)
+ assertEquals(1, rendered.height)
+ assertEquals(Color.RED, rendered.getPixel(0, 0))
+ }
+
+ private fun Icon.toIntrinsicBitmap(): Bitmap {
+ val drawable = loadDrawable(context)
+ val bitmap = createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight)
+ drawable.setBounds(0, 0, drawable.intrinsicHeight, drawable.intrinsicHeight)
+ drawable.draw(Canvas(bitmap))
+ return bitmap
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/location/LocationTest.kt b/core/ktx/src/androidTest/java/androidx/core/location/LocationTest.kt
new file mode 100644
index 0000000..0f289ef
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/location/LocationTest.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.location
+
+import android.location.Location
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class LocationTest {
+ @Test fun destructuring() {
+ val (lat, lon) = Location("").apply {
+ latitude = 1.0
+ longitude = 2.0
+ }
+ assertEquals(lat, 1.0, 0.0)
+ assertEquals(lon, 2.0, 0.0)
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/net/UriTest.kt b/core/ktx/src/androidTest/java/androidx/core/net/UriTest.kt
new file mode 100644
index 0000000..d25de35
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/net/UriTest.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.net
+
+import android.net.Uri
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import java.io.File
+
+class UriTest {
+ @Test fun uriFromString() {
+ val string = "https://test.example.com/foo?bar#baz"
+ assertEquals(Uri.parse(string), string.toUri())
+ }
+
+ @Test fun uriFromFile() {
+ val file = File("/path/to/my/file")
+ assertEquals(Uri.fromFile(file), file.toUri())
+ }
+
+ @Test fun fileFromUri() {
+ val uri = Uri.parse("path/to/my/file")
+ assertEquals(File(uri.path), uri.toFile())
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/os/BundleTest.kt b/core/ktx/src/androidTest/java/androidx/core/os/BundleTest.kt
new file mode 100644
index 0000000..924e9ad
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/os/BundleTest.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.os
+
+import android.graphics.Rect
+import android.os.Binder
+import android.os.Bundle
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SdkSuppress
+import android.util.Size
+import android.util.SizeF
+import android.view.View
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Test
+import java.util.concurrent.atomic.AtomicInteger
+
+class BundleTest {
+ @Test fun bundleOfValid() {
+ val bundleValue = Bundle()
+ val charSequenceValue = "hey"
+ val parcelableValue = Rect(1, 2, 3, 4)
+ val serializableValue = AtomicInteger(1)
+
+ val bundle = bundleOf(
+ "null" to null,
+
+ "boolean" to true,
+ "byte" to 1.toByte(),
+ "char" to 'a',
+ "double" to 1.0,
+ "float" to 1f,
+ "int" to 1,
+ "long" to 1L,
+ "short" to 1.toShort(),
+
+ "bundle" to bundleValue,
+ "charSequence" to charSequenceValue,
+ "parcelable" to parcelableValue,
+
+ "booleanArray" to booleanArrayOf(),
+ "byteArray" to byteArrayOf(),
+ "charArray" to charArrayOf(),
+ "doubleArray" to doubleArrayOf(),
+ "floatArray" to floatArrayOf(),
+ "intArray" to intArrayOf(),
+ "longArray" to longArrayOf(),
+ "shortArray" to shortArrayOf(),
+
+ "parcelableArray" to arrayOf(parcelableValue),
+ "stringArray" to arrayOf("hey"),
+ "charSequenceArray" to arrayOf<CharSequence>("hey"),
+ "serializableArray" to arrayOf(serializableValue),
+
+ "serializable" to serializableValue
+ )
+
+ assertEquals(25, bundle.size())
+
+ assertNull(bundle["null"])
+
+ assertEquals(true, bundle["boolean"])
+ assertEquals(1.toByte(), bundle["byte"])
+ assertEquals('a', bundle["char"])
+ assertEquals(1.0, bundle["double"])
+ assertEquals(1f, bundle["float"])
+ assertEquals(1, bundle["int"])
+ assertEquals(1L, bundle["long"])
+ assertEquals(1.toShort(), bundle["short"])
+
+ assertSame(bundleValue, bundle["bundle"])
+ assertSame(charSequenceValue, bundle["charSequence"])
+ assertSame(parcelableValue, bundle["parcelable"])
+
+ assertArrayEquals(booleanArrayOf(), bundle["booleanArray"] as BooleanArray)
+ assertArrayEquals(byteArrayOf(), bundle["byteArray"] as ByteArray)
+ assertArrayEquals(charArrayOf(), bundle["charArray"] as CharArray)
+ assertArrayEquals(doubleArrayOf(), bundle["doubleArray"] as DoubleArray, 0.0)
+ assertArrayEquals(floatArrayOf(), bundle["floatArray"] as FloatArray, 0f)
+ assertArrayEquals(intArrayOf(), bundle["intArray"] as IntArray)
+ assertArrayEquals(longArrayOf(), bundle["longArray"] as LongArray)
+ assertArrayEquals(shortArrayOf(), bundle["shortArray"] as ShortArray)
+
+ assertThat(bundle["parcelableArray"] as Array<*>).asList().containsExactly(parcelableValue)
+ assertThat(bundle["stringArray"] as Array<*>).asList().containsExactly("hey")
+ assertThat(bundle["charSequenceArray"] as Array<*>).asList().containsExactly("hey")
+ assertThat(bundle["serializableArray"] as Array<*>).asList()
+ .containsExactly(serializableValue)
+
+ assertSame(serializableValue, bundle["serializable"])
+ }
+
+ @SdkSuppress(minSdkVersion = 18)
+ @Test fun bundleOfValidApi18() {
+ val binderValue = Binder()
+ val bundle = bundleOf("binder" to binderValue)
+ assertSame(binderValue, bundle["binder"])
+ }
+
+ @SdkSuppress(minSdkVersion = 21)
+ @Test fun bundleOfValidApi21() {
+ val sizeValue = Size(1, 1)
+ val sizeFValue = SizeF(1f, 1f)
+
+ val bundle = bundleOf(
+ "size" to sizeValue,
+ "sizeF" to sizeFValue
+ )
+
+ assertSame(sizeValue, bundle["size"])
+ assertSame(sizeFValue, bundle["sizeF"])
+ }
+
+ @Test fun bundleOfInvalid() {
+ assertThrows<IllegalArgumentException> {
+ bundleOf("nope" to View(InstrumentationRegistry.getContext()))
+ }.hasMessageThat().isEqualTo("Illegal value type android.view.View for key \"nope\"")
+
+ assertThrows<IllegalArgumentException> {
+ bundleOf("nopes" to arrayOf(View(InstrumentationRegistry.getContext())))
+ }.hasMessageThat().isEqualTo("Illegal value array type android.view.View for key \"nopes\"")
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/os/HandlerTest.kt b/core/ktx/src/androidTest/java/androidx/core/os/HandlerTest.kt
new file mode 100644
index 0000000..e0fdf4e
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/os/HandlerTest.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.os
+
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.SystemClock
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeUnit.MILLISECONDS
+import java.util.concurrent.TimeUnit.SECONDS
+
+class HandlerTest {
+ private val handlerThread = HandlerThread("handler-test")
+ private lateinit var handler: Handler
+
+ @Before fun before() {
+ handlerThread.start()
+ handler = Handler(handlerThread.looper)
+ }
+
+ @After fun after() {
+ handlerThread.quit()
+ }
+
+ @Test fun postDelayedLambdaMillis() {
+ var called = 0
+ handler.postDelayed(10) {
+ called++
+ }
+
+ handler.await(20, MILLISECONDS)
+ assertEquals(1, called)
+ }
+
+ @Test fun postDelayedLambdaMillisRemoved() {
+ var called = 0
+ val runnable = handler.postDelayed(10) {
+ called++
+ }
+ handler.removeCallbacks(runnable)
+
+ handler.await(20, MILLISECONDS)
+ assertEquals(0, called)
+ }
+
+ @Test fun postAtTimeLambda() {
+ var called = 0
+ handler.postAtTime(SystemClock.uptimeMillis() + 10) {
+ called++
+ }
+
+ handler.await(20, MILLISECONDS)
+ assertEquals(1, called)
+ }
+
+ @Test fun postAtTimeLambdaRemoved() {
+ var called = 0
+ val runnable = handler.postAtTime(SystemClock.uptimeMillis() + 10) {
+ called++
+ }
+ handler.removeCallbacks(runnable)
+
+ handler.await(20, MILLISECONDS)
+ assertEquals(0, called)
+ }
+
+ @Test fun postAtTimeLambdaWithTokenRuns() {
+ val token = Any()
+ var called = 0
+ handler.postAtTime(SystemClock.uptimeMillis() + 10, token) {
+ called++
+ }
+
+ handler.await(20, MILLISECONDS)
+ assertEquals(1, called)
+ }
+
+ @Test fun postAtTimeLambdaWithTokenCancelWithToken() {
+ // This test uses the token to cancel the runnable as it's the only way we have to verify
+ // that the Runnable was actually posted with the token.
+
+ val token = Any()
+ var called = 0
+ handler.postAtTime(SystemClock.uptimeMillis() + 10, token) {
+ called++
+ }
+ handler.removeCallbacksAndMessages(token)
+
+ handler.await(20, MILLISECONDS)
+ assertEquals(0, called)
+ }
+
+ private fun Handler.await(amount: Long, unit: TimeUnit) {
+ val latch = CountDownLatch(1)
+ postDelayed(latch::countDown, unit.toMillis(amount))
+
+ // Wait up to 1s longer than desired to account for time skew.
+ val wait = unit.toMillis(amount) + SECONDS.toMillis(1)
+ assertTrue(latch.await(wait, MILLISECONDS))
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/os/PersistableBundleTest.kt b/core/ktx/src/androidTest/java/androidx/core/os/PersistableBundleTest.kt
new file mode 100644
index 0000000..8683155
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/os/PersistableBundleTest.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.os
+
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SdkSuppress
+import android.view.View
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+@SdkSuppress(minSdkVersion = 21)
+class PersistableBundleTest {
+ @Test fun persistableBundleOfValid() {
+ val bundle = persistableBundleOf(
+ "null" to null,
+
+ "double" to 1.0,
+ "int" to 1,
+ "long" to 1L,
+
+ "string" to "hey",
+
+ "doubleArray" to doubleArrayOf(),
+ "intArray" to intArrayOf(),
+ "longArray" to longArrayOf(),
+
+ "stringArray" to arrayOf("hey")
+ )
+
+ assertEquals(9, bundle.size())
+
+ assertNull(bundle["null"])
+
+ assertEquals(1.0, bundle["double"])
+ assertEquals(1, bundle["int"])
+ assertEquals(1L, bundle["long"])
+
+ assertEquals("hey", bundle["string"])
+
+ assertArrayEquals(doubleArrayOf(), bundle["doubleArray"] as DoubleArray, 0.0)
+ assertArrayEquals(intArrayOf(), bundle["intArray"] as IntArray)
+ assertArrayEquals(longArrayOf(), bundle["longArray"] as LongArray)
+
+ assertThat(bundle["stringArray"] as Array<*>).asList().containsExactly("hey")
+ }
+
+ @SdkSuppress(minSdkVersion = 22)
+ @Test fun persistableBundleOfValidApi22() {
+ val bundle = persistableBundleOf(
+ "boolean" to true,
+ "booleanArray" to booleanArrayOf()
+ )
+
+ assertEquals(true, bundle["boolean"])
+ assertArrayEquals(booleanArrayOf(), bundle["booleanArray"] as BooleanArray)
+ }
+
+ @Test fun persistableBundleOfInvalid() {
+ assertThrows<IllegalArgumentException> {
+ persistableBundleOf("nope" to View(InstrumentationRegistry.getContext()))
+ }.hasMessageThat().isEqualTo("Illegal value type android.view.View for key \"nope\"")
+
+ assertThrows<IllegalArgumentException> {
+ persistableBundleOf("nopes" to arrayOf(View(InstrumentationRegistry.getContext())))
+ }.hasMessageThat().isEqualTo("Illegal value array type android.view.View for key \"nopes\"")
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/preference/PreferenceGroupTest.kt b/core/ktx/src/androidTest/java/androidx/core/preference/PreferenceGroupTest.kt
new file mode 100644
index 0000000..120718e
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/preference/PreferenceGroupTest.kt
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.preference
+
+import android.preference.Preference
+import android.preference.PreferenceFragment
+import android.preference.PreferenceGroup
+import android.support.test.InstrumentationRegistry
+import android.support.test.rule.ActivityTestRule
+import androidx.core.TestPreferenceActivity
+import androidx.testutils.assertThrows
+import androidx.testutils.fail
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class PreferenceGroupTest {
+
+ @JvmField
+ @Rule
+ val rule = ActivityTestRule(TestPreferenceActivity::class.java)
+ private val context = InstrumentationRegistry.getContext()
+ private lateinit var preferenceGroup: PreferenceGroup
+
+ @Before fun setup() {
+ preferenceGroup = (rule
+ .activity
+ .fragmentManager
+ .findFragmentByTag(TestPreferenceActivity.TAG) as PreferenceFragment).preferenceScreen
+ }
+
+ @Test fun get() {
+ val key = "key"
+ val preference = Preference(context)
+ preference.key = key
+ preferenceGroup.addPreference(preference)
+ assertSame(preference, preferenceGroup[key])
+ assertSame(preference, preferenceGroup[0])
+ }
+
+ @Test fun contains() {
+ val preference = Preference(context)
+ assertFalse(preference in preferenceGroup)
+ assertTrue(preference !in preferenceGroup)
+ preferenceGroup.addPreference(preference)
+ assertFalse(preference !in preferenceGroup)
+ assertTrue(preference in preferenceGroup)
+ preferenceGroup.removePreference(preference)
+ assertFalse(preference in preferenceGroup)
+ assertTrue(preference !in preferenceGroup)
+ }
+
+ @Test fun plusAssign() {
+ assertEquals(0, preferenceGroup.preferenceCount)
+
+ val preference1 = Preference(context)
+ preferenceGroup += preference1
+ assertEquals(1, preferenceGroup.preferenceCount)
+ assertSame(preference1, preferenceGroup.getPreference(0))
+
+ val preference2 = Preference(context)
+ preferenceGroup += preference2
+ assertEquals(2, preferenceGroup.preferenceCount)
+ assertSame(preference2, preferenceGroup.getPreference(1))
+ }
+
+ @Test fun minusAssign() {
+ val preference1 = Preference(context)
+ preferenceGroup.addPreference(preference1)
+ val preference2 = Preference(context)
+ preferenceGroup.addPreference(preference2)
+
+ assertEquals(2, preferenceGroup.preferenceCount)
+
+ preferenceGroup -= preference2
+ assertEquals(1, preferenceGroup.preferenceCount)
+ assertSame(preference1, preferenceGroup.getPreference(0))
+
+ preferenceGroup -= preference1
+ assertEquals(0, preferenceGroup.preferenceCount)
+ }
+
+ @Test fun size() {
+ assertEquals(0, preferenceGroup.size)
+
+ val preference = Preference(context)
+ preferenceGroup.addPreference(preference)
+ assertEquals(1, preferenceGroup.size)
+
+ preferenceGroup.removePreference(preference)
+ assertEquals(0, preferenceGroup.size)
+ }
+
+ @Test fun isEmpty() {
+ assertTrue(preferenceGroup.isEmpty())
+ preferenceGroup.addPreference(Preference(context))
+ assertFalse(preferenceGroup.isEmpty())
+ }
+
+ @Test fun isNotEmpty() {
+ assertFalse(preferenceGroup.isNotEmpty())
+ preferenceGroup.addPreference(Preference(context))
+ assertTrue(preferenceGroup.isNotEmpty())
+ }
+
+ @Test fun forEach() {
+ preferenceGroup.forEach {
+ fail("Empty preference group should not invoke lambda")
+ }
+
+ val preference1 = Preference(context).apply { key = "ASD" }
+ preferenceGroup.addPreference(preference1)
+ val preference2 = Preference(context)
+ preferenceGroup.addPreference(preference2)
+
+ val preferences = mutableListOf<Preference>()
+ preferenceGroup.forEach {
+ preferences += it
+ }
+ assertThat(preferences).containsExactly(preference1, preference2)
+ }
+
+ @Test fun forEachIndexed() {
+ preferenceGroup.forEach {
+ fail("Empty preference group should not invoke lambda")
+ }
+
+ val preference1 = Preference(context)
+ preferenceGroup.addPreference(preference1)
+ val preference2 = Preference(context)
+ preferenceGroup.addPreference(preference2)
+
+ val preferences = mutableListOf<Preference>()
+ preferenceGroup.forEachIndexed { index, preference ->
+ assertEquals(index, preferences.size)
+ preferences += preference
+ }
+ assertThat(preferences).containsExactly(preference1, preference2)
+ }
+
+ @Test fun iterator() {
+ val preference1 = Preference(context)
+ preferenceGroup.addPreference(preference1)
+ val preference2 = Preference(context)
+ preferenceGroup.addPreference(preference2)
+
+ val iterator = preferenceGroup.iterator()
+ assertTrue(iterator.hasNext())
+ assertSame(preference1, iterator.next())
+ assertTrue(iterator.hasNext())
+ assertSame(preference2, iterator.next())
+ assertFalse(iterator.hasNext())
+ assertThrows<IndexOutOfBoundsException> {
+ iterator.next()
+ }
+ }
+
+ @Test fun children() {
+ val preferences = listOf(Preference(context), Preference(context), Preference(context))
+ preferences.forEach { preferenceGroup.addPreference(it) }
+
+ preferenceGroup.children.forEachIndexed { index, child ->
+ assertSame(preferences[index], child)
+ }
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/text/CharSequenceTest.kt b/core/ktx/src/androidTest/java/androidx/core/text/CharSequenceTest.kt
new file mode 100644
index 0000000..0ce0a3c
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/text/CharSequenceTest.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.text
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class CharSequenceTest {
+ @Test fun isDigitsOnly() {
+ assertTrue("012345".isDigitsOnly())
+ assertFalse("0123 abc".isDigitsOnly())
+ }
+
+ @Test fun trimmedLength() {
+ assertEquals(6, "string ".trimmedLength())
+ assertEquals(6, " string".trimmedLength())
+ assertEquals(6, "string".trimmedLength())
+ assertEquals(0, "".trimmedLength())
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/text/HtmlTest.kt b/core/ktx/src/androidTest/java/androidx/core/text/HtmlTest.kt
new file mode 100644
index 0000000..0fef5ed
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/text/HtmlTest.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.text
+
+import android.text.Html.FROM_HTML_MODE_COMPACT
+import android.text.Html.ImageGetter
+import android.text.Html.TagHandler
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class HtmlTest {
+ private val imageGetter = ImageGetter { null }
+ private val tagHandler = TagHandler { _, _, _, _ -> }
+
+ @Test fun parseAsHtml() {
+ val parsed = "<b>Hi</b> © > <".parseAsHtml().toString()
+ assertEquals("Hi \u00a9 > <", parsed)
+ }
+
+ @Test fun parseAsHtmlFlags() {
+ val parsed = "<b>Hi</b> © > <".parseAsHtml(FROM_HTML_MODE_COMPACT).toString()
+ assertEquals("Hi \u00a9 > <", parsed)
+ }
+
+ @Test fun parseAsHtmlImageGetterTagHandler() {
+ val parsed = "<b>Hi</b> © > <"
+ .parseAsHtml(FROM_HTML_MODE_COMPACT, imageGetter, tagHandler)
+ .toString()
+ assertEquals("Hi \u00a9 > <", parsed)
+ }
+
+ @Test fun parseAsHtmlFlagsImageGetterTagHandler() {
+ val parsed = "<b>Hi</b> © > <"
+ .parseAsHtml(imageGetter = imageGetter, tagHandler = tagHandler)
+ .toString()
+ assertEquals("Hi \u00a9 > <", parsed)
+ }
+
+ @Test fun convertToHtml() {
+ val html = buildSpannedString {
+ bold {
+ append("Hi")
+ }
+ }.toHtml()
+ assertTrue(html, html.contains("<b>Hi</b>"))
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/text/SpannableStringBuilderTest.kt b/core/ktx/src/androidTest/java/androidx/core/text/SpannableStringBuilderTest.kt
new file mode 100644
index 0000000..c66dd1f
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/text/SpannableStringBuilderTest.kt
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.text
+
+import android.graphics.Color.RED
+import android.graphics.Color.YELLOW
+import android.graphics.Typeface.BOLD
+import android.graphics.Typeface.ITALIC
+import android.text.SpannedString
+import android.text.style.BackgroundColorSpan
+import android.text.style.BulletSpan
+import android.text.style.ForegroundColorSpan
+import android.text.style.RelativeSizeSpan
+import android.text.style.StrikethroughSpan
+import android.text.style.StyleSpan
+import android.text.style.SubscriptSpan
+import android.text.style.SuperscriptSpan
+import android.text.style.UnderlineSpan
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertSame
+import org.junit.Test
+
+class SpannableStringBuilderTest {
+ @Test fun builder() {
+ val result: SpannedString = buildSpannedString {
+ append("Hello,")
+ append(" World")
+ }
+ assertEquals("Hello, World", result.toString())
+ }
+
+ @Test fun builderInSpan() {
+ val bold = StyleSpan(BOLD)
+ val result: SpannedString = buildSpannedString {
+ append("Hello, ")
+ inSpans(bold) {
+ append("World")
+ }
+ }
+ assertEquals("Hello, World", result.toString())
+
+ val spans = result.getSpans<Any>()
+ assertEquals(1, spans.size)
+
+ val boldSpan = spans.filterIsInstance<StyleSpan>().single()
+ assertSame(bold, boldSpan)
+ assertEquals(7, result.getSpanStart(bold))
+ assertEquals(12, result.getSpanEnd(bold))
+ }
+
+ @Test fun builderBold() {
+ val result: SpannedString = buildSpannedString {
+ append("Hello, ")
+ bold {
+ append("World")
+ }
+ }
+ assertEquals("Hello, World", result.toString())
+
+ val spans = result.getSpans<Any>()
+ assertEquals(1, spans.size)
+
+ val bold = spans.filterIsInstance<StyleSpan>().single()
+ assertEquals(BOLD, bold.style)
+ assertEquals(7, result.getSpanStart(bold))
+ assertEquals(12, result.getSpanEnd(bold))
+ }
+
+ @Test fun builderItalic() {
+ val result: SpannedString = buildSpannedString {
+ append("Hello, ")
+ italic {
+ append("World")
+ }
+ }
+ assertEquals("Hello, World", result.toString())
+
+ val spans = result.getSpans<Any>()
+ assertEquals(1, spans.size)
+
+ val italic = spans.filterIsInstance<StyleSpan>().single()
+ assertEquals(ITALIC, italic.style)
+ assertEquals(7, result.getSpanStart(italic))
+ assertEquals(12, result.getSpanEnd(italic))
+ }
+
+ @Test fun builderUnderline() {
+ val result: SpannedString = buildSpannedString {
+ append("Hello, ")
+ underline {
+ append("World")
+ }
+ }
+ assertEquals("Hello, World", result.toString())
+
+ val spans = result.getSpans<Any>()
+ assertEquals(1, spans.size)
+
+ val underline = spans.filterIsInstance<UnderlineSpan>().single()
+ assertEquals(7, result.getSpanStart(underline))
+ assertEquals(12, result.getSpanEnd(underline))
+ }
+
+ @Test fun builderColor() {
+ val result: SpannedString = buildSpannedString {
+ append("Hello, ")
+ color(RED) {
+ append("World")
+ }
+ }
+ assertEquals("Hello, World", result.toString())
+
+ val spans = result.getSpans<Any>()
+ assertEquals(1, spans.size)
+
+ val color = spans.filterIsInstance<ForegroundColorSpan>().single()
+ assertEquals(RED, color.foregroundColor)
+ assertEquals(7, result.getSpanStart(color))
+ assertEquals(12, result.getSpanEnd(color))
+ }
+
+ @Test fun builderBackgroundColor() {
+ val result: SpannedString = buildSpannedString {
+ append("Hello, ")
+ backgroundColor(RED) {
+ append("World")
+ }
+ }
+ assertEquals("Hello, World", result.toString())
+
+ val spans = result.getSpans<Any>()
+ assertEquals(1, spans.size)
+
+ val color = spans.filterIsInstance<BackgroundColorSpan>().single()
+ assertEquals(RED, color.backgroundColor)
+ assertEquals(7, result.getSpanStart(color))
+ assertEquals(12, result.getSpanEnd(color))
+ }
+
+ @Test fun builderStrikeThrough() {
+ val result: SpannedString = buildSpannedString {
+ append("Hello, ")
+ strikeThrough {
+ append("World")
+ }
+ }
+ assertEquals("Hello, World", result.toString())
+
+ val spans = result.getSpans<Any>()
+ assertEquals(1, spans.size)
+
+ val strikeThrough = spans.filterIsInstance<StrikethroughSpan>().single()
+ assertEquals(7, result.getSpanStart(strikeThrough))
+ assertEquals(12, result.getSpanEnd(strikeThrough))
+ }
+
+ @Test fun builderScale() {
+ val result: SpannedString = buildSpannedString {
+ append("Hello, ")
+ scale(2f) {
+ append("World")
+ }
+ }
+ assertEquals("Hello, World", result.toString())
+
+ val spans = result.getSpans<Any>()
+ assertEquals(1, spans.size)
+
+ val scale = spans.filterIsInstance<RelativeSizeSpan>().single()
+ assertEquals(2f, scale.sizeChange)
+ assertEquals(7, result.getSpanStart(scale))
+ assertEquals(12, result.getSpanEnd(scale))
+ }
+
+ @Test fun builderSuperscript() {
+ val result: SpannedString = buildSpannedString {
+ append("Hello, ")
+ superscript {
+ append("World")
+ }
+ }
+ assertEquals("Hello, World", result.toString())
+
+ val spans = result.getSpans<Any>()
+ assertEquals(1, spans.size)
+
+ val superscript = spans.filterIsInstance<SuperscriptSpan>().single()
+ assertEquals(7, result.getSpanStart(superscript))
+ assertEquals(12, result.getSpanEnd(superscript))
+ }
+
+ @Test fun builderSubscript() {
+ val result: SpannedString = buildSpannedString {
+ append("Hello, ")
+ subscript {
+ append("World")
+ }
+ }
+ assertEquals("Hello, World", result.toString())
+
+ val spans = result.getSpans<Any>()
+ assertEquals(1, spans.size)
+
+ val subscript = spans.filterIsInstance<SubscriptSpan>().single()
+ assertEquals(7, result.getSpanStart(subscript))
+ assertEquals(12, result.getSpanEnd(subscript))
+ }
+
+ @Test fun nested() {
+ val result: SpannedString = buildSpannedString {
+ color(RED) {
+ append('H')
+ subscript {
+ append('e')
+ }
+ append('l')
+ superscript {
+ append('l')
+ }
+ append('o')
+ }
+ append(", ")
+ backgroundColor(YELLOW) {
+ append('W')
+ underline {
+ append('o')
+ bold {
+ append('r')
+ }
+ append('l')
+ }
+ append('d')
+ }
+ }
+ assertEquals("Hello, World", result.toString())
+
+ val spans = result.getSpans<Any>()
+ assertEquals(6, spans.size)
+
+ val color = spans.filterIsInstance<ForegroundColorSpan>().single()
+ assertEquals(RED, color.foregroundColor)
+ assertEquals(0, result.getSpanStart(color))
+ assertEquals(5, result.getSpanEnd(color))
+
+ val subscript = spans.filterIsInstance<SubscriptSpan>().single()
+ assertEquals(1, result.getSpanStart(subscript))
+ assertEquals(2, result.getSpanEnd(subscript))
+
+ val superscript = spans.filterIsInstance<SuperscriptSpan>().single()
+ assertEquals(3, result.getSpanStart(superscript))
+ assertEquals(4, result.getSpanEnd(superscript))
+
+ val backgroundColor = spans.filterIsInstance<BackgroundColorSpan>().single()
+ assertEquals(YELLOW, backgroundColor.backgroundColor)
+ assertEquals(7, result.getSpanStart(backgroundColor))
+ assertEquals(12, result.getSpanEnd(backgroundColor))
+
+ val underline = spans.filterIsInstance<UnderlineSpan>().single()
+ assertEquals(8, result.getSpanStart(underline))
+ assertEquals(11, result.getSpanEnd(underline))
+
+ val bold = spans.filterIsInstance<StyleSpan>().single()
+ assertEquals(BOLD, bold.style)
+ assertEquals(9, result.getSpanStart(bold))
+ assertEquals(10, result.getSpanEnd(bold))
+ }
+
+ @Test fun multipleSpans() {
+ val result: SpannedString = buildSpannedString {
+ append("Hello, ")
+ inSpans(BulletSpan(), UnderlineSpan()) {
+ append("World")
+ }
+ }
+ assertEquals("Hello, World", result.toString())
+
+ val spans = result.getSpans<Any>()
+ assertEquals(2, spans.size)
+
+ val bullet = spans.filterIsInstance<BulletSpan>().single()
+ assertEquals(7, result.getSpanStart(bullet))
+ assertEquals(12, result.getSpanEnd(bullet))
+ val underline = spans.filterIsInstance<UnderlineSpan>().single()
+ assertEquals(7, result.getSpanStart(underline))
+ assertEquals(12, result.getSpanEnd(underline))
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/text/SpannableStringTest.kt b/core/ktx/src/androidTest/java/androidx/core/text/SpannableStringTest.kt
new file mode 100644
index 0000000..5fe3c5d
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/text/SpannableStringTest.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.text
+
+import android.graphics.Typeface.BOLD
+import android.text.SpannableString
+import android.text.style.StyleSpan
+import android.text.style.UnderlineSpan
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class SpannableStringTest {
+
+ @Test fun toSpannableString() = assertTrue("Hello, World".toSpannable() is SpannableString)
+
+ @Test fun plusAssign() {
+ val s = "Hello, World".toSpannable()
+
+ val bold = StyleSpan(BOLD)
+ s += bold
+ assertEquals(0, s.getSpanStart(bold))
+ assertEquals(s.length, s.getSpanEnd(bold))
+ }
+
+ @Test fun minusAssign() {
+ val s = "Hello, World".toSpannable()
+ val bold = StyleSpan(BOLD)
+ s += bold
+ assertTrue(s.getSpans<Any>().isNotEmpty())
+ s -= bold
+ assertTrue(s.getSpans<Any>().isEmpty())
+ }
+
+ @Test fun clearSpans() {
+ val s = "Hello, World".toSpannable()
+ s += StyleSpan(BOLD)
+ s += UnderlineSpan()
+ assertTrue(s.getSpans<Any>().isNotEmpty())
+ s.clearSpans()
+ assertTrue(s.getSpans<Any>().isEmpty())
+ }
+
+ @Test fun setIndices() {
+ val s = "Hello, World".toSpannable()
+ s[0, 5] = StyleSpan(BOLD)
+ s[7, 12] = UnderlineSpan()
+
+ val spans = s.getSpans<Any>()
+
+ val bold = spans.filterIsInstance<StyleSpan>().single()
+ assertEquals(0, s.getSpanStart(bold))
+ assertEquals(5, s.getSpanEnd(bold))
+
+ val underline = spans.filterIsInstance<UnderlineSpan>().single()
+ assertEquals(7, s.getSpanStart(underline))
+ assertEquals(12, s.getSpanEnd(underline))
+ }
+
+ @Test fun setRange() {
+ val s = "Hello, World".toSpannable()
+ s[0..5] = StyleSpan(BOLD)
+ s[7..12] = UnderlineSpan()
+
+ val spans = s.getSpans<Any>()
+
+ val bold = spans.filterIsInstance<StyleSpan>().single()
+ assertEquals(0, s.getSpanStart(bold))
+ assertEquals(5, s.getSpanEnd(bold))
+
+ val underline = spans.filterIsInstance<UnderlineSpan>().single()
+ assertEquals(7, s.getSpanStart(underline))
+ assertEquals(12, s.getSpanEnd(underline))
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/text/SpannedStringTest.kt b/core/ktx/src/androidTest/java/androidx/core/text/SpannedStringTest.kt
new file mode 100644
index 0000000..cb8930a
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/text/SpannedStringTest.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.text
+
+import android.graphics.Typeface.BOLD
+import android.text.Spanned.SPAN_INCLUSIVE_EXCLUSIVE
+import android.text.SpannedString
+import android.text.style.StyleSpan
+import android.text.style.UnderlineSpan
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class SpannedStringTest {
+
+ @Test fun toSpanned() = assertTrue("Hello, World".toSpanned() is SpannedString)
+
+ @Test fun getSpans() {
+ val bold = StyleSpan(BOLD)
+ val underline = UnderlineSpan()
+
+ val s = "Hello, World".toSpannable()
+ s.setSpan(bold, 0, 5, SPAN_INCLUSIVE_EXCLUSIVE)
+ s.setSpan(underline, 7, 12, SPAN_INCLUSIVE_EXCLUSIVE)
+
+ assertSame(bold, s.getSpans<StyleSpan>().single())
+ assertSame(underline, s.getSpans<UnderlineSpan>().single())
+ assertEquals(s.getSpans<Any>().size, 2)
+
+ assertSame(bold, s.getSpans<Any>(0, 6).single())
+ assertSame(underline, s.getSpans<Any>(6, 12).single())
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/text/StringTest.kt b/core/ktx/src/androidTest/java/androidx/core/text/StringTest.kt
new file mode 100644
index 0000000..2812799
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/text/StringTest.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.text
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class StringTest {
+ @Test fun htmlEncode() {
+ assertEquals("<> & " '", """<> & " '""".htmlEncode())
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/transition/TransitionTest.kt b/core/ktx/src/androidTest/java/androidx/core/transition/TransitionTest.kt
new file mode 100644
index 0000000..c3a2d22
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/transition/TransitionTest.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.transition
+
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SdkSuppress
+import android.support.test.rule.ActivityTestRule
+import android.transition.Fade
+import android.transition.Transition
+import android.transition.TransitionManager
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.core.TestActivity
+import androidx.core.ktx.test.R
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
+
+@SdkSuppress(minSdkVersion = 19)
+class TransitionTest {
+ @JvmField @Rule val rule = ActivityTestRule<TestActivity>(TestActivity::class.java)
+
+ private lateinit var transition: Transition
+
+ @Before fun setup() {
+ transition = Fade().setDuration(50)
+ }
+
+ @Test fun testDoOnStart() {
+ val called = AtomicBoolean()
+ transition.doOnStart {
+ called.set(true)
+ }
+
+ startTransition(transition)
+ assertTrue(called.get())
+ }
+
+ @Test fun testDoOnEnd() {
+ val called = AtomicBoolean()
+ transition.doOnEnd {
+ called.set(true)
+ }
+
+ val latch = CountDownLatch(1)
+ transition.addListener(object : Transition.TransitionListener {
+ override fun onTransitionEnd(transition: Transition?) {
+ latch.countDown()
+ }
+
+ override fun onTransitionResume(transition: Transition?) = Unit
+ override fun onTransitionPause(transition: Transition?) = Unit
+ override fun onTransitionCancel(transition: Transition?) = Unit
+ override fun onTransitionStart(transition: Transition?) = Unit
+ })
+
+ startTransition(transition)
+
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+
+ assertTrue(called.get())
+ }
+
+ private fun startTransition(t: Transition) {
+ rule.runOnUiThread {
+ val sceneRoot = rule.activity.findViewById<ViewGroup>(R.id.root)
+ val view = rule.activity.findViewById<ImageView>(R.id.image_view)
+
+ TransitionManager.beginDelayedTransition(sceneRoot, t)
+
+ view.visibility = View.INVISIBLE
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/ArrayMapTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/ArrayMapTest.kt
new file mode 100644
index 0000000..5eddef2
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/ArrayMapTest.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.support.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+@SdkSuppress(minSdkVersion = 19)
+class ArrayMapTest {
+ @Test fun empty() {
+ val map = arrayMapOf<String, String>()
+ assertEquals(0, map.size)
+ }
+
+ @Test fun nonEmpty() {
+ val map = arrayMapOf("foo" to "bar", "bar" to "baz")
+ assertThat(map).containsExactly("foo", "bar", "bar", "baz")
+ }
+
+ @Test fun duplicateKeyKeepsLast() {
+ val map = arrayMapOf("foo" to "bar", "foo" to "baz")
+ assertThat(map).containsExactly("foo", "baz")
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/ArraySetTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/ArraySetTest.kt
new file mode 100644
index 0000000..934f18c
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/ArraySetTest.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.support.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+@SdkSuppress(minSdkVersion = 23)
+class ArraySetTest {
+ @Test fun empty() {
+ val set = arraySetOf<String>()
+ assertEquals(0, set.size)
+ }
+
+ @Test fun nonEmpty() {
+ val set = arraySetOf("foo", "bar", "baz")
+ assertThat(set).containsExactly("foo", "bar", "baz")
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/AtomicFileTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/AtomicFileTest.kt
new file mode 100644
index 0000000..59fc8d9
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/AtomicFileTest.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.support.test.filters.SdkSuppress
+import android.util.AtomicFile
+import androidx.testutils.assertThrows
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import java.io.IOException
+
+@SdkSuppress(minSdkVersion = 17)
+class AtomicFileTest {
+ @get:Rule val temporaryFolder = TemporaryFolder()
+
+ private lateinit var file: AtomicFile
+
+ @Before fun before() {
+ file = AtomicFile(temporaryFolder.newFile())
+ }
+
+ @Test fun tryWriteSuccess() {
+ file.tryWrite {
+ it.write(byteArrayOf(0, 1, 2))
+ }
+ val bytes = file.openRead().use { it.readBytes() }
+ assertArrayEquals(byteArrayOf(0, 1, 2), bytes)
+ }
+
+ @Test fun tryWriteFail() {
+ val os = file.startWrite()
+ os.write(byteArrayOf(0, 1, 2))
+ file.finishWrite(os)
+
+ val failure = IOException("Broken!")
+ assertThrows<IOException> {
+ file.tryWrite {
+ it.write(byteArrayOf(3, 4, 5))
+ throw failure
+ }
+ }.isSameAs(failure)
+
+ val bytes = file.openRead().use { it.readBytes() }
+ assertArrayEquals(byteArrayOf(0, 1, 2), bytes)
+ }
+
+ @Test fun writeBytes() {
+ file.writeBytes(byteArrayOf(0, 1, 2))
+
+ val bytes = file.openRead().use { it.readBytes() }
+ assertArrayEquals(byteArrayOf(0, 1, 2), bytes)
+ }
+
+ @Test fun writeText() {
+ file.writeText("Hey")
+
+ val bytes = file.openRead().use { it.readBytes() }
+ assertArrayEquals(byteArrayOf(72, 101, 121), bytes)
+ }
+
+ @Test fun writeTextCharset() {
+ file.writeText("Hey", charset = Charsets.UTF_16LE)
+
+ val bytes = file.openRead().use { it.readBytes() }
+ assertArrayEquals(byteArrayOf(72, 0, 101, 0, 121, 0), bytes)
+ }
+
+ @Test fun readBytes() {
+ val os = file.startWrite()
+ os.write(byteArrayOf(0, 1, 2))
+ file.finishWrite(os)
+
+ assertArrayEquals(byteArrayOf(0, 1, 2), file.readBytes())
+ }
+
+ @Test fun readText() {
+ val os = file.startWrite()
+ os.write(byteArrayOf(72, 101, 121))
+ file.finishWrite(os)
+
+ assertEquals("Hey", file.readText())
+ }
+
+ @Test fun readTextCharset() {
+ val os = file.startWrite()
+ os.write(byteArrayOf(72, 0, 101, 0, 121, 0))
+ file.finishWrite(os)
+
+ assertEquals("Hey", file.readText(charset = Charsets.UTF_16LE))
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/HalfTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/HalfTest.kt
new file mode 100644
index 0000000..b79e064
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/HalfTest.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.support.test.filters.SdkSuppress
+import android.util.Half
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+@SdkSuppress(minSdkVersion = 26)
+class HalfTest {
+ @Test fun shortToHalf() = assertEquals(Half(1.toShort()), 1.toShort().toHalf())
+
+ @Test fun floatToHalf() = assertEquals(Half(1f), 1f.toHalf())
+
+ @Test fun doubleToHalf() = assertEquals(Half(1.0), 1.0.toHalf())
+
+ @Test fun stringToHalf() = assertEquals(Half("1"), "1".toHalf())
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/LocaleTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/LocaleTest.kt
new file mode 100644
index 0000000..0a345f0
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/LocaleTest.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.view.View
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import java.util.Locale
+
+class LocaleTest {
+ @Test fun layoutDirectionWithLTR() {
+ val ltrLocale = Locale.Builder().setLanguage("en").build()
+ assertEquals(View.LAYOUT_DIRECTION_LTR, ltrLocale.layoutDirection)
+ }
+
+ @Test fun layoutDirectionWithRTL() {
+ val rtlLocale = Locale.Builder().setLanguage("ar").build()
+ assertEquals(View.LAYOUT_DIRECTION_RTL, rtlLocale.layoutDirection)
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/LongSparseArrayTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/LongSparseArrayTest.kt
new file mode 100644
index 0000000..c2289e6
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/LongSparseArrayTest.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.support.test.filters.SdkSuppress
+import android.util.LongSparseArray
+import androidx.testutils.fail
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@SdkSuppress(minSdkVersion = 16)
+class LongSparseArrayTest {
+ @Test fun sizeProperty() {
+ val array = LongSparseArray<String>()
+ assertEquals(0, array.size)
+ array.put(1L, "one")
+ assertEquals(1, array.size)
+ }
+
+ @Test fun containsOperator() {
+ val array = LongSparseArray<String>()
+ assertFalse(1L in array)
+ array.put(1L, "one")
+ assertTrue(1L in array)
+ }
+
+ @Test fun containsOperatorWithValue() {
+ val array = LongSparseArray<String>()
+
+ array.put(1L, "one")
+ assertFalse(2L in array)
+
+ array.put(2L, "two")
+ assertTrue(2L in array)
+ }
+
+ @Test fun setOperator() {
+ val array = LongSparseArray<String>()
+ array[1L] = "one"
+ assertEquals("one", array.get(1L))
+ }
+
+ @Test fun plusOperator() {
+ val first = LongSparseArray<String>().apply { put(1L, "one") }
+ val second = LongSparseArray<String>().apply { put(2L, "two") }
+ val combined = first + second
+ assertEquals(2, combined.size())
+ assertEquals(1L, combined.keyAt(0))
+ assertEquals("one", combined.valueAt(0))
+ assertEquals(2L, combined.keyAt(1))
+ assertEquals("two", combined.valueAt(1))
+ }
+
+ @Test fun containsKey() {
+ val array = LongSparseArray<String>()
+ assertFalse(array.containsKey(1L))
+ array.put(1L, "one")
+ assertTrue(array.containsKey(1L))
+ }
+
+ @Test fun containsKeyWithValue() {
+ val array = LongSparseArray<String>()
+
+ array.put(1L, "one")
+ assertFalse(array.containsKey(2L))
+
+ array.put(2L, "one")
+ assertTrue(array.containsKey(2L))
+ }
+
+ @Test fun containsValue() {
+ val array = LongSparseArray<String>()
+ assertFalse(array.containsValue("one"))
+ array.put(1L, "one")
+ assertTrue(array.containsValue("one"))
+ }
+
+ @Test fun getOrDefault() {
+ val array = LongSparseArray<Any>()
+ val default = Any()
+ assertSame(default, array.getOrDefault(1L, default))
+ array.put(1L, "one")
+ assertEquals("one", array.getOrDefault(1L, default))
+ }
+
+ @Test fun getOrElse() {
+ val array = LongSparseArray<Any>()
+ val default = Any()
+ assertSame(default, array.getOrElse(1L) { default })
+ array.put(1L, "one")
+ assertEquals("one", array.getOrElse(1L) { fail() })
+ }
+
+ @Test fun isEmpty() {
+ val array = LongSparseArray<String>()
+ assertTrue(array.isEmpty())
+ array.put(1L, "one")
+ assertFalse(array.isEmpty())
+ }
+
+ @Test fun isNotEmpty() {
+ val array = LongSparseArray<String>()
+ assertFalse(array.isNotEmpty())
+ array.put(1L, "one")
+ assertTrue(array.isNotEmpty())
+ }
+
+ @Test fun removeValue() {
+ val array = LongSparseArray<String>()
+ array.put(1L, "one")
+ assertFalse(array.remove(0L, "one"))
+ assertEquals(1, array.size())
+ assertFalse(array.remove(1L, "two"))
+ assertEquals(1, array.size())
+ assertTrue(array.remove(1L, "one"))
+ assertEquals(0, array.size())
+ }
+
+ @Test fun putAll() {
+ val dest = LongSparseArray<String>()
+ val source = LongSparseArray<String>()
+ source.put(1L, "one")
+
+ assertEquals(0, dest.size())
+ dest.putAll(source)
+ assertEquals(1, dest.size())
+ }
+
+ @Test fun forEach() {
+ val array = LongSparseArray<String>()
+ array.forEach { _, _ -> fail() }
+
+ array.put(1L, "one")
+ array.put(2L, "two")
+ array.put(6L, "six")
+
+ val keys = mutableListOf<Long>()
+ val values = mutableListOf<String>()
+ array.forEach { key, value ->
+ keys.add(key)
+ values.add(value)
+ }
+ assertThat(keys).containsExactly(1L, 2L, 6L)
+ assertThat(values).containsExactly("one", "two", "six")
+ }
+
+ @Test fun keyIterator() {
+ val array = LongSparseArray<String>()
+ assertFalse(array.keyIterator().hasNext())
+
+ array.put(1L, "one")
+ array.put(2L, "two")
+ array.put(6L, "six")
+
+ val iterator = array.keyIterator()
+ assertTrue(iterator.hasNext())
+ assertEquals(1L, iterator.nextLong())
+ assertTrue(iterator.hasNext())
+ assertEquals(2L, iterator.nextLong())
+ assertTrue(iterator.hasNext())
+ assertEquals(6L, iterator.nextLong())
+ assertFalse(iterator.hasNext())
+ }
+
+ @Test fun valueIterator() {
+ val array = LongSparseArray<String>()
+ assertFalse(array.valueIterator().hasNext())
+
+ array.put(1L, "one")
+ array.put(2L, "two")
+ array.put(6L, "six")
+
+ val iterator = array.valueIterator()
+ assertTrue(iterator.hasNext())
+ assertEquals("one", iterator.next())
+ assertTrue(iterator.hasNext())
+ assertEquals("two", iterator.next())
+ assertTrue(iterator.hasNext())
+ assertEquals("six", iterator.next())
+ assertFalse(iterator.hasNext())
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/LruCacheTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/LruCacheTest.kt
new file mode 100644
index 0000000..198e13d
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/LruCacheTest.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class LruCacheTest {
+ private data class TestData(val x: String = "bla")
+
+ @Test fun size() {
+ val cache = lruCache<String, TestData>(200, { k, (x) -> k.length * x.length })
+ cache.put("long", TestData())
+ assertEquals(cache.size(), 12)
+ }
+
+ @Test fun create() {
+ val cache = lruCache<String, TestData>(200, create = { key -> TestData("$key foo") })
+ assertEquals(cache.get("kung"), TestData("kung foo"))
+ }
+
+ @Test fun onEntryRemoved() {
+ var wasCalled = false
+
+ val cache = lruCache<String, TestData>(200, onEntryRemoved = { _, _, _, _ ->
+ wasCalled = true
+ })
+ val initial = TestData()
+ cache.put("a", initial)
+ cache.remove("a")
+ assertTrue(wasCalled)
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/PairTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/PairTest.kt
new file mode 100644
index 0000000..e3fac39
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/PairTest.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.util.Pair
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertSame
+import org.junit.Test
+
+class PairTest {
+ @Test fun destructuringNonNull() {
+ val pair = Pair("one", "two")
+ val (first: String, second: String) = pair
+ assertSame(pair.first, first)
+ assertSame(pair.second, second)
+ }
+
+ @Test fun destructuringNullable() {
+ val pair = Pair("one", "two")
+ val (first: String?, second: String?) = pair
+ assertSame(pair.first, first)
+ assertSame(pair.second, second)
+ }
+
+ @Test fun toKotlin() {
+ val android = Pair("one", "two")
+ val kotlin = android.toKotlinPair()
+ assertEquals(android.first to android.second, kotlin)
+ }
+
+ @Test fun toAndroid() {
+ val kotlin = kotlin.Pair("one", "two")
+ val android = kotlin.toAndroidPair()
+ assertEquals(Pair(kotlin.first, kotlin.second), android)
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/RangeTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/RangeTest.kt
new file mode 100644
index 0000000..ec5d238
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/RangeTest.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.support.test.filters.SdkSuppress
+import android.util.Range
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+@SdkSuppress(minSdkVersion = 21)
+class RangeTest {
+ @Test fun infixFactory() {
+ val range: Range<String> = "a" rangeTo "c"
+ assertEquals("a", range.lower)
+ assertEquals("c", range.upper)
+ }
+
+ @Test fun extendValue() {
+ val range = ("a" rangeTo "c") + "e"
+ assertEquals("a", range.lower)
+ assertEquals("e", range.upper)
+ }
+
+ @Test fun extendRange() {
+ val range = ("a" rangeTo "c") + ("e" rangeTo "g")
+ assertEquals("a", range.lower)
+ assertEquals("g", range.upper)
+ }
+
+ @Test fun intersection() {
+ val range = ("a" rangeTo "e") and ("c" rangeTo "g")
+ assertEquals("c", range.lower)
+ assertEquals("e", range.upper)
+ }
+
+ @Test fun kotlinToAndroid() {
+ val range: Range<Int> = (1..3).toRange()
+ assertEquals(1, range.lower)
+ assertEquals(3, range.upper)
+ }
+
+ @Test fun androidToKotlin() {
+ val range: ClosedRange<String> = Range<String>("a", "c").toClosedRange()
+ assertEquals("a", range.start)
+ assertEquals("c", range.endInclusive)
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/SizeTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/SizeTest.kt
new file mode 100644
index 0000000..b38c747
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/SizeTest.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.support.test.filters.SdkSuppress
+import android.util.Size
+import android.util.SizeF
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+@SdkSuppress(minSdkVersion = 21)
+class SizeTest {
+ @Test fun destructuringSize() {
+ val (w, h) = Size(320, 240)
+ assertEquals(320, w)
+ assertEquals(240, h)
+ }
+
+ @Test fun destructuringSizeF() {
+ val (w, h) = SizeF(1920.0f, 1080.0f)
+ assertEquals(1920.0f, w)
+ assertEquals(1080.0f, h)
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/SparseArrayTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/SparseArrayTest.kt
new file mode 100644
index 0000000..4a49214
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/SparseArrayTest.kt
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.util.SparseArray
+import androidx.testutils.fail
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class SparseArrayTest {
+ @Test fun sizeProperty() {
+ val array = SparseArray<String>()
+ assertEquals(0, array.size)
+ array.put(1, "one")
+ assertEquals(1, array.size)
+ }
+
+ @Test fun containsOperator() {
+ val array = SparseArray<String>()
+ assertFalse(1 in array)
+ array.put(1, "one")
+ assertTrue(1 in array)
+ }
+
+ @Test fun containsOperatorWithItem() {
+ val array = SparseArray<String>()
+
+ array.put(1, "one")
+ assertFalse(2 in array)
+
+ array.put(2, "two")
+ assertTrue(2 in array)
+ }
+
+ @Test fun setOperator() {
+ val array = SparseArray<String>()
+ array[1] = "one"
+ assertEquals("one", array.get(1))
+ }
+
+ @Test fun plusOperator() {
+ val first = SparseArray<String>().apply { put(1, "one") }
+ val second = SparseArray<String>().apply { put(2, "two") }
+ val combined = first + second
+ assertEquals(2, combined.size())
+ assertEquals(1, combined.keyAt(0))
+ assertEquals("one", combined.valueAt(0))
+ assertEquals(2, combined.keyAt(1))
+ assertEquals("two", combined.valueAt(1))
+ }
+
+ @Test fun containsKey() {
+ val array = SparseArray<String>()
+ assertFalse(array.containsKey(1))
+ array.put(1, "one")
+ assertTrue(array.containsKey(1))
+ }
+
+ @Test fun containsValue() {
+ val array = SparseArray<String>()
+ assertFalse(array.containsValue("one"))
+ array.put(1, "one")
+ assertTrue(array.containsValue("one"))
+ }
+
+ @Test fun getOrDefault() {
+ val array = SparseArray<Any>()
+ val default = Any()
+ assertSame(default, array.getOrDefault(1, default))
+ array.put(1, "one")
+ assertEquals("one", array.getOrDefault(1, default))
+ }
+
+ @Test fun getOrElse() {
+ val array = SparseArray<Any>()
+ val default = Any()
+ assertSame(default, array.getOrElse(1) { default })
+ array.put(1, "one")
+ assertEquals("one", array.getOrElse(1) { fail() })
+ }
+
+ @Test fun isEmpty() {
+ val array = SparseArray<String>()
+ assertTrue(array.isEmpty())
+ array.put(1, "one")
+ assertFalse(array.isEmpty())
+ }
+
+ @Test fun isNotEmpty() {
+ val array = SparseArray<String>()
+ assertFalse(array.isNotEmpty())
+ array.put(1, "one")
+ assertTrue(array.isNotEmpty())
+ }
+
+ @Test fun removeValue() {
+ val array = SparseArray<String>()
+ array.put(1, "one")
+ assertFalse(array.remove(0, "one"))
+ assertEquals(1, array.size())
+ assertFalse(array.remove(1, "two"))
+ assertEquals(1, array.size())
+ assertTrue(array.remove(1, "one"))
+ assertEquals(0, array.size())
+ }
+
+ @Test fun putAll() {
+ val dest = SparseArray<String>()
+ val source = SparseArray<String>()
+ source.put(1, "one")
+
+ assertEquals(0, dest.size())
+ dest.putAll(source)
+ assertEquals(1, dest.size())
+ }
+
+ @Test fun forEach() {
+ val array = SparseArray<String>()
+ array.forEach { _, _ -> fail() }
+
+ array.put(1, "one")
+ array.put(2, "two")
+ array.put(6, "six")
+
+ val keys = mutableListOf<Int>()
+ val values = mutableListOf<String>()
+ array.forEach { key, value ->
+ keys.add(key)
+ values.add(value)
+ }
+ assertThat(keys).containsExactly(1, 2, 6)
+ assertThat(values).containsExactly("one", "two", "six")
+ }
+
+ @Test fun keyIterator() {
+ val array = SparseArray<String>()
+ assertFalse(array.keyIterator().hasNext())
+
+ array.put(1, "one")
+ array.put(2, "two")
+ array.put(6, "six")
+
+ val iterator = array.keyIterator()
+ assertTrue(iterator.hasNext())
+ assertEquals(1, iterator.nextInt())
+ assertTrue(iterator.hasNext())
+ assertEquals(2, iterator.nextInt())
+ assertTrue(iterator.hasNext())
+ assertEquals(6, iterator.nextInt())
+ assertFalse(iterator.hasNext())
+ }
+
+ @Test fun valueIterator() {
+ val array = SparseArray<String>()
+ assertFalse(array.valueIterator().hasNext())
+
+ array.put(1, "one")
+ array.put(2, "two")
+ array.put(6, "six")
+
+ val iterator = array.valueIterator()
+ assertTrue(iterator.hasNext())
+ assertEquals("one", iterator.next())
+ assertTrue(iterator.hasNext())
+ assertEquals("two", iterator.next())
+ assertTrue(iterator.hasNext())
+ assertEquals("six", iterator.next())
+ assertFalse(iterator.hasNext())
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/SparseBooleanArrayTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/SparseBooleanArrayTest.kt
new file mode 100644
index 0000000..1d67493
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/SparseBooleanArrayTest.kt
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.util.SparseBooleanArray
+import androidx.testutils.fail
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class SparseBooleanArrayTest {
+ @Test fun sizeProperty() {
+ val array = SparseBooleanArray()
+ assertEquals(0, array.size)
+ array.put(1, true)
+ assertEquals(1, array.size)
+ }
+
+ @Test fun containsOperator() {
+ val array = SparseBooleanArray()
+ assertFalse(1 in array)
+ array.put(1, true)
+ assertTrue(1 in array)
+ }
+
+ @Test fun containsOperatorWithValue() {
+ val array = SparseBooleanArray()
+
+ array.put(1, true)
+ assertFalse(2 in array)
+
+ array.put(2, true)
+ assertTrue(2 in array)
+ }
+
+ @Test fun setOperator() {
+ val array = SparseBooleanArray()
+ array[1] = true
+ assertTrue(array.get(1))
+ }
+
+ @Test fun plusOperator() {
+ val first = SparseBooleanArray().apply { put(1, true) }
+ val second = SparseBooleanArray().apply { put(2, false) }
+ val combined = first + second
+ assertEquals(2, combined.size())
+ assertEquals(1, combined.keyAt(0))
+ assertTrue(combined.valueAt(0))
+ assertEquals(2, combined.keyAt(1))
+ assertFalse(combined.valueAt(1))
+ }
+
+ @Test fun containsKey() {
+ val array = SparseBooleanArray()
+ assertFalse(array.containsKey(1))
+ array.put(1, true)
+ assertTrue(array.containsKey(1))
+ }
+
+ @Test fun containsKeyWithValue() {
+ val array = SparseBooleanArray()
+
+ array.put(1, true)
+ assertFalse(array.containsKey(2))
+
+ array.put(2, true)
+ assertTrue(array.containsKey(2))
+ }
+
+ @Test fun containsValue() {
+ val array = SparseBooleanArray()
+ assertFalse(array.containsValue(true))
+ array.put(1, true)
+ assertTrue(array.containsValue(true))
+ }
+
+ @Test fun getOrDefault() {
+ val array = SparseBooleanArray()
+ assertFalse(array.getOrDefault(1, false))
+ array.put(1, true)
+ assertTrue(array.getOrDefault(1, false))
+ }
+
+ @Test fun getOrElse() {
+ val array = SparseBooleanArray()
+ assertFalse(array.getOrElse(1) { false })
+ array.put(1, true)
+ assertTrue(array.getOrElse(1) { fail() })
+ }
+
+ @Test fun isEmpty() {
+ val array = SparseBooleanArray()
+ assertTrue(array.isEmpty())
+ array.put(1, true)
+ assertFalse(array.isEmpty())
+ }
+
+ @Test fun isNotEmpty() {
+ val array = SparseBooleanArray()
+ assertFalse(array.isNotEmpty())
+ array.put(1, true)
+ assertTrue(array.isNotEmpty())
+ }
+
+ @Test fun removeValue() {
+ val array = SparseBooleanArray()
+ array.put(1, true)
+ assertFalse(array.remove(0, true))
+ assertEquals(1, array.size())
+ assertFalse(array.remove(1, false))
+ assertEquals(1, array.size())
+ assertTrue(array.remove(1, true))
+ assertEquals(0, array.size())
+ }
+
+ @Test fun putAll() {
+ val dest = SparseBooleanArray()
+ val source = SparseBooleanArray()
+ source.put(1, true)
+
+ assertEquals(0, dest.size())
+ dest.putAll(source)
+ assertEquals(1, dest.size())
+ }
+
+ @Test fun forEach() {
+ val array = SparseBooleanArray()
+ array.forEach { _, _ -> fail() }
+
+ array.put(1, true)
+ array.put(2, false)
+ array.put(6, true)
+
+ val keys = mutableListOf<Int>()
+ val values = mutableListOf<Boolean>()
+ array.forEach { key, value ->
+ keys.add(key)
+ values.add(value)
+ }
+ assertThat(keys).containsExactly(1, 2, 6)
+ assertThat(values).containsExactly(true, false, true)
+ }
+
+ @Test fun keyIterator() {
+ val array = SparseBooleanArray()
+ assertFalse(array.keyIterator().hasNext())
+
+ array.put(1, true)
+ array.put(2, false)
+ array.put(6, true)
+
+ val iterator = array.keyIterator()
+ assertTrue(iterator.hasNext())
+ assertEquals(1, iterator.nextInt())
+ assertTrue(iterator.hasNext())
+ assertEquals(2, iterator.nextInt())
+ assertTrue(iterator.hasNext())
+ assertEquals(6, iterator.nextInt())
+ assertFalse(iterator.hasNext())
+ }
+
+ @Test fun valueIterator() {
+ val array = SparseBooleanArray()
+ assertFalse(array.valueIterator().hasNext())
+
+ array.put(1, true)
+ array.put(2, false)
+ array.put(6, true)
+
+ val iterator = array.valueIterator()
+ assertTrue(iterator.hasNext())
+ assertTrue(iterator.nextBoolean())
+ assertTrue(iterator.hasNext())
+ assertFalse(iterator.nextBoolean())
+ assertTrue(iterator.hasNext())
+ assertTrue(iterator.nextBoolean())
+ assertFalse(iterator.hasNext())
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/SparseIntArrayTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/SparseIntArrayTest.kt
new file mode 100644
index 0000000..40794c1
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/SparseIntArrayTest.kt
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.util.SparseIntArray
+import androidx.testutils.fail
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class SparseIntArrayTest {
+ @Test fun sizeProperty() {
+ val array = SparseIntArray()
+ assertEquals(0, array.size)
+ array.put(1, 11)
+ assertEquals(1, array.size)
+ }
+
+ @Test fun containsOperator() {
+ val array = SparseIntArray()
+ assertFalse(1 in array)
+ array.put(1, 11)
+ assertTrue(1 in array)
+ }
+
+ @Test fun containsOperatorWithValue() {
+ val array = SparseIntArray()
+
+ array.put(1, 11)
+ assertFalse(2 in array)
+
+ array.put(2, 22)
+ assertTrue(2 in array)
+ }
+
+ @Test fun setOperator() {
+ val array = SparseIntArray()
+ array[1] = 11
+ assertEquals(11, array.get(1))
+ }
+
+ @Test fun plusOperator() {
+ val first = SparseIntArray().apply { put(1, 11) }
+ val second = SparseIntArray().apply { put(2, 22) }
+ val combined = first + second
+ assertEquals(2, combined.size())
+ assertEquals(1, combined.keyAt(0))
+ assertEquals(11, combined.valueAt(0))
+ assertEquals(2, combined.keyAt(1))
+ assertEquals(22, combined.valueAt(1))
+ }
+
+ @Test fun containsKey() {
+ val array = SparseIntArray()
+ assertFalse(array.containsKey(1))
+ array.put(1, 11)
+ assertTrue(array.containsKey(1))
+ }
+
+ @Test fun containsKeyWithValue() {
+ val array = SparseIntArray()
+
+ array.put(1, 11)
+ assertFalse(array.containsKey(2))
+
+ array.put(2, 22)
+ assertTrue(array.containsKey(2))
+ }
+
+ @Test fun containsValue() {
+ val array = SparseIntArray()
+ assertFalse(array.containsValue(11))
+ array.put(1, 11)
+ assertTrue(array.containsValue(11))
+ }
+
+ @Test fun getOrDefault() {
+ val array = SparseIntArray()
+ assertEquals(22, array.getOrDefault(1, 22))
+ array.put(1, 11)
+ assertEquals(11, array.getOrDefault(1, 22))
+ }
+
+ @Test fun getOrElse() {
+ val array = SparseIntArray()
+ assertEquals(22, array.getOrElse(1) { 22 })
+ array.put(1, 11)
+ assertEquals(11, array.getOrElse(1) { fail() })
+ }
+
+ @Test fun isEmpty() {
+ val array = SparseIntArray()
+ assertTrue(array.isEmpty())
+ array.put(1, 11)
+ assertFalse(array.isEmpty())
+ }
+
+ @Test fun isNotEmpty() {
+ val array = SparseIntArray()
+ assertFalse(array.isNotEmpty())
+ array.put(1, 11)
+ assertTrue(array.isNotEmpty())
+ }
+
+ @Test fun removeValue() {
+ val array = SparseIntArray()
+ array.put(1, 11)
+ assertFalse(array.remove(0, 11))
+ assertEquals(1, array.size())
+ assertFalse(array.remove(1, 22))
+ assertEquals(1, array.size())
+ assertTrue(array.remove(1, 11))
+ assertEquals(0, array.size())
+ }
+
+ @Test fun putAll() {
+ val dest = SparseIntArray()
+ val source = SparseIntArray()
+ source.put(1, 11)
+
+ assertEquals(0, dest.size())
+ dest.putAll(source)
+ assertEquals(1, dest.size())
+ }
+
+ @Test fun forEach() {
+ val array = SparseIntArray()
+ array.forEach { _, _ -> fail() }
+
+ array.put(1, 11)
+ array.put(2, 22)
+ array.put(6, 66)
+
+ val keys = mutableListOf<Int>()
+ val values = mutableListOf<Int>()
+ array.forEach { key, value ->
+ keys.add(key)
+ values.add(value)
+ }
+ assertThat(keys).containsExactly(1, 2, 6)
+ assertThat(values).containsExactly(11, 22, 66)
+ }
+
+ @Test fun keyIterator() {
+ val array = SparseIntArray()
+ assertFalse(array.keyIterator().hasNext())
+
+ array.put(1, 11)
+ array.put(2, 22)
+ array.put(6, 66)
+
+ val iterator = array.keyIterator()
+ assertTrue(iterator.hasNext())
+ assertEquals(1, iterator.nextInt())
+ assertTrue(iterator.hasNext())
+ assertEquals(2, iterator.nextInt())
+ assertTrue(iterator.hasNext())
+ assertEquals(6, iterator.nextInt())
+ assertFalse(iterator.hasNext())
+ }
+
+ @Test fun valueIterator() {
+ val array = SparseIntArray()
+ assertFalse(array.valueIterator().hasNext())
+
+ array.put(1, 11)
+ array.put(2, 22)
+ array.put(6, 66)
+
+ val iterator = array.valueIterator()
+ assertTrue(iterator.hasNext())
+ assertEquals(11, iterator.nextInt())
+ assertTrue(iterator.hasNext())
+ assertEquals(22, iterator.nextInt())
+ assertTrue(iterator.hasNext())
+ assertEquals(66, iterator.nextInt())
+ assertFalse(iterator.hasNext())
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/util/SparseLongArrayTest.kt b/core/ktx/src/androidTest/java/androidx/core/util/SparseLongArrayTest.kt
new file mode 100644
index 0000000..c65c2ad
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/util/SparseLongArrayTest.kt
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.support.test.filters.SdkSuppress
+import android.util.SparseLongArray
+import androidx.testutils.fail
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@SdkSuppress(minSdkVersion = 18)
+class SparseLongArrayTest {
+ @Test fun sizeProperty() {
+ val array = SparseLongArray()
+ assertEquals(0, array.size)
+ array.put(1, 11L)
+ assertEquals(1, array.size)
+ }
+
+ @Test fun containsOperator() {
+ val array = SparseLongArray()
+ assertFalse(1 in array)
+ array.put(1, 11L)
+ assertTrue(1 in array)
+ }
+
+ @Test fun containsOperatorWithValue() {
+ val array = SparseLongArray()
+
+ array.put(1, 11L)
+ assertFalse(2 in array)
+
+ array.put(2, 22L)
+ assertTrue(2 in array)
+ }
+
+ @Test fun setOperator() {
+ val array = SparseLongArray()
+ array[1] = 11L
+ assertEquals(11L, array.get(1))
+ }
+
+ @Test fun plusOperator() {
+ val first = SparseLongArray().apply { put(1, 11L) }
+ val second = SparseLongArray().apply { put(2, 22L) }
+ val combined = first + second
+ assertEquals(2, combined.size())
+ assertEquals(1, combined.keyAt(0))
+ assertEquals(11L, combined.valueAt(0))
+ assertEquals(2, combined.keyAt(1))
+ assertEquals(22L, combined.valueAt(1))
+ }
+
+ @Test fun containsKey() {
+ val array = SparseLongArray()
+ assertFalse(array.containsKey(1))
+ array.put(1, 11L)
+ assertTrue(array.containsKey(1))
+ }
+
+ @Test fun containsKeyWithValue() {
+ val array = SparseLongArray()
+
+ array.put(1, 11L)
+ assertFalse(array.containsKey(2))
+
+ array.put(2, 22L)
+ assertTrue(array.containsKey(2))
+ }
+
+ @Test fun containsValue() {
+ val array = SparseLongArray()
+ assertFalse(array.containsValue(11L))
+ array.put(1, 11L)
+ assertTrue(array.containsValue(11L))
+ }
+
+ @Test fun getOrDefault() {
+ val array = SparseLongArray()
+ assertEquals(22L, array.getOrDefault(1, 22L))
+ array.put(1, 11L)
+ assertEquals(11L, array.getOrDefault(1, 22L))
+ }
+
+ @Test fun getOrElse() {
+ val array = SparseLongArray()
+ assertEquals(22L, array.getOrElse(1) { 22L })
+ array.put(1, 11L)
+ assertEquals(11L, array.getOrElse(1) { fail() })
+ }
+
+ @Test fun isEmpty() {
+ val array = SparseLongArray()
+ assertTrue(array.isEmpty())
+ array.put(1, 11L)
+ assertFalse(array.isEmpty())
+ }
+
+ @Test fun isNotEmpty() {
+ val array = SparseLongArray()
+ assertFalse(array.isNotEmpty())
+ array.put(1, 11L)
+ assertTrue(array.isNotEmpty())
+ }
+
+ @Test fun removeValue() {
+ val array = SparseLongArray()
+ array.put(1, 11L)
+ assertFalse(array.remove(0, 11L))
+ assertEquals(1, array.size())
+ assertFalse(array.remove(1, 22L))
+ assertEquals(1, array.size())
+ assertTrue(array.remove(1, 11L))
+ assertEquals(0, array.size())
+ }
+
+ @Test fun putAll() {
+ val dest = SparseLongArray()
+ val source = SparseLongArray()
+ source.put(1, 11L)
+
+ assertEquals(0, dest.size())
+ dest.putAll(source)
+ assertEquals(1, dest.size())
+ }
+
+ @Test fun forEach() {
+ val array = SparseLongArray()
+ array.forEach { _, _ -> fail() }
+
+ array.put(1, 11L)
+ array.put(2, 22L)
+ array.put(6, 66L)
+
+ val keys = mutableListOf<Int>()
+ val values = mutableListOf<Long>()
+ array.forEach { key, value ->
+ keys.add(key)
+ values.add(value)
+ }
+ assertThat(keys).containsExactly(1, 2, 6)
+ assertThat(values).containsExactly(11L, 22L, 66L)
+ }
+
+ @Test fun keyIterator() {
+ val array = SparseLongArray()
+ assertFalse(array.keyIterator().hasNext())
+
+ array.put(1, 11L)
+ array.put(2, 22L)
+ array.put(6, 66L)
+
+ val iterator = array.keyIterator()
+ assertTrue(iterator.hasNext())
+ assertEquals(1, iterator.nextInt())
+ assertTrue(iterator.hasNext())
+ assertEquals(2, iterator.nextInt())
+ assertTrue(iterator.hasNext())
+ assertEquals(6, iterator.nextInt())
+ assertFalse(iterator.hasNext())
+ }
+
+ @Test fun valueIterator() {
+ val array = SparseLongArray()
+ assertFalse(array.valueIterator().hasNext())
+
+ array.put(1, 11L)
+ array.put(2, 22L)
+ array.put(6, 66L)
+
+ val iterator = array.valueIterator()
+ assertTrue(iterator.hasNext())
+ assertEquals(11, iterator.nextLong())
+ assertTrue(iterator.hasNext())
+ assertEquals(22, iterator.nextLong())
+ assertTrue(iterator.hasNext())
+ assertEquals(66, iterator.nextLong())
+ assertFalse(iterator.hasNext())
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/view/AccessibilityAnnouncementCapturingView.kt b/core/ktx/src/androidTest/java/androidx/core/view/AccessibilityAnnouncementCapturingView.kt
new file mode 100644
index 0000000..81996b8
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/view/AccessibilityAnnouncementCapturingView.kt
@@ -0,0 +1,14 @@
+package androidx.core.view
+
+import android.content.Context
+import android.view.View
+
+class AccessibilityAnnouncementCapturingView(context: Context?) : View(context) {
+
+ var announcement: CharSequence? = null
+
+ override fun announceForAccessibility(text: CharSequence?) {
+ super.announceForAccessibility(text)
+ announcement = text
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/view/MenuTest.kt b/core/ktx/src/androidTest/java/androidx/core/view/MenuTest.kt
new file mode 100644
index 0000000..313ed1b
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/view/MenuTest.kt
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.view
+
+import android.support.test.InstrumentationRegistry
+import android.view.Menu.NONE
+import android.view.MenuItem
+import android.widget.Toolbar
+import androidx.testutils.assertThrows
+import androidx.testutils.fail
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class MenuTest {
+ private val menu = Toolbar(InstrumentationRegistry.getContext()).menu
+
+ @Test fun get() {
+ val item = menu.add("")
+ assertSame(item, menu[0])
+ }
+
+ @Test fun contains() {
+ val item1 = menu.add("")
+ assertTrue(item1 in menu)
+
+ val item2 = menu.add("")
+ assertTrue(item2 in menu)
+ }
+
+ @Test fun minusAssign() {
+ val item1 = menu.add(NONE, 1, NONE, "")
+ val item2 = menu.add(NONE, 2, NONE, "")
+
+ assertEquals(2, menu.size)
+
+ menu -= item2
+ assertEquals(1, menu.size)
+ assertSame(item1, menu.getItem(0))
+
+ menu -= item1
+ assertEquals(0, menu.size)
+ }
+
+ @Test fun size() {
+ assertEquals(0, menu.size)
+
+ menu.add("")
+ assertEquals(1, menu.size)
+
+ menu.add(NONE, 123, NONE, "")
+ assertEquals(2, menu.size)
+
+ menu.removeItem(123)
+ assertEquals(1, menu.size)
+ }
+
+ @Test fun isEmpty() {
+ assertTrue(menu.isEmpty())
+ menu.add("")
+ assertFalse(menu.isEmpty())
+ }
+
+ @Test fun isNotEmpty() {
+ assertFalse(menu.isNotEmpty())
+ menu.add("")
+ assertTrue(menu.isNotEmpty())
+ }
+
+ @Test fun forEach() {
+ menu.forEach {
+ fail("Empty menu should not invoke lambda")
+ }
+
+ val item1 = menu.add("")
+ val item2 = menu.add("")
+
+ val items = mutableListOf<MenuItem>()
+ menu.forEach {
+ items += it
+ }
+ assertThat(items).containsExactly(item1, item2)
+ }
+
+ @Test fun forEachIndexed() {
+ menu.forEachIndexed { _, _ ->
+ fail("Empty menu should not invoke lambda")
+ }
+
+ val item1 = menu.add("")
+ val item2 = menu.add("")
+
+ val items = mutableListOf<MenuItem>()
+ menu.forEachIndexed { index, item ->
+ assertEquals(index, items.size)
+ items += item
+ }
+ assertThat(items).containsExactly(item1, item2)
+ }
+
+ @Test fun iterator() {
+ val item1 = menu.add("")
+ val item2 = menu.add("")
+
+ val iterator = menu.iterator()
+ assertTrue(iterator.hasNext())
+ assertSame(item1, iterator.next())
+ assertTrue(iterator.hasNext())
+ assertSame(item2, iterator.next())
+ assertFalse(iterator.hasNext())
+ assertThrows<IndexOutOfBoundsException> {
+ iterator.next()
+ }
+ }
+
+ @Test fun iteratorRemoving() {
+ val item1 = menu.add("")
+ val item2 = menu.add("")
+
+ val iterator = menu.iterator()
+
+ assertSame(item1, iterator.next())
+ iterator.remove()
+ assertFalse(item1 in menu)
+ assertEquals(1, menu.size())
+
+ assertSame(item2, iterator.next())
+ iterator.remove()
+ assertFalse(item2 in menu)
+ assertEquals(0, menu.size())
+ }
+
+ @Test fun children() {
+ val items = listOf(
+ menu.add(NONE, 1, NONE, ""),
+ menu.add(NONE, 2, NONE, ""),
+ menu.add(NONE, 3, NONE, "")
+ )
+
+ menu.children.forEachIndexed { index, child ->
+ assertSame(items[index], child)
+ }
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/view/ViewGroupTest.kt b/core/ktx/src/androidTest/java/androidx/core/view/ViewGroupTest.kt
new file mode 100644
index 0000000..7ec791d
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/view/ViewGroupTest.kt
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.view
+
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SdkSuppress
+import android.view.View
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import androidx.testutils.assertThrows
+import androidx.testutils.fail
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class ViewGroupTest {
+ private val context = InstrumentationRegistry.getContext()
+ private val viewGroup = LinearLayout(context)
+
+ @Test fun get() {
+ val view1 = View(context)
+ viewGroup.addView(view1)
+ val view2 = View(context)
+ viewGroup.addView(view2)
+
+ assertSame(view1, viewGroup[0])
+ assertSame(view2, viewGroup[1])
+
+ assertThrows<IndexOutOfBoundsException> {
+ viewGroup[-1]
+ }.hasMessageThat().isEqualTo("Index: -1, Size: 2")
+
+ assertThrows<IndexOutOfBoundsException> {
+ viewGroup[2]
+ }.hasMessageThat().isEqualTo("Index: 2, Size: 2")
+ }
+
+ @Test fun contains() {
+ val view1 = View(context)
+ viewGroup.addView(view1)
+ assertTrue(view1 in viewGroup)
+ assertFalse(view1 !in viewGroup)
+
+ val view2 = View(context)
+ assertFalse(view2 in viewGroup)
+ assertTrue(view2 !in viewGroup)
+ }
+
+ @Test fun plusAssign() {
+ assertEquals(0, viewGroup.childCount)
+
+ val view1 = View(context)
+ viewGroup += view1
+ assertEquals(1, viewGroup.childCount)
+ assertSame(view1, viewGroup.getChildAt(0))
+
+ val view2 = View(context)
+ viewGroup += view2
+ assertEquals(2, viewGroup.childCount)
+ assertSame(view2, viewGroup.getChildAt(1))
+ }
+
+ @Test fun minusAssign() {
+ val view1 = View(context)
+ viewGroup.addView(view1)
+ val view2 = View(context)
+ viewGroup.addView(view2)
+
+ assertEquals(2, viewGroup.childCount)
+
+ viewGroup -= view2
+ assertEquals(1, viewGroup.childCount)
+ assertSame(view1, viewGroup.getChildAt(0))
+
+ viewGroup -= view1
+ assertEquals(0, viewGroup.childCount)
+ }
+
+ @Test fun size() {
+ assertEquals(0, viewGroup.size)
+
+ viewGroup.addView(View(context))
+ assertEquals(1, viewGroup.size)
+
+ viewGroup.addView(View(context))
+ assertEquals(2, viewGroup.size)
+
+ viewGroup.removeViewAt(0)
+ assertEquals(1, viewGroup.size)
+ }
+
+ @Test fun isEmpty() {
+ assertTrue(viewGroup.isEmpty())
+ viewGroup.addView(View(context))
+ assertFalse(viewGroup.isEmpty())
+ }
+
+ @Test fun isNotEmpty() {
+ assertFalse(viewGroup.isNotEmpty())
+ viewGroup.addView(View(context))
+ assertTrue(viewGroup.isNotEmpty())
+ }
+
+ @Test fun forEach() {
+ viewGroup.forEach {
+ fail("Empty view group should not invoke lambda")
+ }
+
+ val view1 = View(context)
+ viewGroup.addView(view1)
+ val view2 = View(context)
+ viewGroup.addView(view2)
+
+ val views = mutableListOf<View>()
+ viewGroup.forEach {
+ views += it
+ }
+ assertThat(views).containsExactly(view1, view2)
+ }
+
+ @Test fun forEachIndexed() {
+ viewGroup.forEachIndexed { _, _ ->
+ fail("Empty view group should not invoke lambda")
+ }
+
+ val view1 = View(context)
+ viewGroup.addView(view1)
+ val view2 = View(context)
+ viewGroup.addView(view2)
+
+ val views = mutableListOf<View>()
+ viewGroup.forEachIndexed { index, view ->
+ assertEquals(index, views.size)
+ views += view
+ }
+ assertThat(views).containsExactly(view1, view2)
+ }
+
+ @Test fun iterator() {
+ val view1 = View(context)
+ viewGroup.addView(view1)
+ val view2 = View(context)
+ viewGroup.addView(view2)
+
+ val iterator = viewGroup.iterator()
+ assertTrue(iterator.hasNext())
+ assertSame(view1, iterator.next())
+ assertTrue(iterator.hasNext())
+ assertSame(view2, iterator.next())
+ assertFalse(iterator.hasNext())
+ assertThrows<IndexOutOfBoundsException> {
+ iterator.next()
+ }
+ }
+
+ @Test fun iteratorRemoving() {
+ val view1 = View(context)
+ viewGroup.addView(view1)
+ val view2 = View(context)
+ viewGroup.addView(view2)
+
+ val iterator = viewGroup.iterator()
+
+ assertSame(view1, iterator.next())
+ iterator.remove()
+ assertFalse(view1 in viewGroup)
+ assertEquals(1, viewGroup.childCount)
+
+ assertSame(view2, iterator.next())
+ iterator.remove()
+ assertFalse(view2 in viewGroup)
+ assertEquals(0, viewGroup.childCount)
+ }
+
+ @Test fun iteratorForEach() {
+ val views = listOf(View(context), View(context))
+ views.forEach(viewGroup::addView)
+
+ var index = 0
+ for (view in viewGroup) {
+ assertSame(views[index++], view)
+ }
+ }
+
+ @Test fun children() {
+ val views = listOf(View(context), View(context), View(context))
+ views.forEach { viewGroup.addView(it) }
+
+ viewGroup.children.forEachIndexed { index, child ->
+ assertSame(views[index], child)
+ }
+ }
+
+ @Test fun setMargins() {
+ val layoutParams = ViewGroup.MarginLayoutParams(100, 200)
+ layoutParams.setMargins(42)
+ assertEquals(42, layoutParams.leftMargin)
+ assertEquals(42, layoutParams.topMargin)
+ assertEquals(42, layoutParams.rightMargin)
+ assertEquals(42, layoutParams.bottomMargin)
+ }
+
+ @Test fun updateMargins() {
+ val layoutParams = ViewGroup.MarginLayoutParams(100, 200)
+ layoutParams.updateMargins(top = 10, right = 20)
+ assertEquals(0, layoutParams.leftMargin)
+ assertEquals(10, layoutParams.topMargin)
+ assertEquals(20, layoutParams.rightMargin)
+ assertEquals(0, layoutParams.bottomMargin)
+ }
+
+ @Test fun updateMarginsNoOp() {
+ val layoutParams = ViewGroup.MarginLayoutParams(100, 200)
+ layoutParams.setMargins(10, 20, 30, 40)
+ layoutParams.updateMargins()
+ assertEquals(10, layoutParams.leftMargin)
+ assertEquals(20, layoutParams.topMargin)
+ assertEquals(30, layoutParams.rightMargin)
+ assertEquals(40, layoutParams.bottomMargin)
+ }
+
+ @SdkSuppress(minSdkVersion = 17)
+ @Test fun updateMarginsRelative() {
+ val layoutParams = ViewGroup.MarginLayoutParams(100, 200)
+ layoutParams.updateMarginsRelative(start = 10, end = 20)
+ assertEquals(0, layoutParams.leftMargin)
+ assertEquals(0, layoutParams.topMargin)
+ assertEquals(0, layoutParams.rightMargin)
+ assertEquals(0, layoutParams.bottomMargin)
+ assertEquals(10, layoutParams.marginStart)
+ assertEquals(20, layoutParams.marginEnd)
+ assertTrue(layoutParams.isMarginRelative)
+ }
+
+ @SdkSuppress(minSdkVersion = 17)
+ @Test fun updateMarginsRelativeNoOp() {
+ val layoutParams = ViewGroup.MarginLayoutParams(100, 200)
+ layoutParams.setMargins(10, 20, 30, 40)
+ layoutParams.updateMarginsRelative()
+ assertEquals(10, layoutParams.leftMargin)
+ assertEquals(20, layoutParams.topMargin)
+ assertEquals(30, layoutParams.rightMargin)
+ assertEquals(40, layoutParams.bottomMargin)
+ assertEquals(10, layoutParams.marginStart)
+ assertEquals(30, layoutParams.marginEnd)
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/view/ViewTest.kt b/core/ktx/src/androidTest/java/androidx/core/view/ViewTest.kt
new file mode 100644
index 0000000..1ccd517
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/view/ViewTest.kt
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.view
+
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.support.test.InstrumentationRegistry
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import android.widget.RelativeLayout
+import androidx.core.ktx.test.R
+import androidx.testutils.assertThrows
+import androidx.testutils.fail
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class ViewTest {
+ private val context = InstrumentationRegistry.getContext()
+ private val view = View(context)
+
+ @Test
+ fun doOnNextLayout() {
+ var calls = 0
+ view.doOnNextLayout {
+ calls++
+ }
+ view.layout(0, 0, 10, 10)
+ assertEquals(1, calls)
+
+ // Now layout again and make sure that the listener was removed
+ view.layout(0, 0, 10, 10)
+ assertEquals(1, calls)
+ }
+
+ @Test
+ fun doOnLayoutBeforeLayout() {
+ var called = false
+ view.doOnLayout {
+ called = true
+ }
+ view.layout(0, 0, 10, 10)
+ assertTrue(called)
+ }
+
+ @Test
+ fun doOnLayoutAfterLayout() {
+ view.layout(0, 0, 10, 10)
+
+ var called = false
+ view.doOnLayout {
+ called = true
+ }
+ assertTrue(called)
+ }
+
+ @Test
+ fun doOnLayoutWhileLayoutRequested() {
+ // First layout the view
+ view.layout(0, 0, 10, 10)
+ // Then later a layout is requested
+ view.requestLayout()
+
+ var called = false
+ view.doOnLayout {
+ called = true
+ }
+
+ // Assert that we haven't been called while the layout pass is pending
+ assertFalse(called)
+
+ // Now layout the view and assert that we're called
+ view.layout(0, 0, 20, 20)
+ assertTrue(called)
+ }
+
+ @Test
+ fun doOnPreDraw() {
+ var calls = 0
+ view.doOnPreDraw {
+ calls++
+ }
+ view.viewTreeObserver.dispatchOnPreDraw()
+ assertEquals(1, calls)
+
+ // Now dispatch again to make sure that the listener was removed
+ view.viewTreeObserver.dispatchOnPreDraw()
+ assertEquals(1, calls)
+ }
+
+ @Test
+ fun setPadding() {
+ view.setPadding(42)
+ assertEquals(42, view.paddingLeft)
+ assertEquals(42, view.paddingTop)
+ assertEquals(42, view.paddingRight)
+ assertEquals(42, view.paddingBottom)
+ }
+
+ @Test
+ fun updatePadding() {
+ view.updatePadding(top = 10, right = 20)
+ assertEquals(0, view.paddingLeft)
+ assertEquals(10, view.paddingTop)
+ assertEquals(20, view.paddingRight)
+ assertEquals(0, view.paddingBottom)
+ }
+
+ @Test
+ fun updatePaddingNoOp() {
+ view.setPadding(10, 20, 30, 40)
+ view.updatePadding()
+ assertEquals(10, view.paddingLeft)
+ assertEquals(20, view.paddingTop)
+ assertEquals(30, view.paddingRight)
+ assertEquals(40, view.paddingBottom)
+ }
+
+ @Test
+ fun updatePaddingRelative() {
+ view.updatePaddingRelative(start = 10, end = 20)
+ assertEquals(10, view.paddingStart)
+ assertEquals(0, view.paddingTop)
+ assertEquals(20, view.paddingEnd)
+ assertEquals(0, view.paddingBottom)
+ }
+
+ @Test
+ fun updatePaddingRelativeNoOp() {
+ view.setPaddingRelative(10, 20, 30, 40)
+ view.updatePaddingRelative()
+ assertEquals(10, view.paddingStart)
+ assertEquals(20, view.paddingTop)
+ assertEquals(30, view.paddingEnd)
+ assertEquals(40, view.paddingBottom)
+ }
+
+ @Test
+ fun toBitmapBeforeLayout() {
+ assertThrows<IllegalStateException> {
+ view.toBitmap()
+ }
+ }
+
+ @Test
+ fun toBitmap() {
+ view.layout(0, 0, 100, 100)
+ val bitmap = view.toBitmap()
+
+ assertEquals(100, bitmap.width)
+ assertEquals(100, bitmap.height)
+ }
+
+ @Test
+ fun toBitmapCustomConfig() {
+ view.layout(0, 0, 100, 100)
+ val bitmap = view.toBitmap(Bitmap.Config.RGB_565)
+
+ assertSame(Bitmap.Config.RGB_565, bitmap.config)
+ }
+
+ @Test
+ fun toBitmapScrolls() {
+ val scrollView = LayoutInflater.from(context)!!
+ .inflate(R.layout.test_bitmap_scrolls, null, false)
+
+ val size = 100
+
+ scrollView.measure(
+ View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.EXACTLY),
+ View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.EXACTLY))
+ scrollView.layout(0, 0, size, size)
+
+ val noScroll = scrollView.toBitmap()
+ assertEquals(Color.WHITE, noScroll.getPixel(0, 0))
+ assertEquals(Color.WHITE, noScroll.getPixel(size - 1, size - 1))
+
+ scrollView.scrollTo(0, size)
+ val scrolls = scrollView.toBitmap()
+
+ assertEquals(Color.BLACK, scrolls.getPixel(0, 0))
+ assertEquals(Color.BLACK, scrolls.getPixel(size - 1, size - 1))
+ }
+
+ @Test fun isVisible() {
+ view.isVisible = true
+ assertTrue(view.isVisible)
+ assertEquals(View.VISIBLE, view.visibility)
+
+ view.isVisible = false
+ assertFalse(view.isVisible)
+ assertEquals(View.GONE, view.visibility)
+ }
+
+ @Test fun isInvisible() {
+ view.isInvisible = true
+ assertTrue(view.isInvisible)
+ assertEquals(View.INVISIBLE, view.visibility)
+
+ view.isInvisible = false
+ assertFalse(view.isInvisible)
+ assertEquals(View.VISIBLE, view.visibility)
+ }
+
+ @Test fun isGone() {
+ view.isGone = true
+ assertTrue(view.isGone)
+ assertEquals(View.GONE, view.visibility)
+
+ view.isGone = false
+ assertFalse(view.isGone)
+ assertEquals(View.VISIBLE, view.visibility)
+ }
+
+ @Test fun updateLayoutParams() {
+ view.layoutParams = ViewGroup.LayoutParams(0, 0)
+ view.updateLayoutParams {
+ assertSame(view.layoutParams, this)
+
+ width = 500
+ height = 1000
+ }
+
+ assertEquals(500, view.layoutParams.width)
+ assertEquals(1000, view.layoutParams.height)
+ }
+
+ @Test fun updateLayoutParamsAsType() {
+ view.layoutParams = LinearLayout.LayoutParams(0, 0)
+ view.updateLayoutParams<LinearLayout.LayoutParams> {
+ assertSame(view.layoutParams, this)
+
+ weight = 2f
+ }
+
+ assertEquals(2f, (view.layoutParams as LinearLayout.LayoutParams).weight)
+ }
+
+ @Test fun updateLayoutParamsWrongType() {
+ assertThrows<ClassCastException> {
+ view.updateLayoutParams<RelativeLayout.LayoutParams> {
+ fail()
+ }
+ }
+ }
+
+ @Test fun announceForAccessibility() {
+ val testView = AccessibilityAnnouncementCapturingView(context)
+
+ testView.announceForAccessibility(R.string.text)
+
+ val resolvedText = context.getText(R.string.text)
+ assertEquals(testView.announcement, resolvedText)
+ }
+}
diff --git a/core/ktx/src/androidTest/java/androidx/core/widget/ToastTest.kt b/core/ktx/src/androidTest/java/androidx/core/widget/ToastTest.kt
new file mode 100644
index 0000000..f8e9931
--- /dev/null
+++ b/core/ktx/src/androidTest/java/androidx/core/widget/ToastTest.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.widget
+
+import android.support.test.InstrumentationRegistry
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import android.widget.Toast
+import androidx.core.ktx.test.R
+import androidx.core.view.forEach
+import androidx.core.view.isVisible
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class ToastTest {
+ private val context = InstrumentationRegistry.getContext()
+ private val instrumentation = InstrumentationRegistry.getInstrumentation()
+
+ @Test
+ fun createToastWithTextShort() = instrumentation.runOnMainSync {
+ val toast = context.toast("Short Toast")
+ assertEquals(Toast.LENGTH_SHORT, toast.duration)
+ assertTrue(containsText(toast.view, "Short Toast"))
+ assertTrue(toast.view.isVisible)
+ }
+
+ @Test
+ fun createToastWithTextLong() = instrumentation.runOnMainSync {
+ val toast = context.toast("Long Toast", Toast.LENGTH_LONG)
+ assertEquals(Toast.LENGTH_LONG, toast.duration)
+ assertTrue(containsText(toast.view, "Long Toast"))
+ assertTrue(toast.view.isVisible)
+ }
+
+ @Test
+ fun createToastWithResIdShort() = instrumentation.runOnMainSync {
+ val toast = context.toast(R.string.text)
+ assertEquals(Toast.LENGTH_SHORT, toast.duration)
+ assertTrue(containsText(toast.view, context.getString(R.string.text)))
+ assertTrue(toast.view.isVisible)
+ }
+
+ @Test
+ fun createToastWithResIdLong() = instrumentation.runOnMainSync {
+ val toast = context.toast(R.string.text, Toast.LENGTH_LONG)
+ assertEquals(Toast.LENGTH_LONG, toast.duration)
+ assertTrue(containsText(toast.view, context.getString(R.string.text)))
+ assertTrue(toast.view.isVisible)
+ }
+
+ private fun containsText(view: View, text: String): Boolean {
+ if (view is TextView && view.text == text) {
+ return true
+ }
+ if (view is ViewGroup) {
+ view.forEach {
+ if (containsText(it, text)) {
+ return true
+ }
+ }
+ }
+ return false
+ }
+}
diff --git a/core/ktx/src/androidTest/res/drawable/box.xml b/core/ktx/src/androidTest/res/drawable/box.xml
new file mode 100644
index 0000000..3915878
--- /dev/null
+++ b/core/ktx/src/androidTest/res/drawable/box.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2018 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <size android:height="10px" android:width="10px"/>
+ <solid android:color="#fff"/>
+</shape>
diff --git a/core/ktx/src/androidTest/res/font/inconsolata_regular.ttf b/core/ktx/src/androidTest/res/font/inconsolata_regular.ttf
new file mode 100644
index 0000000..fc981ce
--- /dev/null
+++ b/core/ktx/src/androidTest/res/font/inconsolata_regular.ttf
Binary files differ
diff --git a/core/ktx/src/androidTest/res/layout/test_activity.xml b/core/ktx/src/androidTest/res/layout/test_activity.xml
new file mode 100644
index 0000000..a477350
--- /dev/null
+++ b/core/ktx/src/androidTest/res/layout/test_activity.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/root"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ImageView android:id="@+id/image_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@android:drawable/ic_media_play"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/core/ktx/src/androidTest/res/layout/test_attrs.xml b/core/ktx/src/androidTest/res/layout/test_attrs.xml
new file mode 100644
index 0000000..d5975c5
--- /dev/null
+++ b/core/ktx/src/androidTest/res/layout/test_attrs.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2018 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/root"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:sample="42" />
diff --git a/core/ktx/src/androidTest/res/layout/test_bitmap_scrolls.xml b/core/ktx/src/androidTest/res/layout/test_bitmap_scrolls.xml
new file mode 100644
index 0000000..c2cad5c
--- /dev/null
+++ b/core/ktx/src/androidTest/res/layout/test_bitmap_scrolls.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2018 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="100px"
+ android:layout_height="100px"
+ android:scrollbars="none"
+ >
+
+ <LinearLayout
+ android:layout_width="100px"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="100px"
+ android:background="@android:color/white" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="100px"
+ android:background="@android:color/black" />
+
+ </LinearLayout>
+
+</ScrollView>
\ No newline at end of file
diff --git a/core/ktx/src/androidTest/res/layout/typed_array.xml b/core/ktx/src/androidTest/res/layout/typed_array.xml
new file mode 100644
index 0000000..e485ffd
--- /dev/null
+++ b/core/ktx/src/androidTest/res/layout/typed_array.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2018 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<View 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="match_parent"
+ app:boolean_present="true"
+ app:color_present="#fff"
+ app:dimension_present="1px"
+ app:drawable_present="@drawable/box"
+ app:string_present="Hello"
+ app:float_present="0.1"
+ app:integer_present="1"
+ app:resource_present="@font/inconsolata_regular"
+ app:font_present="@font/inconsolata_regular"
+ app:text_array_present="@array/text_array"
+/>
diff --git a/core/ktx/src/androidTest/res/values/attrs.xml b/core/ktx/src/androidTest/res/values/attrs.xml
new file mode 100644
index 0000000..e3f9723
--- /dev/null
+++ b/core/ktx/src/androidTest/res/values/attrs.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2018 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources>
+ <declare-styleable name="SampleAttrs">
+ <attr name="sample" format="integer" />
+ </declare-styleable>
+</resources>
diff --git a/core/ktx/src/androidTest/res/values/strings.xml b/core/ktx/src/androidTest/res/values/strings.xml
new file mode 100644
index 0000000..20877e9
--- /dev/null
+++ b/core/ktx/src/androidTest/res/values/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2018 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources>
+ <string name="text">Hello World</string>
+ <string-array name="text_array">
+ <item>Hello</item>
+ <item>World</item>
+ </string-array>
+</resources>
diff --git a/core/ktx/src/androidTest/res/values/styles.xml b/core/ktx/src/androidTest/res/values/styles.xml
new file mode 100644
index 0000000..ea35e47
--- /dev/null
+++ b/core/ktx/src/androidTest/res/values/styles.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2018 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources>
+ <declare-styleable name="TypedArrayTypes">
+ <attr name="boolean_present" format="boolean"/>
+ <attr name="boolean_absent" format="boolean"/>
+
+ <attr name="color_present" format="color"/>
+ <attr name="color_absent" format="color"/>
+
+ <attr name="dimension_present" format="dimension"/>
+ <attr name="dimension_absent" format="dimension"/>
+
+ <attr name="drawable_present" format="reference"/>
+ <attr name="drawable_absent" format="reference"/>
+
+ <attr name="float_present" format="float"/>
+ <attr name="float_absent" format="float"/>
+
+ <attr name="font_present" format="reference"/>
+ <attr name="font_absent" format="reference"/>
+
+ <attr name="integer_present" format="integer"/>
+ <attr name="integer_absent" format="integer"/>
+
+ <attr name="resource_present" format="reference"/>
+ <attr name="resource_absent" format="reference"/>
+
+ <attr name="string_present" format="string"/>
+ <attr name="string_absent" format="string"/>
+
+ <attr name="text_array_present" format="reference"/>
+ <attr name="text_array_absent" format="reference"/>
+ </declare-styleable>
+</resources>
diff --git a/core/ktx/src/androidTest/res/xml/preferences.xml b/core/ktx/src/androidTest/res/xml/preferences.xml
new file mode 100644
index 0000000..33629e1
--- /dev/null
+++ b/core/ktx/src/androidTest/res/xml/preferences.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2018 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<PreferenceScreen/>
\ No newline at end of file
diff --git a/core/ktx/src/main/AndroidManifest.xml b/core/ktx/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..7f93e5e
--- /dev/null
+++ b/core/ktx/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest package="androidx.core.ktx"/>
diff --git a/core/ktx/src/main/java/androidx/core/animation/Animator.kt b/core/ktx/src/main/java/androidx/core/animation/Animator.kt
new file mode 100644
index 0000000..1438bcd
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/animation/Animator.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.animation
+
+import android.animation.Animator
+import androidx.annotation.RequiresApi
+
+/**
+ * Add an action which will be invoked when the animation has ended.
+ *
+ * @return the [Animator.AnimatorListener] added to the Animator
+ * @see Animator.end
+ */
+fun Animator.doOnEnd(action: (animator: Animator) -> Unit) = addListener(onEnd = action)
+
+/**
+ * Add an action which will be invoked when the animation has started.
+ *
+ * @return the [Animator.AnimatorListener] added to the Animator
+ * @see Animator.start
+ */
+fun Animator.doOnStart(action: (animator: Animator) -> Unit) = addListener(onStart = action)
+
+/**
+ * Add an action which will be invoked when the animation has been cancelled.
+ *
+ * @return the [Animator.AnimatorListener] added to the Animator
+ * @see Animator.cancel
+ */
+fun Animator.doOnCancel(action: (animator: Animator) -> Unit) = addListener(onCancel = action)
+
+/**
+ * Add an action which will be invoked when the animation has repeated.
+ * @return the [Animator.AnimatorListener] added to the Animator
+ */
+fun Animator.doOnRepeat(action: (animator: Animator) -> Unit) = addListener(onRepeat = action)
+
+/**
+ * Add an action which will be invoked when the animation has resumed after a pause.
+ *
+ * @return the [Animator.AnimatorPauseListener] added to the Animator
+ * @see Animator.resume
+ */
+@RequiresApi(19)
+fun Animator.doOnResume(action: (animator: Animator) -> Unit) = addPauseListener(onResume = action)
+
+/**
+ * Add an action which will be invoked when the animation has been paused.
+ *
+ * @return the [Animator.AnimatorPauseListener] added to the Animator
+ * @see Animator.pause
+ */
+@RequiresApi(19)
+fun Animator.doOnPause(action: (animator: Animator) -> Unit) = addPauseListener(onPause = action)
+
+/**
+ * Add a listener to this Animator using the provided actions.
+ */
+fun Animator.addListener(
+ onEnd: ((animator: Animator) -> Unit)? = null,
+ onStart: ((animator: Animator) -> Unit)? = null,
+ onCancel: ((animator: Animator) -> Unit)? = null,
+ onRepeat: ((animator: Animator) -> Unit)? = null
+): Animator.AnimatorListener {
+ val listener = object : Animator.AnimatorListener {
+ override fun onAnimationRepeat(animator: Animator) {
+ onRepeat?.invoke(animator)
+ }
+
+ override fun onAnimationEnd(animator: Animator) {
+ onEnd?.invoke(animator)
+ }
+
+ override fun onAnimationCancel(animator: Animator) {
+ onCancel?.invoke(animator)
+ }
+
+ override fun onAnimationStart(animator: Animator) {
+ onStart?.invoke(animator)
+ }
+ }
+ addListener(listener)
+ return listener
+}
+
+/**
+ * Add a pause and resume listener to this Animator using the provided actions.
+ */
+@RequiresApi(19)
+fun Animator.addPauseListener(
+ onResume: ((animator: Animator) -> Unit)? = null,
+ onPause: ((animator: Animator) -> Unit)? = null
+): Animator.AnimatorPauseListener {
+ val listener = object : Animator.AnimatorPauseListener {
+ override fun onAnimationPause(animator: Animator) {
+ onPause?.invoke(animator)
+ }
+
+ override fun onAnimationResume(animator: Animator) {
+ onResume?.invoke(animator)
+ }
+ }
+ addPauseListener(listener)
+ return listener
+}
diff --git a/core/ktx/src/main/java/androidx/core/content/ContentValues.kt b/core/ktx/src/main/java/androidx/core/content/ContentValues.kt
new file mode 100644
index 0000000..e1a0d01
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/content/ContentValues.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.content
+
+import android.content.ContentValues
+
+/**
+ * Returns a new [ContentValues] with the given key/value pairs as elements.
+ *
+ * @throws IllegalArgumentException When a value is not a supported type of [ContentValues].
+ */
+fun contentValuesOf(vararg pairs: Pair<String, Any?>) = ContentValues(pairs.size).apply {
+ for ((key, value) in pairs) {
+ when (value) {
+ null -> putNull(key)
+ is String -> put(key, value)
+ is Int -> put(key, value)
+ is Long -> put(key, value)
+ is Boolean -> put(key, value)
+ is Float -> put(key, value)
+ is Double -> put(key, value)
+ is ByteArray -> put(key, value)
+ is Byte -> put(key, value)
+ is Short -> put(key, value)
+ else -> {
+ val valueType = value.javaClass.canonicalName
+ throw IllegalArgumentException("Illegal value type $valueType for key \"$key\"")
+ }
+ }
+ }
+}
diff --git a/core/ktx/src/main/java/androidx/core/content/Context.kt b/core/ktx/src/main/java/androidx/core/content/Context.kt
new file mode 100644
index 0000000..2ffb17b
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/content/Context.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.content
+
+import android.content.Context
+import android.content.res.TypedArray
+import android.util.AttributeSet
+import androidx.annotation.AttrRes
+import androidx.annotation.RequiresApi
+import androidx.annotation.StyleRes
+
+/**
+ * Return the handle to a system-level service by class.
+ *
+ * The return type of this function intentionally uses a
+ * [platform type](https://kotlinlang.org/docs/reference/java-interop.html#null-safety-and-platform-types)
+ * to allow callers to decide whether they require a service be present or can tolerate its absence.
+ *
+ * @see Context.getSystemService(Class)
+ */
+@RequiresApi(23)
+@Suppress("HasPlatformType") // Intentionally propagating platform type with unknown nullability.
+inline fun <reified T> Context.systemService() = getSystemService(T::class.java)
+
+/**
+ * Executes [block] on a [TypedArray] receiver. The [TypedArray] holds the attribute
+ * values in [set] that are listed in [attrs]. In addition, if the given [AttributeSet]
+ * specifies a style class (through the `style` attribute), that style will be applied
+ * on top of the base attributes it defines.
+ *
+ * @param set The base set of attribute values.
+ * @param attrs The desired attributes to be retrieved. These attribute IDs must be
+ * sorted in ascending order.
+ * @param defStyleAttr An attribute in the current theme that contains a reference to
+ * a style resource that supplies defaults values for the [TypedArray].
+ * Can be 0 to not look for defaults.
+ * @param defStyleRes A resource identifier of a style resource that supplies default values
+ * for the [TypedArray], used only if [defStyleAttr] is 0 or can not be found
+ * in the theme. Can be 0 to not look for defaults.
+ *
+ * @see Context.obtainStyledAttributes
+ * @see android.content.res.Resources.Theme.obtainStyledAttributes
+ */
+inline fun Context.withStyledAttributes(
+ set: AttributeSet? = null,
+ attrs: IntArray,
+ @AttrRes defStyleAttr: Int = 0,
+ @StyleRes defStyleRes: Int = 0,
+ block: TypedArray.() -> Unit
+) {
+ val typedArray = obtainStyledAttributes(set, attrs, defStyleAttr, defStyleRes)
+ try {
+ typedArray.block()
+ } finally {
+ typedArray.recycle()
+ }
+}
+
+/**
+ * Executes [block] on a [TypedArray] receiver. The [TypedArray] holds the the values
+ * defined by the style resource [resourceId] which are listed in [attrs].
+ *
+ * @param attrs The desired attributes. These attribute IDs must be sorted in ascending order.
+ *
+ * @see Context.obtainStyledAttributes
+ * @see android.content.res.Resources.Theme.obtainStyledAttributes
+ */
+inline fun Context.withStyledAttributes(
+ @StyleRes resourceId: Int,
+ attrs: IntArray,
+ block: TypedArray.() -> Unit
+) {
+ val typedArray = obtainStyledAttributes(resourceId, attrs)
+ try {
+ typedArray.block()
+ } finally {
+ typedArray.recycle()
+ }
+}
diff --git a/core/ktx/src/main/java/androidx/core/content/SharedPreferences.kt b/core/ktx/src/main/java/androidx/core/content/SharedPreferences.kt
new file mode 100644
index 0000000..c4677ea
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/content/SharedPreferences.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.content
+
+import android.annotation.SuppressLint
+import android.content.SharedPreferences
+
+/**
+ * Allows editing of this preference instance with a call to [apply][SharedPreferences.Editor.apply]
+ * or [commit][SharedPreferences.Editor.commit] to persist the changes.
+ * Default behaviour is [apply][SharedPreferences.Editor.apply].
+ * ```
+ * prefs.edit {
+ * putString("key", value)
+ * }
+ * ```
+ * To [commit][SharedPreferences.Editor.commit] changes:
+ * ```
+ * prefs.edit(commit = true) {
+ * putString("key", value)
+ * }
+ * ```
+ */
+@SuppressLint("ApplySharedPref")
+inline fun SharedPreferences.edit(
+ commit: Boolean = false,
+ action: SharedPreferences.Editor.() -> Unit
+) {
+ val editor = edit()
+ action(editor)
+ if (commit) {
+ editor.commit()
+ } else {
+ editor.apply()
+ }
+}
diff --git a/core/ktx/src/main/java/androidx/core/content/res/TypedArray.kt b/core/ktx/src/main/java/androidx/core/content/res/TypedArray.kt
new file mode 100644
index 0000000..a4f6fef
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/content/res/TypedArray.kt
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.content.res
+
+import android.content.res.ColorStateList
+import android.content.res.TypedArray
+import android.graphics.Typeface
+import android.graphics.drawable.Drawable
+import androidx.annotation.AnyRes
+import androidx.annotation.ColorInt
+import androidx.annotation.Dimension
+import androidx.annotation.RequiresApi
+import androidx.annotation.StyleableRes
+
+private fun TypedArray.checkAttribute(@StyleableRes index: Int) {
+ if (!hasValue(index)) {
+ throw IllegalArgumentException("Attribute not defined in set.")
+ }
+}
+
+/**
+ * Retrieve the boolean value for the attribute at [index] or throws [IllegalArgumentException]
+ * if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getBoolean
+ */
+fun TypedArray.getBooleanOrThrow(@StyleableRes index: Int): Boolean {
+ checkAttribute(index)
+ return getBoolean(index, false)
+}
+
+/**
+ * Retrieve the color value for the attribute at [index] or throws [IllegalArgumentException]
+ * if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getColor
+ */
+@ColorInt
+fun TypedArray.getColorOrThrow(@StyleableRes index: Int): Int {
+ checkAttribute(index)
+ return getColor(index, 0)
+}
+
+/**
+ * Retrieve the color state list value for the attribute at [index] or throws
+ * [IllegalArgumentException] if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getColorStateList
+ */
+fun TypedArray.getColorStateListOrThrow(@StyleableRes index: Int): ColorStateList {
+ checkAttribute(index)
+ return checkNotNull(getColorStateList(index)) {
+ "Attribute value was not a color or color state list."
+ }
+}
+
+/**
+ * Retrieve the dimension value for the attribute at [index] or throws [IllegalArgumentException]
+ * if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getDimension
+ */
+fun TypedArray.getDimensionOrThrow(@StyleableRes index: Int): Float {
+ checkAttribute(index)
+ return getDimension(index, 0f)
+}
+
+/**
+ * Retrieve the dimension pixel offset value for the attribute at [index] or throws
+ * [IllegalArgumentException] if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getDimensionPixelOffset
+ */
+@Dimension
+fun TypedArray.getDimensionPixelOffsetOrThrow(@StyleableRes index: Int): Int {
+ checkAttribute(index)
+ return getDimensionPixelOffset(index, 0)
+}
+
+/**
+ * Retrieve the dimension pixel size value for the attribute at [index] or throws
+ * [IllegalArgumentException] if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getDimensionPixelSize
+ */
+@Dimension
+fun TypedArray.getDimensionPixelSizeOrThrow(@StyleableRes index: Int): Int {
+ checkAttribute(index)
+ return getDimensionPixelSize(index, 0)
+}
+
+/**
+ * Retrieve the drawable value for the attribute at [index] or throws [IllegalArgumentException]
+ * if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getDrawable
+ */
+fun TypedArray.getDrawableOrThrow(@StyleableRes index: Int): Drawable {
+ checkAttribute(index)
+ return getDrawable(index)
+}
+
+/**
+ * Retrieve the float value for the attribute at [index] or throws [IllegalArgumentException]
+ * if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getFloat
+ */
+fun TypedArray.getFloatOrThrow(@StyleableRes index: Int): Float {
+ checkAttribute(index)
+ return getFloat(index, 0f)
+}
+
+/**
+ * Retrieve the font value for the attribute at [index] or throws [IllegalArgumentException]
+ * if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getFont
+ */
+@RequiresApi(26)
+fun TypedArray.getFontOrThrow(@StyleableRes index: Int): Typeface {
+ checkAttribute(index)
+ return getFont(index)
+}
+
+/**
+ * Retrieve the integer value for the attribute at [index] or throws [IllegalArgumentException]
+ * if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getInt
+ */
+fun TypedArray.getIntOrThrow(@StyleableRes index: Int): Int {
+ checkAttribute(index)
+ return getInt(index, 0)
+}
+
+/**
+ * Retrieve the integer value for the attribute at [index] or throws [IllegalArgumentException]
+ * if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getInteger
+ */
+fun TypedArray.getIntegerOrThrow(@StyleableRes index: Int): Int {
+ checkAttribute(index)
+ return getInteger(index, 0)
+}
+
+/**
+ * Retrieves the resource identifier for the attribute at [index] or throws
+ * [IllegalArgumentException] if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getResourceId
+ */
+@AnyRes
+fun TypedArray.getResourceIdOrThrow(@StyleableRes index: Int): Int {
+ checkAttribute(index)
+ return getResourceId(index, 0)
+}
+
+/**
+ * Retrieve the string value for the attribute at [index] or throws [IllegalArgumentException]
+ * if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getString
+ */
+fun TypedArray.getStringOrThrow(@StyleableRes index: Int): String {
+ checkAttribute(index)
+ return checkNotNull(getString(index)) {
+ "Attribute value could not be coerced to String."
+ }
+}
+
+/**
+ * Retrieve the text value for the attribute at [index] or throws [IllegalArgumentException]
+ * if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getText
+ */
+fun TypedArray.getTextOrThrow(@StyleableRes index: Int): CharSequence {
+ checkAttribute(index)
+ return checkNotNull(getText(index)) {
+ "Attribute value could not be coerced to CharSequence."
+ }
+}
+
+/**
+ * Retrieve the text array value for the attribute at [index] or throws
+ * [IllegalArgumentException] if not defined.
+ *
+ * @see TypedArray.hasValue
+ * @see TypedArray.getTextArray
+ */
+fun TypedArray.getTextArrayOrThrow(@StyleableRes index: Int): Array<CharSequence> {
+ checkAttribute(index)
+ return getTextArray(index)
+}
+
+/**
+ * Executes the given [block] function on this TypedArray and then recycles it.
+ *
+ * @see kotlin.io.use
+ */
+inline fun <R> TypedArray.use(block: (TypedArray) -> R): R {
+ return block(this).also {
+ recycle()
+ }
+}
diff --git a/core/ktx/src/main/java/androidx/core/database/Cursor.kt b/core/ktx/src/main/java/androidx/core/database/Cursor.kt
new file mode 100644
index 0000000..b63e719
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/database/Cursor.kt
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to other public API.
+
+package androidx.core.database
+
+import android.database.Cursor
+
+/**
+ * Returns the value of the requested column as a byte array.
+ *
+ * The result and whether this method throws an exception when the column value is null or the
+ * column type is not a blob type is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.getBlob
+ */
+inline fun Cursor.getBlob(columnName: String): ByteArray =
+ getBlob(getColumnIndexOrThrow(columnName))
+
+/**
+ * Returns the value of the requested column as a double.
+ *
+ * The result and whether this method throws an exception when the column value is null or the
+ * column type is not a floating-point type is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.getDouble
+ */
+inline fun Cursor.getDouble(columnName: String): Double =
+ getDouble(getColumnIndexOrThrow(columnName))
+
+/**
+ * Returns the value of the requested column as a float.
+ *
+ * The result and whether this method throws an exception when the column value is null or the
+ * column type is not a floating-point type is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.getFloat
+ */
+inline fun Cursor.getFloat(columnName: String): Float = getFloat(getColumnIndexOrThrow(columnName))
+
+/**
+ * Returns the value of the requested column as an integer.
+ *
+ * The result and whether this method throws an exception when the column value is null or the
+ * column type is not an integral type is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.getInt
+ */
+inline fun Cursor.getInt(columnName: String): Int = getInt(getColumnIndexOrThrow(columnName))
+
+/**
+ * Returns the value of the requested column as a long.
+ *
+ * The result and whether this method throws an exception when the column value is null or the
+ * column type is not an integral type is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.getLong
+ */
+inline fun Cursor.getLong(columnName: String): Long = getLong(getColumnIndexOrThrow(columnName))
+
+/**
+ * Returns the value of the requested column as a short.
+ *
+ * The result and whether this method throws an exception when the column value is null or the
+ * column type is not an integral type is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.getShort
+ */
+inline fun Cursor.getShort(columnName: String): Short = getShort(getColumnIndexOrThrow(columnName))
+
+/**
+ * Returns the value of the requested column as a string.
+ *
+ * The result and whether this method throws an exception when the column value is null or the
+ * column type is not a string type is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.getString
+ */
+inline fun Cursor.getString(columnName: String): String =
+ getString(getColumnIndexOrThrow(columnName))
+
+/**
+ * Returns the value of the requested column as a nullable byte array.
+ *
+ * The result and whether this method throws an exception when the column type is not a blob type is
+ * implementation-defined.
+ *
+ * @see Cursor.isNull
+ * @see Cursor.getBlob
+ */
+inline fun Cursor.getBlobOrNull(index: Int) = if (isNull(index)) null else getBlob(index)
+
+/**
+ * Returns the value of the requested column as a nullable double.
+ *
+ * The result and whether this method throws an exception when the column type is not a
+ * floating-point type is implementation-defined.
+ *
+ * @see Cursor.isNull
+ * @see Cursor.getDouble
+ */
+inline fun Cursor.getDoubleOrNull(index: Int) = if (isNull(index)) null else getDouble(index)
+
+/**
+ * Returns the value of the requested column as a nullable float.
+ *
+ * The result and whether this method throws an exception when the column type is not a
+ * floating-point type is implementation-defined.
+ *
+ * @see Cursor.isNull
+ * @see Cursor.getFloat
+ */
+inline fun Cursor.getFloatOrNull(index: Int) = if (isNull(index)) null else getFloat(index)
+
+/**
+ * Returns the value of the requested column as a nullable integer.
+ *
+ * The result and whether this method throws an exception when the column type is not an integral
+ * type is implementation-defined.
+ *
+ * @see Cursor.isNull
+ * @see Cursor.getInt
+ */
+inline fun Cursor.getIntOrNull(index: Int) = if (isNull(index)) null else getInt(index)
+
+/**
+ * Returns the value of the requested column as a nullable long.
+ *
+ * The result and whether this method throws an exception when the column type is not an integral
+ * type is implementation-defined.
+ *
+ * @see Cursor.isNull
+ * @see Cursor.getLong
+ */
+inline fun Cursor.getLongOrNull(index: Int) = if (isNull(index)) null else getLong(index)
+
+/**
+ * Returns the value of the requested column as a nullable short.
+ *
+ * The result and whether this method throws an exception when the column type is not an integral
+ * type is implementation-defined.
+ *
+ * @see Cursor.isNull
+ * @see Cursor.getShort
+ */
+inline fun Cursor.getShortOrNull(index: Int) = if (isNull(index)) null else getShort(index)
+
+/**
+ * Returns the value of the requested column as a nullable string.
+ *
+ * The result and whether this method throws an exception when the column type is not a string type
+ * is implementation-defined.
+ *
+ * @see Cursor.isNull
+ * @see Cursor.getString
+ */
+inline fun Cursor.getStringOrNull(index: Int) = if (isNull(index)) null else getString(index)
+
+/**
+ * Returns the value of the requested column as a nullable byte array.
+ *
+ * The result and whether this method throws an exception when the column type is not a blob type is
+ * implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.isNull
+ * @see Cursor.getBlob
+ */
+inline fun Cursor.getBlobOrNull(columnName: String) =
+ getColumnIndexOrThrow(columnName).let { if (isNull(it)) null else getBlob(it) }
+
+/**
+ * Returns the value of the requested column as a nullable double.
+ *
+ * The result and whether this method throws an exception when the column type is not a
+ * floating-point type is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.isNull
+ * @see Cursor.getDouble
+ */
+inline fun Cursor.getDoubleOrNull(columnName: String) =
+ getColumnIndexOrThrow(columnName).let { if (isNull(it)) null else getDouble(it) }
+
+/**
+ * Returns the value of the requested column as a nullable float.
+ *
+ * The result and whether this method throws an exception when the column type is not a
+ * floating-point type is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.isNull
+ * @see Cursor.getFloat
+ */
+inline fun Cursor.getFloatOrNull(columnName: String) =
+ getColumnIndexOrThrow(columnName).let { if (isNull(it)) null else getFloat(it) }
+
+/**
+ * Returns the value of the requested column as a nullable integer.
+ *
+ * The result and whether this method throws an exception when the column type is not an integral
+ * type is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.isNull
+ * @see Cursor.getInt
+ */
+inline fun Cursor.getIntOrNull(columnName: String) =
+ getColumnIndexOrThrow(columnName).let { if (isNull(it)) null else getInt(it) }
+
+/**
+ * Returns the value of the requested column as a nullable long.
+ *
+ * The result and whether this method throws an exception when the column type is not an integral
+ * type is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.isNull
+ * @see Cursor.getLong
+ */
+inline fun Cursor.getLongOrNull(columnName: String) =
+ getColumnIndexOrThrow(columnName).let { if (isNull(it)) null else getLong(it) }
+
+/**
+ * Returns the value of the requested column as a nullable short.
+ *
+ * The result and whether this method throws an exception when the column type is not an integral
+ * type is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.isNull
+ * @see Cursor.getShort
+ */
+inline fun Cursor.getShortOrNull(columnName: String) =
+ getColumnIndexOrThrow(columnName).let { if (isNull(it)) null else getShort(it) }
+
+/**
+ * Returns the value of the requested column as a nullable string.
+ *
+ * The result and whether this method throws an exception when the column type is not a string type
+ * is implementation-defined.
+ *
+ * @see Cursor.getColumnIndexOrThrow
+ * @see Cursor.isNull
+ * @see Cursor.getString
+ */
+inline fun Cursor.getStringOrNull(columnName: String) =
+ getColumnIndexOrThrow(columnName).let { if (isNull(it)) null else getString(it) }
diff --git a/core/ktx/src/main/java/androidx/core/database/sqlite/SQLiteDatabase.kt b/core/ktx/src/main/java/androidx/core/database/sqlite/SQLiteDatabase.kt
new file mode 100644
index 0000000..bdbd94e
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/database/sqlite/SQLiteDatabase.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.database.sqlite
+
+import android.database.sqlite.SQLiteDatabase
+
+/**
+ * Run [body] in a transaction marking it as successful if it completes without exception.
+ *
+ * @param exclusive Run in `EXCLUSIVE` mode when true, `IMMEDIATE` mode otherwise.
+ */
+inline fun <T> SQLiteDatabase.transaction(
+ exclusive: Boolean = true,
+ body: SQLiteDatabase.() -> T
+): T {
+ if (exclusive) {
+ beginTransaction()
+ } else {
+ beginTransactionNonExclusive()
+ }
+ try {
+ val result = body()
+ setTransactionSuccessful()
+ return result
+ } finally {
+ endTransaction()
+ }
+}
diff --git a/core/ktx/src/main/java/androidx/core/graphics/Bitmap.kt b/core/ktx/src/main/java/androidx/core/graphics/Bitmap.kt
new file mode 100644
index 0000000..45ab6ba
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/Bitmap.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.graphics
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.ColorSpace
+import androidx.annotation.ColorInt
+import androidx.annotation.RequiresApi
+
+/**
+ * Creates a new [Canvas] to draw on this bitmap and executes the specified
+ * [block] on the newly created canvas. Example:
+ *
+ * ```
+ * return Bitmap.createBitmap(…).applyCanvas {
+ * drawLine(…)
+ * translate(…)
+ * drawRect(…)
+ * }
+ * ```
+ */
+inline fun Bitmap.applyCanvas(block: Canvas.() -> Unit): Bitmap {
+ val c = Canvas(this)
+ c.block()
+ return this
+}
+
+/**
+ * Returns the value of the pixel at the specified location. The returned value
+ * is a [color int][android.graphics.Color] in the sRGB color space.
+ */
+inline operator fun Bitmap.get(x: Int, y: Int) = getPixel(x, y)
+
+/**
+ * Writes the specified [color int][android.graphics.Color] into the bitmap
+ * (assuming it is mutable) at the specified `(x, y)` coordinate. The specified
+ * color is converted from sRGB to the bitmap's color space if needed.
+ */
+inline operator fun Bitmap.set(x: Int, y: Int, @ColorInt color: Int) = setPixel(x, y, color)
+
+/**
+ * Creates a new bitmap, scaled from this bitmap, when possible. If the specified
+ * [width] and [height] are the same as the current width and height of this bitmap,
+ * this bitmap is returned and no new bitmap is created.
+ *
+ * @param width The new bitmap's desired width
+ * @param height The new bitmap's desired height
+ * @param filter `true` if the source should be filtered (`true` by default)
+ *
+ * @return The new scaled bitmap or the source bitmap if no scaling is required.
+ */
+inline fun Bitmap.scale(width: Int, height: Int, filter: Boolean = true): Bitmap {
+ return Bitmap.createScaledBitmap(this, width, height, filter)
+}
+
+/**
+ * Returns a mutable bitmap with the specified [width] and [height]. A config
+ * can be optionally specified. If not, the default config is [Bitmap.Config.ARGB_8888].
+ *
+ * @param width The new bitmap's desired width
+ * @param height The new bitmap's desired height
+ * @param config The new bitmap's desired [config][Bitmap.Config]
+ *
+ * @return A new bitmap with the specified dimensions and config
+ */
+inline fun createBitmap(
+ width: Int,
+ height: Int,
+ config: Bitmap.Config = Bitmap.Config.ARGB_8888
+): Bitmap {
+ return Bitmap.createBitmap(width, height, config)
+}
+
+/**
+ * Returns a mutable bitmap with the specified [width] and [height]. The config,
+ * transparency and color space can optionally be specified. They respectively
+ * default to [Bitmap.Config.ARGB_8888], `true` and [sRGB][ColorSpace.Named.SRGB].
+ *
+ * @param width The new bitmap's desired width
+ * @param height The new bitmap's desired height
+ * @param config The new bitmap's desired [config][Bitmap.Config]
+ * @param hasAlpha Whether the new bitmap is opaque or not
+ * @param colorSpace The new bitmap's color space
+ *
+ * @return A new bitmap with the specified dimensions and config
+ */
+@RequiresApi(26)
+inline fun createBitmap(
+ width: Int,
+ height: Int,
+ config: Bitmap.Config = Bitmap.Config.ARGB_8888,
+ hasAlpha: Boolean = true,
+ colorSpace: ColorSpace = ColorSpace.get(ColorSpace.Named.SRGB)
+): Bitmap {
+ return Bitmap.createBitmap(width, height, config, hasAlpha, colorSpace)
+}
diff --git a/core/ktx/src/main/java/androidx/core/graphics/Canvas.kt b/core/ktx/src/main/java/androidx/core/graphics/Canvas.kt
new file mode 100644
index 0000000..71efc08
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/Canvas.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Canvas
+import android.graphics.Matrix
+
+/**
+ * Wrap the specified [block] in calls to [Canvas.save]
+ * and [Canvas.restoreToCount].
+ */
+inline fun Canvas.withSave(block: Canvas.() -> Unit) {
+ val checkpoint = save()
+ try {
+ block()
+ } finally {
+ restoreToCount(checkpoint)
+ }
+}
+
+/**
+ * Wrap the specified [block] in calls to [Canvas.save]/[Canvas.translate]
+ * and [Canvas.restoreToCount].
+ */
+inline fun Canvas.withTranslation(
+ x: Float = 0.0f,
+ y: Float = 0.0f,
+ block: Canvas.() -> Unit
+) {
+ val checkpoint = save()
+ translate(x, y)
+ try {
+ block()
+ } finally {
+ restoreToCount(checkpoint)
+ }
+}
+
+/**
+ * Wrap the specified [block] in calls to [Canvas.save]/[Canvas.rotate]
+ * and [Canvas.restoreToCount].
+ */
+inline fun Canvas.withRotation(
+ degrees: Float = 0.0f,
+ pivotX: Float = 0.0f,
+ pivotY: Float = 0.0f,
+ block: Canvas.() -> Unit
+) {
+ val checkpoint = save()
+ rotate(degrees, pivotX, pivotY)
+ try {
+ block()
+ } finally {
+ restoreToCount(checkpoint)
+ }
+}
+
+/**
+ * Wrap the specified [block] in calls to [Canvas.save]/[Canvas.scale]
+ * and [Canvas.restoreToCount].
+ */
+inline fun Canvas.withScale(
+ x: Float = 1.0f,
+ y: Float = 1.0f,
+ pivotX: Float = 0.0f,
+ pivotY: Float = 0.0f,
+ block: Canvas.() -> Unit
+) {
+ val checkpoint = save()
+ scale(x, y, pivotX, pivotY)
+ try {
+ block()
+ } finally {
+ restoreToCount(checkpoint)
+ }
+}
+
+/**
+ * Wrap the specified [block] in calls to [Canvas.save]/[Canvas.skew]
+ * and [Canvas.restoreToCount].
+ */
+inline fun Canvas.withSkew(
+ x: Float = 0.0f,
+ y: Float = 0.0f,
+ block: Canvas.() -> Unit
+) {
+ val checkpoint = save()
+ skew(x, y)
+ try {
+ block()
+ } finally {
+ restoreToCount(checkpoint)
+ }
+}
+
+/**
+ * Wrap the specified [block] in calls to [Canvas.save]/[Canvas.concat]
+ * and [Canvas.restoreToCount].
+ */
+inline fun Canvas.withMatrix(
+ matrix: Matrix = Matrix(),
+ block: Canvas.() -> Unit
+) {
+ val checkpoint = save()
+ concat(matrix)
+ try {
+ block()
+ } finally {
+ restoreToCount(checkpoint)
+ }
+}
diff --git a/core/ktx/src/main/java/androidx/core/graphics/Color.kt b/core/ktx/src/main/java/androidx/core/graphics/Color.kt
new file mode 100644
index 0000000..f853422
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/Color.kt
@@ -0,0 +1,364 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE", "WRONG_ANNOTATION_TARGET_WITH_USE_SITE_TARGET_ON_TYPE")
+
+package androidx.core.graphics
+
+import android.graphics.Color
+import android.graphics.ColorSpace
+import androidx.annotation.ColorInt
+import androidx.annotation.ColorLong
+import androidx.annotation.RequiresApi
+
+/**
+ * Returns the first component of the color. For instance, when the color model
+ * of the color is [android.graphics.ColorSpace.Model.RGB], the first component
+ * is "red".
+ *
+ * This method allows to use destructuring declarations when working with colors,
+ * for example:
+ * ```
+ * val (red, green, blue) = myColor
+ * ```
+ */
+@RequiresApi(26)
+inline operator fun Color.component1() = getComponent(0)
+
+/**
+ * Returns the second component of the color. For instance, when the color model
+ * of the color is [android.graphics.ColorSpace.Model.RGB], the second component
+ * is "green".
+ *
+ * This method allows to use destructuring declarations when working with colors,
+ * for example:
+ * ```
+ * val (red, green, blue) = myColor
+ * ```
+ */
+@RequiresApi(26)
+inline operator fun Color.component2() = getComponent(1)
+
+/**
+ * Returns the third component of the color. For instance, when the color model
+ * of the color is [android.graphics.ColorSpace.Model.RGB], the third component
+ * is "blue".
+= *
+ * This method allows to use destructuring declarations when working with colors,
+ * for example:
+ * ```
+ * val (red, green, blue) = myColor
+ * ```
+ */
+@RequiresApi(26)
+inline operator fun Color.component3() = getComponent(2)
+
+/**
+ * Returns the fourth component of the color. For instance, when the color model
+ * of the color is [android.graphics.ColorSpace.Model.RGB], the fourth component
+ * is "alpha".
+ *
+ * This method allows to use destructuring declarations when working with colors,
+ * for example:
+ * ```
+ * val (red, green, blue, alpha) = myColor
+ * ```
+ */
+@RequiresApi(26)
+inline operator fun Color.component4() = getComponent(3)
+
+/**
+ * Composites two translucent colors together. More specifically, adds two colors using
+ * the [source over][android.graphics.PorterDuff.Mode.SRC_OVER] blending mode. The colors
+ * must not be pre-multiplied and the result is a non pre-multiplied color.
+ *
+ * If the two colors have different color spaces, the color in the right-hand part
+ * of the expression is converted to the color space of the color in left-hand part
+ * of the expression.
+ *
+ * The following example creates a purple color by blending opaque blue with
+ * semi-translucent red:
+ *
+ * ```
+ * val purple = Color.valueOf(0f, 0f, 1f) + Color.valueOf(1f, 0f, 0f, 0.5f)
+ * ```
+ *
+ * @throws IllegalArgumentException if the [color models][android.graphics.Color.getModel]
+ * of the colors do not match
+ */
+@RequiresApi(26)
+operator fun Color.plus(c: Color): Color = ColorUtils.compositeColors(c, this)
+
+/**
+ * Return the alpha component of a color int. This is equivalent to calling:
+ * ```
+ * Color.alpha(myInt)
+ * ```
+ */
+inline val @receiver:ColorInt Int.alpha get() = (this shr 24) and 0xff
+
+/**
+ * Return the red component of a color int. This is equivalent to calling:
+ * ```
+ * Color.red(myInt)
+ * ```
+ */
+inline val @receiver:ColorInt Int.red get() = (this shr 16) and 0xff
+
+/**
+ * Return the green component of a color int. This is equivalent to calling:
+ * ```
+ * Color.green(myInt)
+ * ```
+ */
+inline val @receiver:ColorInt Int.green get() = (this shr 8) and 0xff
+
+/**
+ * Return the blue component of a color int. This is equivalent to calling:
+ * ```
+ * Color.blue(myInt)
+ * ```
+ */
+inline val @receiver:ColorInt Int.blue get() = this and 0xff
+
+/**
+ * Return the alpha component of a color int. This is equivalent to calling:
+ * ```
+ * Color.alpha(myInt)
+ * ```
+ *
+ * This method allows to use destructuring declarations when working with colors,
+ * for example:
+ * ```
+ * val (alpha, red, green, blue) = myColor
+ * ```
+ */
+inline operator fun @receiver:ColorInt Int.component1() = (this shr 24) and 0xff
+
+/**
+ * Return the red component of a color int. This is equivalent to calling:
+ * ```
+ * Color.red(myInt)
+ * ```
+ *
+ * This method allows to use destructuring declarations when working with colors,
+ * for example:
+ * ```
+ * val (alpha, red, green, blue) = myColor
+ * ```
+ */
+inline operator fun @receiver:ColorInt Int.component2() = (this shr 16) and 0xff
+
+/**
+ * Return the green component of a color int. This is equivalent to calling:
+ * ```
+ * Color.green(myInt)
+ * ```
+ *
+ * This method allows to use destructuring declarations when working with colors,
+ * for example:
+ * ```
+ * val (alpha, red, green, blue) = myColor
+ * ```
+ */
+inline operator fun @receiver:ColorInt Int.component3() = (this shr 8) and 0xff
+
+/**
+ * Return the blue component of a color int. This is equivalent to calling:
+ * ```
+ * Color.blue(myInt)
+ * ```
+ *
+ * This method allows to use destructuring declarations when working with colors,
+ * for example:
+ * ```
+ * val (alpha, red, green, blue) = myColor
+ * ```
+ */
+inline operator fun @receiver:ColorInt Int.component4() = this and 0xff
+
+/**
+ * Returns the relative luminance of a color int, assuming sRGB encoding.
+ * Based on the formula for relative luminance defined in WCAG 2.0,
+ * W3C Recommendation 11 December 2008.
+ */
+@get:RequiresApi(26)
+inline val @receiver:ColorInt Int.luminance get() = Color.luminance(this)
+
+/**
+ * Creates a new [Color] instance from a color int. The resulting color
+ * is in the [sRGB][android.graphics.ColorSpace.Named.SRGB] color space.
+ */
+@RequiresApi(26)
+inline fun @receiver:ColorInt Int.toColor(): Color = Color.valueOf(this)
+
+/**
+ * Converts the specified ARGB [color int][Color] to an RGBA [color long][Color]
+ * in the [sRGB][android.graphics.ColorSpace.Named.SRGB] color space.
+ */
+@RequiresApi(26)
+@ColorLong
+inline fun @receiver:ColorInt Int.toColorLong() = Color.pack(this)
+
+/**
+ * Returns the first component of the color. For instance, when the color model
+ * of the color is [android.graphics.ColorSpace.Model.RGB], the first component
+ * is "red".
+ *
+ * This method allows to use destructuring declarations when working with colors,
+ * for example:
+ * ```
+ * val (red, green, blue, alpha) = myColorLong
+ * ```
+ */
+@RequiresApi(26)
+inline operator fun @receiver:ColorLong Long.component1() = Color.red(this)
+
+/**
+ * Returns the second component of the color. For instance, when the color model
+ * of the color is [android.graphics.ColorSpace.Model.RGB], the second component
+ * is "green".
+ *
+ * This method allows to use destructuring declarations when working with colors,
+ * for example:
+ * ```
+ * val (red, green, blue, alpha) = myColorLong
+ * ```
+ */
+@RequiresApi(26)
+inline operator fun @receiver:ColorLong Long.component2() = Color.green(this)
+
+/**
+ * Returns the third component of the color. For instance, when the color model
+ * of the color is [android.graphics.ColorSpace.Model.RGB], the third component
+ * is "blue".
+ *
+ * This method allows to use destructuring declarations when working with colors,
+ * for example:
+ * ```
+ * val (red, green, blue, alpha) = myColorLong
+ * ```
+ */
+@RequiresApi(26)
+inline operator fun @receiver:ColorLong Long.component3() = Color.blue(this)
+
+/**
+ * Returns the fourth component of the color. For instance, when the color model
+ * of the color is [android.graphics.ColorSpace.Model.RGB], the fourth component
+ * is "alpha".
+ *
+ * This method allows to use destructuring declarations when working with colors,
+ * for example:
+ * ```
+ * val (red, green, blue, alpha) = myColorLong
+ * ```
+ */
+@RequiresApi(26)
+inline operator fun @receiver:ColorLong Long.component4() = Color.alpha(this)
+
+/**
+ * Return the alpha component of a color long. This is equivalent to calling:
+ * ```
+ * Color.alpha(myLong)
+ * ```
+ */
+@get:RequiresApi(26)
+inline val @receiver:ColorLong Long.alpha get() = Color.alpha(this)
+
+/**
+ * Return the red component of a color long. This is equivalent to calling:
+ * ```
+ * Color.red(myLong)
+ * ```
+ */
+@get:RequiresApi(26)
+inline val @receiver:ColorLong Long.red get() = Color.red(this)
+
+/**
+ * Return the green component of a color long. This is equivalent to calling:
+ * ```
+ * Color.green(myLong)
+ * ```
+ */
+@get:RequiresApi(26)
+inline val @receiver:ColorLong Long.green get() = Color.green(this)
+
+/**
+ * Return the blue component of a color long. This is equivalent to calling:
+ * ```
+ * Color.blue(myLong)
+ * ```
+ */
+@get:RequiresApi(26)
+inline val @receiver:ColorLong Long.blue get() = Color.blue(this)
+
+/**
+ * Returns the relative luminance of a color. Based on the formula for
+ * relative luminance defined in WCAG 2.0, W3C Recommendation 11 December 2008.
+ */
+@get:RequiresApi(26)
+inline val @receiver:ColorLong Long.luminance get() = Color.luminance(this)
+
+/**
+ * Creates a new [Color] instance from a [color long][Color].
+ */
+@RequiresApi(26)
+inline fun @receiver:ColorLong Long.toColor(): Color = Color.valueOf(this)
+
+/**
+ * Converts the specified [color long][Color] to an ARGB [color int][Color].
+ */
+@RequiresApi(26)
+@ColorInt
+inline fun @receiver:ColorLong Long.toColorInt() = Color.toArgb(this)
+
+/**
+ * Indicates whether the color is in the [sRGB][android.graphics.ColorSpace.Named.SRGB]
+ * color space.
+ */
+@get:RequiresApi(26)
+inline val @receiver:ColorLong Long.isSrgb get() = Color.isSrgb(this)
+
+/**
+ * Indicates whether the color is in a [wide-gamut][android.graphics.ColorSpace] color space.
+ */
+@get:RequiresApi(26)
+inline val @receiver:ColorLong Long.isWideGamut get() = Color.isWideGamut(this)
+
+/**
+ * Returns the color space encoded in the specified color long.
+ */
+@get:RequiresApi(26)
+inline val @receiver:ColorLong Long.colorSpace: ColorSpace get() = Color.colorSpace(this)
+
+/**
+ * Return a corresponding [Int] color of this [String].
+ *
+ * Supported formats are:
+ * ```
+ * #RRGGBB
+ * #AARRGGBB
+ * ```
+ *
+ * The following names are also accepted: "red", "blue", "green", "black", "white",
+ * "gray", "cyan", "magenta", "yellow", "lightgray", "darkgray",
+ * "grey", "lightgrey", "darkgrey", "aqua", "fuchsia", "lime",
+ * "maroon", "navy", "olive", "purple", "silver", "teal".
+ *
+ * @throws IllegalArgumentException if this [String] cannot be parsed.
+ */
+@ColorInt
+inline fun String.toColorInt(): Int = Color.parseColor(this)
diff --git a/core/ktx/src/main/java/androidx/core/graphics/Matrix.kt b/core/ktx/src/main/java/androidx/core/graphics/Matrix.kt
new file mode 100644
index 0000000..5cb0cdf
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/Matrix.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.graphics
+
+import android.graphics.Matrix
+
+/**
+ * Multiplies this [Matrix] by another matrix and returns the result as
+ * a new matrix.
+ */
+inline operator fun Matrix.times(m: Matrix) = Matrix(this).apply { preConcat(m) }
+
+/**
+ * Returns the 9 values of this [Matrix] as a new array of floats.
+ */
+inline fun Matrix.values() = FloatArray(9).apply { getValues(this) }
+
+/**
+ * Creates a translation matrix with the translation amounts [tx] and [ty]
+ * respectively on the `x` and `y` axis.
+ */
+fun translationMatrix(tx: Float = 0.0f, ty: Float = 0.0f) = Matrix().apply { setTranslate(tx, ty) }
+
+/**
+ * Creates a scale matrix with the scale factor [sx] and [sy] respectively on the
+ * `x` and `y` axis.
+ */
+fun scaleMatrix(sx: Float = 1.0f, sy: Float = 1.0f) = Matrix().apply { setScale(sx, sy) }
+
+/**
+ * Creates a rotation matrix, defined by a rotation angle in degrees around the pivot
+ * point located at the coordinates ([px], [py]).
+ */
+fun rotationMatrix(degrees: Float, px: Float = 0.0f, py: Float = 0.0f) =
+ Matrix().apply { setRotate(degrees, px, py) }
diff --git a/core/ktx/src/main/java/androidx/core/graphics/Path.kt b/core/ktx/src/main/java/androidx/core/graphics/Path.kt
new file mode 100644
index 0000000..ca70533
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/Path.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.graphics
+
+import android.graphics.Path
+import androidx.annotation.RequiresApi
+
+/**
+ * Flattens (or approximate) the [Path] with a series of line segments.
+ *
+ * @param error The acceptable error for a line on the Path. Typically this would be
+ * 0.5 so that the error is less than half a pixel. This value must be
+ * positive and is set to 0.5 by default.
+ *
+ * @see Path.approximate
+ */
+@RequiresApi(26)
+fun Path.flatten(error: Float = 0.5f): Iterable<PathSegment> = PathUtils.flatten(this, error)
+
+/**
+ * Returns the union of two paths as a new [Path].
+ */
+@RequiresApi(19)
+inline operator fun Path.plus(p: Path): Path {
+ return Path(this).apply {
+ op(p, Path.Op.UNION)
+ }
+}
+
+/**
+ * Returns the difference of two paths as a new [Path].
+ */
+@RequiresApi(19)
+inline operator fun Path.minus(p: Path): Path {
+ return Path(this).apply {
+ op(p, Path.Op.DIFFERENCE)
+ }
+}
+
+/**
+ * Returns the union of two paths as a new [Path].
+ */
+@RequiresApi(19)
+inline infix fun Path.and(p: Path) = this + p
+
+/**
+ * Returns the intersection of two paths as a new [Path].
+ * If the paths do not intersect, returns an empty path.
+ */
+@RequiresApi(19)
+inline infix fun Path.or(p: Path): Path {
+ return Path().apply {
+ op(this@or, p, Path.Op.INTERSECT)
+ }
+}
+
+/**
+ * Returns the union minus the intersection of two paths as a new [Path].
+ */
+@RequiresApi(19)
+inline infix fun Path.xor(p: Path): Path {
+ return Path(this).apply {
+ op(p, Path.Op.XOR)
+ }
+}
diff --git a/core/ktx/src/main/java/androidx/core/graphics/Picture.kt b/core/ktx/src/main/java/androidx/core/graphics/Picture.kt
new file mode 100644
index 0000000..d1e8c99
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/Picture.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 20188 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.graphics
+
+import android.graphics.Canvas
+import android.graphics.Picture
+
+/**
+ * Creates a new [Canvas] to record commands in this [Picture], executes the specified
+ * [block] on the newly created canvas and returns this [Picture]. Example:
+ *
+ * ```
+ * return myPicture.record(1280, 720) {
+ * drawLine(…)
+ * translate(…)
+ * drawRect(…)
+ * }
+ * ```
+ */
+inline fun Picture.record(width: Int, height: Int, block: Canvas.() -> Unit): Picture {
+ val c = beginRecording(width, height)
+ try {
+ c.block()
+ } finally {
+ endRecording()
+ }
+ return this
+}
diff --git a/core/ktx/src/main/java/androidx/core/graphics/Point.kt b/core/ktx/src/main/java/androidx/core/graphics/Point.kt
new file mode 100644
index 0000000..b6e35e6
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/Point.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.graphics
+
+import android.graphics.Point
+import android.graphics.PointF
+
+/**
+ * Returns the x coordinate of this point.
+ *
+ * This method allows to use destructuring declarations when working with points,
+ * for example:
+ * ```
+ * val (x, y) = myPoint
+ * ```
+ */
+inline operator fun Point.component1() = this.x
+
+/**
+ * Returns the y coordinate of this point.
+ *
+ * This method allows to use destructuring declarations when working with points,
+ * for example:
+ * ```
+ * val (x, y) = myPoint
+ * ```
+ */
+inline operator fun Point.component2() = this.y
+
+/**
+ * Returns the x coordinate of this point.
+ *
+ * This method allows to use destructuring declarations when working with points,
+ * for example:
+ * ```
+ * val (x, y) = myPoint
+ * ```
+ */
+inline operator fun PointF.component1() = this.x
+
+/**
+ * Returns the y coordinate of this point.
+ *
+ * This method allows to use destructuring declarations when working with points,
+ * for example:
+ * ```
+ * val (x, y) = myPoint
+ * ```
+ */
+inline operator fun PointF.component2() = this.y
+
+/**
+ * Offsets this point by the specified point and returns the result as a new point.
+ */
+inline operator fun Point.plus(p: Point): Point {
+ return Point(x, y).apply {
+ offset(p.x, p.y)
+ }
+}
+
+/**
+ * Offsets this point by the specified point and returns the result as a new point.
+ */
+inline operator fun PointF.plus(p: PointF): PointF {
+ return PointF(x, y).apply {
+ offset(p.x, p.y)
+ }
+}
+
+/**
+ * Offsets this point by the specified amount on both X and Y axis and returns the
+ * result as a new point.
+ */
+inline operator fun Point.plus(xy: Int): Point {
+ return Point(x, y).apply {
+ offset(xy, xy)
+ }
+}
+
+/**
+ * Offsets this point by the specified amount on both X and Y axis and returns the
+ * result as a new point.
+ */
+inline operator fun PointF.plus(xy: Float): PointF {
+ return PointF(x, y).apply {
+ offset(xy, xy)
+ }
+}
+
+/**
+ * Offsets this point by the negation of the specified point and returns the result
+ * as a new point.
+ */
+inline operator fun Point.minus(p: Point): Point {
+ return Point(x, y).apply {
+ offset(-p.x, -p.y)
+ }
+}
+
+/**
+ * Offsets this point by the negation of the specified point and returns the result
+ * as a new point.
+ */
+inline operator fun PointF.minus(p: PointF): PointF {
+ return PointF(x, y).apply {
+ offset(-p.x, -p.y)
+ }
+}
+
+/**
+ * Offsets this point by the negation of the specified amount on both X and Y axis and
+ * returns the result as a new point.
+ */
+inline operator fun Point.minus(xy: Int): Point {
+ return Point(x, y).apply {
+ offset(-xy, -xy)
+ }
+}
+
+/**
+ * Offsets this point by the negation of the specified amount on both X and Y axis and
+ * returns the result as a new point.
+ */
+inline operator fun PointF.minus(xy: Float): PointF {
+ return PointF(x, y).apply {
+ offset(-xy, -xy)
+ }
+}
+
+/**
+ * Returns a new point representing the negation of this point.
+ */
+inline operator fun Point.unaryMinus() = Point(-x, -y)
+
+/**
+ * Returns a new point representing the negation of this point.
+ */
+inline operator fun PointF.unaryMinus() = PointF(-x, -y)
+
+/**
+ * Returns a [PointF] representation of this point.
+ */
+inline fun Point.toPointF() = PointF(this)
+
+/**
+ * Returns a [Point] representation of this point.
+ */
+inline fun PointF.toPoint() = Point(x.toInt(), y.toInt())
diff --git a/core/ktx/src/main/java/androidx/core/graphics/PorterDuff.kt b/core/ktx/src/main/java/androidx/core/graphics/PorterDuff.kt
new file mode 100644
index 0000000..a34d6f9
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/PorterDuff.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.graphics
+
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.graphics.PorterDuffXfermode
+
+/**
+ * Creates a new [PorterDuffXfermode] that uses this [PorterDuff.Mode] as the
+ * alpha compositing or blending mode.
+ */
+inline fun PorterDuff.Mode.toXfermode() = PorterDuffXfermode(this)
+
+/**
+ * Creates a new [PorterDuffColorFilter] that uses this [PorterDuff.Mode] as the
+ * alpha compositing or blending mode, and the specified [color].
+ */
+inline fun PorterDuff.Mode.toColorFilter(color: Int) = PorterDuffColorFilter(color, this)
diff --git a/core/ktx/src/main/java/androidx/core/graphics/Rect.kt b/core/ktx/src/main/java/androidx/core/graphics/Rect.kt
new file mode 100644
index 0000000..960c0c5
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/Rect.kt
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.graphics
+
+import android.annotation.SuppressLint
+import android.graphics.Matrix
+import android.graphics.Point
+import android.graphics.PointF
+import android.graphics.Rect
+import android.graphics.RectF
+import android.graphics.Region
+
+/**
+ * Returns "left", the first component of the rectangle.
+ *
+ * This method allows to use destructuring declarations when working with rectangles,
+ * for example:
+ * ```
+ * val (left, top, right, bottom) = myRectangle
+ * ```
+ */
+inline operator fun Rect.component1() = this.left
+
+/**
+ * Returns "top", the second component of the rectangle.
+ *
+ * This method allows to use destructuring declarations when working with rectangles,
+ * for example:
+ * ```
+ * val (left, top, right, bottom) = myRectangle
+ * ```
+ */
+inline operator fun Rect.component2() = this.top
+
+/**
+ * Returns "right", the third component of the rectangle.
+ *
+ * This method allows to use destructuring declarations when working with rectangles,
+ * for example:
+ * ```
+ * val (left, top, right, bottom) = myRectangle
+ * ```
+ */
+inline operator fun Rect.component3() = this.right
+
+/**
+ * Returns "bottom", the fourth component of the rectangle.
+ *
+ * This method allows to use destructuring declarations when working with rectangles,
+ * for example:
+ * ```
+ * val (left, top, right, bottom) = myRectangle
+ * ```
+ */
+inline operator fun Rect.component4() = this.bottom
+
+/**
+ * Returns "left", the first component of the rectangle.
+ *
+ * This method allows to use destructuring declarations when working with rectangles,
+ * for example:
+ * ```
+ * val (left, top, right, bottom) = myRectangle
+ * ```
+ */
+inline operator fun RectF.component1() = this.left
+
+/**
+ * Returns "top", the second component of the rectangle.
+ *
+ * This method allows to use destructuring declarations when working with rectangles,
+ * for example:
+ * ```
+ * val (left, top, right, bottom) = myRectangle
+ * ```
+ */
+inline operator fun RectF.component2() = this.top
+
+/**
+ * Returns "right", the third component of the rectangle.
+ *
+ * This method allows to use destructuring declarations when working with rectangles,
+ * for example:
+ * ```
+ * val (left, top, right, bottom) = myRectangle
+ * ```
+ */
+inline operator fun RectF.component3() = this.right
+
+/**
+ * Returns "bottom", the fourth component of the rectangle.
+ *
+ * This method allows to use destructuring declarations when working with rectangles,
+ * for example:
+ * ```
+ * val (left, top, right, bottom) = myRectangle
+ * ```
+ */
+inline operator fun RectF.component4() = this.bottom
+
+/**
+ * Performs the union of this rectangle and the specified rectangle and returns
+ * the result as a new rectangle.
+ */
+inline operator fun Rect.plus(r: Rect): Rect {
+ return Rect(this).apply {
+ union(r)
+ }
+}
+
+/**
+ * Performs the union of this rectangle and the specified rectangle and returns
+ * the result as a new rectangle.
+ */
+inline operator fun RectF.plus(r: RectF): RectF {
+ return RectF(this).apply {
+ union(r)
+ }
+}
+
+/**
+ * Returns a new rectangle representing this rectangle offset by the specified
+ * amount on both X and Y axis.
+ */
+inline operator fun Rect.plus(xy: Int): Rect {
+ return Rect(this).apply {
+ offset(xy, xy)
+ }
+}
+
+/**
+ * Returns a new rectangle representing this rectangle offset by the specified
+ * amount on both X and Y axis.
+ */
+inline operator fun RectF.plus(xy: Float): RectF {
+ return RectF(this).apply {
+ offset(xy, xy)
+ }
+}
+
+/**
+ * Returns a new rectangle representing this rectangle offset by the specified
+ * point.
+ */
+inline operator fun Rect.plus(xy: Point): Rect {
+ return Rect(this).apply {
+ offset(xy.x, xy.y)
+ }
+}
+
+/**
+ * Returns a new rectangle representing this rectangle offset by the specified
+ * point.
+ */
+inline operator fun RectF.plus(xy: PointF): RectF {
+ return RectF(this).apply {
+ offset(xy.x, xy.y)
+ }
+}
+
+/**
+ * Returns the difference of this rectangle and the specified rectangle as a new region.
+ */
+inline operator fun Rect.minus(r: Rect): Region {
+ return Region(this).apply {
+ op(r, Region.Op.DIFFERENCE)
+ }
+}
+
+/**
+ * Returns the difference of this rectangle and the specified rectangle as a new region.
+ * This rectangle is first converted to a [Rect] using [RectF.toRect].
+ */
+inline operator fun RectF.minus(r: RectF): Region {
+ return Region(this.toRect()).apply {
+ op(r.toRect(), Region.Op.DIFFERENCE)
+ }
+}
+
+/**
+ * Returns a new rectangle representing this rectangle offset by the negation
+ * of the specified amount on both X and Y axis.
+ */
+inline operator fun Rect.minus(xy: Int): Rect {
+ return Rect(this).apply {
+ offset(-xy, -xy)
+ }
+}
+
+/**
+ * Returns a new rectangle representing this rectangle offset by the negation
+ * of the specified amount on both X and Y axis.
+ */
+inline operator fun RectF.minus(xy: Float): RectF {
+ return RectF(this).apply {
+ offset(-xy, -xy)
+ }
+}
+
+/**
+ * Returns a new rectangle representing this rectangle offset by the negation of
+ * the specified point.
+ */
+inline operator fun Rect.minus(xy: Point): Rect {
+ return Rect(this).apply {
+ offset(-xy.x, -xy.y)
+ }
+}
+
+/**
+ * Returns a new rectangle representing this rectangle offset by the negation of
+ * the specified point.
+ */
+inline operator fun RectF.minus(xy: PointF): RectF {
+ return RectF(this).apply {
+ offset(-xy.x, -xy.y)
+ }
+}
+
+/**
+ * Returns the union of two rectangles as a new rectangle.
+ */
+inline infix fun Rect.and(r: Rect) = this + r
+
+/**
+ * Returns the union of two rectangles as a new rectangle.
+ */
+inline infix fun RectF.and(r: RectF) = this + r
+
+/**
+ * Returns the intersection of two rectangles as a new rectangle.
+ * If the rectangles do not intersect, returns a copy of the left hand side
+ * rectangle.
+ */
+@SuppressLint("CheckResult")
+inline infix fun Rect.or(r: Rect): Rect {
+ return Rect(this).apply {
+ intersect(r)
+ }
+}
+
+/**
+ * Returns the intersection of two rectangles as a new rectangle.
+ * If the rectangles do not intersect, returns a copy of the left hand side
+ * rectangle.
+ */
+@SuppressLint("CheckResult")
+inline infix fun RectF.or(r: RectF): RectF {
+ return RectF(this).apply {
+ intersect(r)
+ }
+}
+
+/**
+ * Returns the union minus the intersection of two rectangles as a new region.
+ */
+inline infix fun Rect.xor(r: Rect): Region {
+ return Region(this).apply {
+ op(r, Region.Op.XOR)
+ }
+}
+
+/**
+ * Returns the union minus the intersection of two rectangles as a new region.
+ * The two rectangles are first converted to [Rect] using [RectF.toRect].
+ */
+inline infix fun RectF.xor(r: RectF): Region {
+ return Region(this.toRect()).apply {
+ op(r.toRect(), Region.Op.XOR)
+ }
+}
+
+/**
+ * Returns true if the specified point is inside the rectangle.
+ * The left and top are considered to be inside, while the right and bottom are not.
+ * This means that for a point to be contained: left <= x < right and top <= y < bottom.
+ * An empty rectangle never contains any point.
+ */
+inline operator fun Rect.contains(p: Point) = contains(p.x, p.y)
+
+/**
+ * Returns true if the specified point is inside the rectangle.
+ * The left and top are considered to be inside, while the right and bottom are not.
+ * This means that for a point to be contained: left <= x < right and top <= y < bottom.
+ * An empty rectangle never contains any point.
+ */
+inline operator fun RectF.contains(p: PointF) = contains(p.x, p.y)
+
+/**
+ * Returns a [RectF] representation of this rectangle.
+ */
+inline fun Rect.toRectF(): RectF = RectF(this)
+
+/**
+ * Returns a [Rect] representation of this rectangle. The resulting rect will be sized such
+ * that this rect can fit within it.
+ */
+inline fun RectF.toRect(): Rect {
+ val r = Rect()
+ roundOut(r)
+ return r
+}
+
+/**
+ * Returns a [Region] representation of this rectangle.
+ */
+inline fun Rect.toRegion() = Region(this)
+
+/**
+ * Returns a [Region] representation of this rectangle. The resulting rect will be sized such
+ * that this rect can fit within it.
+ */
+inline fun RectF.toRegion() = Region(this.toRect())
+
+/**
+ * Transform this rectangle in place using the supplied [Matrix] and returns
+ * this rectangle.
+ */
+inline fun RectF.transform(m: Matrix) = apply { m.mapRect(this@transform) }
diff --git a/core/ktx/src/main/java/androidx/core/graphics/Region.kt b/core/ktx/src/main/java/androidx/core/graphics/Region.kt
new file mode 100644
index 0000000..4fc5179
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/Region.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.graphics
+
+import android.graphics.Point
+import android.graphics.Rect
+import android.graphics.Region
+import android.graphics.RegionIterator
+
+/**
+ * Return true if the region contains the specified [Point].
+ */
+inline operator fun Region.contains(p: Point) = contains(p.x, p.y)
+
+/**
+ * Return the union of this region and the specified [Rect] as a new region.
+ */
+inline operator fun Region.plus(r: Rect): Region {
+ return Region(this).apply {
+ union(r)
+ }
+}
+
+/**
+ * Return the union of this region and the specified region as a new region.
+ */
+inline operator fun Region.plus(r: Region): Region {
+ return Region(this).apply {
+ op(r, Region.Op.UNION)
+ }
+}
+
+/**
+ * Return the difference of this region and the specified [Rect] as a new region.
+ */
+inline operator fun Region.minus(r: Rect): Region {
+ return Region(this).apply {
+ op(r, Region.Op.DIFFERENCE)
+ }
+}
+
+/**
+ * Return the difference of this region and the specified region as a new region.
+ */
+inline operator fun Region.minus(r: Region): Region {
+ return Region(this).apply {
+ op(r, Region.Op.DIFFERENCE)
+ }
+}
+
+/**
+ * Returns the negation of this region as a new region.
+ */
+inline operator fun Region.unaryMinus(): Region {
+ return Region(bounds).apply {
+ op(this@unaryMinus, Region.Op.DIFFERENCE)
+ }
+}
+
+/**
+ * Returns the negation of this region as a new region.
+ */
+inline operator fun Region.not() = -this
+
+/**
+ * Return the union of this region and the specified [Rect] as a new region.
+ */
+inline infix fun Region.and(r: Rect) = this + r
+
+/**
+ * Return the union of this region and the specified region as a new region.
+ */
+inline infix fun Region.and(r: Region) = this + r
+
+/**
+ * Return the intersection of this region and the specified [Rect] as a new region.
+ */
+inline infix fun Region.or(r: Rect): Region {
+ return Region(this).apply {
+ op(r, Region.Op.INTERSECT)
+ }
+}
+
+/**
+ * Return the intersection of this region and the specified region as a new region.
+ */
+inline infix fun Region.or(r: Region): Region {
+ return Region(this).apply {
+ op(r, Region.Op.INTERSECT)
+ }
+}
+
+/**
+ * Return the union minus the intersection of this region and the specified [Rect]
+ * as a new region.
+ */
+inline infix fun Region.xor(r: Rect): Region {
+ return Region(this).apply {
+ op(r, Region.Op.XOR)
+ }
+}
+
+/**
+ * Return the union minus the intersection of this region and the specified region
+ * as a new region.
+ */
+inline infix fun Region.xor(r: Region): Region {
+ return Region(this).apply {
+ op(r, Region.Op.XOR)
+ }
+}
+
+/** Performs the given action on each rect in this region. */
+inline fun Region.forEach(action: (rect: Rect) -> Unit) {
+ val iterator = RegionIterator(this)
+ while (true) {
+ val r = Rect()
+ if (!iterator.next(r)) {
+ break
+ }
+ action(r)
+ }
+}
+
+/** Returns an [Iterator] over the rects in this region. */
+operator fun Region.iterator() = object : Iterator<Rect> {
+ private val iterator = RegionIterator(this@iterator)
+ private val rect = Rect()
+ private var hasMore = iterator.next(rect)
+
+ override fun hasNext() = hasMore
+
+ override fun next(): Rect {
+ if (hasMore) {
+ val r = Rect(rect)
+ hasMore = iterator.next(rect)
+ return r
+ }
+ throw IndexOutOfBoundsException()
+ }
+}
diff --git a/core/ktx/src/main/java/androidx/core/graphics/Shader.kt b/core/ktx/src/main/java/androidx/core/graphics/Shader.kt
new file mode 100644
index 0000000..0fb93cb
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/Shader.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics
+
+import android.graphics.Matrix
+import android.graphics.Shader
+
+/**
+ * Wrap the specified [block] in calls to [Shader.getLocalMatrix] and [Shader.setLocalMatrix].
+ */
+inline fun Shader.transform(block: Matrix.() -> Unit) {
+ val matrix = Matrix()
+ getLocalMatrix(matrix)
+ block(matrix)
+ setLocalMatrix(matrix)
+}
diff --git a/core/ktx/src/main/java/androidx/core/graphics/drawable/BitmapDrawable.kt b/core/ktx/src/main/java/androidx/core/graphics/drawable/BitmapDrawable.kt
new file mode 100644
index 0000000..4a96aab
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/drawable/BitmapDrawable.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE", "WRONG_ANNOTATION_TARGET_WITH_USE_SITE_TARGET_ON_TYPE")
+
+package androidx.core.graphics.drawable
+
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.drawable.BitmapDrawable
+
+/** Create a [BitmapDrawable] from this [Bitmap]. */
+inline fun Bitmap.toDrawable(resources: Resources) = BitmapDrawable(resources, this)
diff --git a/core/ktx/src/main/java/androidx/core/graphics/drawable/ColorDrawable.kt b/core/ktx/src/main/java/androidx/core/graphics/drawable/ColorDrawable.kt
new file mode 100644
index 0000000..bef04df
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/drawable/ColorDrawable.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE", "WRONG_ANNOTATION_TARGET_WITH_USE_SITE_TARGET_ON_TYPE")
+
+package androidx.core.graphics.drawable
+
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import androidx.annotation.ColorInt
+import androidx.annotation.RequiresApi
+
+/** Create a [ColorDrawable] from this color value. */
+inline fun @receiver:ColorInt Int.toDrawable() = ColorDrawable(this)
+
+/** Create a [ColorDrawable] from this [Color] (via [Color.toArgb]). */
+@RequiresApi(26)
+inline fun Color.toDrawable() = ColorDrawable(toArgb())
diff --git a/core/ktx/src/main/java/androidx/core/graphics/drawable/Drawable.kt b/core/ktx/src/main/java/androidx/core/graphics/drawable/Drawable.kt
new file mode 100644
index 0000000..9e63bbd
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/drawable/Drawable.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.graphics.drawable
+
+import android.graphics.Bitmap
+import android.graphics.Bitmap.Config
+import android.graphics.Canvas
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import androidx.annotation.Px
+import androidx.core.graphics.component1
+import androidx.core.graphics.component2
+import androidx.core.graphics.component3
+import androidx.core.graphics.component4
+
+/**
+ * Return a [Bitmap] representation of this [Drawable].
+ *
+ * If this instance is a [BitmapDrawable] and the [width], [height], and [config] match, the
+ * underlying [Bitmap] instance will be returned directly. If any of those three properties differ
+ * then a new [Bitmap] is created. For all other [Drawable] types, a new [Bitmap] is created.
+ *
+ * @param width Width of the desired bitmap. Defaults to [Drawable.getIntrinsicWidth].
+ * @param height Height of the desired bitmap. Defaults to [Drawable.getIntrinsicHeight].
+ * @param config Bitmap config of the desired bitmap. Null attempts to use the native config, if
+ * any. Defaults to [Config.ARGB_8888] otherwise.
+ */
+fun Drawable.toBitmap(
+ @Px width: Int = intrinsicWidth,
+ @Px height: Int = intrinsicHeight,
+ config: Config? = null
+): Bitmap {
+ if (this is BitmapDrawable) {
+ if (config == null || bitmap.config == config) {
+ // Fast-path to return original. Bitmap.createScaledBitmap will do this check, but it
+ // involves allocation and two jumps into native code so we perform the check ourselves.
+ if (width == intrinsicWidth && height == intrinsicHeight) {
+ return bitmap
+ }
+ return Bitmap.createScaledBitmap(bitmap, width, height, true)
+ }
+ }
+
+ val (oldLeft, oldTop, oldRight, oldBottom) = bounds
+
+ val bitmap = Bitmap.createBitmap(width, height, config ?: Config.ARGB_8888)
+ setBounds(0, 0, width, height)
+ draw(Canvas(bitmap))
+
+ setBounds(oldLeft, oldTop, oldRight, oldBottom)
+ return bitmap
+}
+
+/**
+ * Updates this drawable's bounds. This version of the method allows using named parameters
+ * to just set one or more axes.
+ *
+ * @see Drawable.setBounds
+ */
+fun Drawable.updateBounds(
+ @Px left: Int = bounds.left,
+ @Px top: Int = bounds.top,
+ @Px right: Int = bounds.right,
+ @Px bottom: Int = bounds.bottom
+) {
+ setBounds(left, top, right, bottom)
+}
diff --git a/core/ktx/src/main/java/androidx/core/graphics/drawable/Icon.kt b/core/ktx/src/main/java/androidx/core/graphics/drawable/Icon.kt
new file mode 100644
index 0000000..a9077e2
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/graphics/drawable/Icon.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.graphics.drawable
+
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.net.Uri
+import androidx.annotation.RequiresApi
+
+/**
+ * Create an [Icon] from this adaptive [Bitmap].
+ *
+ * @see Icon.createWithAdaptiveBitmap
+ */
+@RequiresApi(26)
+inline fun Bitmap.toAdaptiveIcon(): Icon = Icon.createWithAdaptiveBitmap(this)
+
+/**
+ * Create an [Icon] from this [Bitmap].
+ *
+ * @see Icon.createWithBitmap
+ */
+@RequiresApi(26)
+inline fun Bitmap.toIcon(): Icon = Icon.createWithBitmap(this)
+
+/**
+ * Create an [Icon] from this [Uri].
+ *
+ * @see Icon.createWithContentUri
+ */
+@RequiresApi(26)
+inline fun Uri.toIcon(): Icon = Icon.createWithContentUri(this)
+
+/**
+ * Create an [Icon] from this [ByteArray].
+ *
+ * @see Icon.createWithData
+ */
+@RequiresApi(26)
+inline fun ByteArray.toIcon(): Icon = Icon.createWithData(this, 0, size)
diff --git a/core/ktx/src/main/java/androidx/core/location/Location.kt b/core/ktx/src/main/java/androidx/core/location/Location.kt
new file mode 100644
index 0000000..fe48322
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/location/Location.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.location
+
+import android.location.Location
+
+/**
+ * Returns the latitude of this [Location].
+ *
+ * This method allows to use destructuring declarations when working with [Location],
+ * for example:
+ * ```
+ * val (lat, lon) = myLocation
+ * ```
+ */
+inline operator fun Location.component1() = this.latitude
+
+/**
+ * Returns the longitude of this [Location].
+ *
+ * This method allows to use destructuring declarations when working with [Location],
+ * for example:
+ * ```
+ * val (lat, lon) = myLocation
+ * ```
+ */
+inline operator fun Location.component2() = this.longitude
diff --git a/core/ktx/src/main/java/androidx/core/net/Uri.kt b/core/ktx/src/main/java/androidx/core/net/Uri.kt
new file mode 100644
index 0000000..a5d9f1d
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/net/Uri.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.net
+
+import android.net.Uri
+import java.io.File
+
+/**
+ * Creates a Uri from the given encoded URI string.
+ *
+ * @see Uri.parse
+ */
+inline fun String.toUri(): Uri = Uri.parse(this)
+
+/**
+ * Creates a Uri from the given file.
+ *
+ * @see Uri.fromFile
+ */
+inline fun File.toUri(): Uri = Uri.fromFile(this)
+
+/** Creates a [File] from the given [Uri]. */
+inline fun Uri.toFile(): File = File(path)
diff --git a/core/ktx/src/main/java/androidx/core/os/Bundle.kt b/core/ktx/src/main/java/androidx/core/os/Bundle.kt
new file mode 100644
index 0000000..53da45d
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/os/Bundle.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.os
+
+import android.os.Binder
+import android.os.Build
+import android.os.Bundle
+import android.os.Parcelable
+import android.util.Size
+import android.util.SizeF
+import java.io.Serializable
+
+/**
+ * Returns a new [Bundle] with the given key/value pairs as elements.
+ *
+ * @throws IllegalArgumentException When a value is not a supported type of [Bundle].
+ */
+fun bundleOf(vararg pairs: Pair<String, Any?>) = Bundle(pairs.size).apply {
+ for ((key, value) in pairs) {
+ when (value) {
+ null -> putString(key, null) // Any nullable type will suffice.
+
+ // Scalars
+ is Boolean -> putBoolean(key, value)
+ is Byte -> putByte(key, value)
+ is Char -> putChar(key, value)
+ is Double -> putDouble(key, value)
+ is Float -> putFloat(key, value)
+ is Int -> putInt(key, value)
+ is Long -> putLong(key, value)
+ is Short -> putShort(key, value)
+
+ // References
+ is Bundle -> putBundle(key, value)
+ is CharSequence -> putCharSequence(key, value)
+ is Parcelable -> putParcelable(key, value)
+
+ // Scalar arrays
+ is BooleanArray -> putBooleanArray(key, value)
+ is ByteArray -> putByteArray(key, value)
+ is CharArray -> putCharArray(key, value)
+ is DoubleArray -> putDoubleArray(key, value)
+ is FloatArray -> putFloatArray(key, value)
+ is IntArray -> putIntArray(key, value)
+ is LongArray -> putLongArray(key, value)
+ is ShortArray -> putShortArray(key, value)
+
+ // Reference arrays
+ is Array<*> -> {
+ val componentType = value::class.java.componentType
+ @Suppress("UNCHECKED_CAST") // Checked by reflection.
+ when {
+ Parcelable::class.java.isAssignableFrom(componentType) -> {
+ putParcelableArray(key, value as Array<Parcelable>)
+ }
+ String::class.java.isAssignableFrom(componentType) -> {
+ putStringArray(key, value as Array<String>)
+ }
+ CharSequence::class.java.isAssignableFrom(componentType) -> {
+ putCharSequenceArray(key, value as Array<CharSequence>)
+ }
+ Serializable::class.java.isAssignableFrom(componentType) -> {
+ putSerializable(key, value)
+ }
+ else -> {
+ val valueType = componentType.canonicalName
+ throw IllegalArgumentException(
+ "Illegal value array type $valueType for key \"$key\"")
+ }
+ }
+ }
+
+ // Last resort. Also we must check this after Array<*> as all arrays are serializable.
+ is Serializable -> putSerializable(key, value)
+
+ else -> {
+ if (Build.VERSION.SDK_INT >= 18 && value is Binder) {
+ putBinder(key, value)
+ } else if (Build.VERSION.SDK_INT >= 21 && value is Size) {
+ putSize(key, value)
+ } else if (Build.VERSION.SDK_INT >= 21 && value is SizeF) {
+ putSizeF(key, value)
+ } else {
+ val valueType = value.javaClass.canonicalName
+ throw IllegalArgumentException("Illegal value type $valueType for key \"$key\"")
+ }
+ }
+ }
+ }
+}
diff --git a/core/ktx/src/main/java/androidx/core/os/Handler.kt b/core/ktx/src/main/java/androidx/core/os/Handler.kt
new file mode 100644
index 0000000..de09a9b
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/os/Handler.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.os
+
+import android.os.Handler
+
+/**
+ * Version of [Handler.postDelayed] which re-orders the parameters, allowing the action to be
+ * placed outside of parentheses.
+ *
+ * ```
+ * handler.postDelayed(200) {
+ * doSomething()
+ * }
+ * ```
+ *
+ * @return the created Runnable
+ */
+inline fun Handler.postDelayed(
+ delayInMillis: Long,
+ token: Any? = null,
+ crossinline action: () -> Unit
+): Runnable {
+ val runnable = Runnable { action() }
+ if (token == null) {
+ postDelayed(runnable, delayInMillis)
+ } else {
+ HandlerCompat.postDelayed(this, runnable, token, delayInMillis)
+ }
+ return runnable
+}
+
+/**
+ * Version of [Handler.postAtTime] which re-orders the parameters, allowing the action to be
+ * placed outside of parentheses.
+ *
+ * ```
+ * handler.postAtTime(200) {
+ * doSomething()
+ * }
+ * ```
+ *
+ * @param token An optional object with which the posted message will be associated.
+ * @return the created Runnable
+ */
+inline fun Handler.postAtTime(
+ uptimeMillis: Long,
+ token: Any? = null,
+ crossinline action: () -> Unit
+): Runnable {
+ val runnable = Runnable { action() }
+ postAtTime(runnable, token, uptimeMillis)
+ return runnable
+}
diff --git a/core/ktx/src/main/java/androidx/core/os/PersistableBundle.kt b/core/ktx/src/main/java/androidx/core/os/PersistableBundle.kt
new file mode 100644
index 0000000..261e72b
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/os/PersistableBundle.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.os
+
+import android.os.Build
+import android.os.PersistableBundle
+import androidx.annotation.RequiresApi
+
+/**
+ * Returns a new [PersistableBundle] with the given key/value pairs as elements.
+ *
+ * @throws IllegalArgumentException When a value is not a supported type of [PersistableBundle].
+ */
+@RequiresApi(21)
+fun persistableBundleOf(vararg pairs: Pair<String, Any?>) = PersistableBundle(pairs.size).apply {
+ for ((key, value) in pairs) {
+ when (value) {
+ null -> putString(key, null) // Any nullable type will suffice.
+
+ // Scalars
+ is Boolean -> {
+ if (Build.VERSION.SDK_INT >= 22) {
+ putBoolean(key, value)
+ } else {
+ throw IllegalArgumentException("Illegal value type boolean for key \"$key\"")
+ }
+ }
+ is Double -> putDouble(key, value)
+ is Int -> putInt(key, value)
+ is Long -> putLong(key, value)
+
+ // References
+ is String -> putString(key, value)
+
+ // Scalar arrays
+ is BooleanArray -> {
+ if (Build.VERSION.SDK_INT >= 22) {
+ putBooleanArray(key, value)
+ } else {
+ throw IllegalArgumentException("Illegal value type boolean[] for key \"$key\"")
+ }
+ }
+ is DoubleArray -> putDoubleArray(key, value)
+ is IntArray -> putIntArray(key, value)
+ is LongArray -> putLongArray(key, value)
+
+ // Reference arrays
+ is Array<*> -> {
+ val componentType = value::class.java.componentType
+ @Suppress("UNCHECKED_CAST") // Checked by reflection.
+ when {
+ String::class.java.isAssignableFrom(componentType) -> {
+ putStringArray(key, value as Array<String>)
+ }
+ else -> {
+ val valueType = componentType.canonicalName
+ throw IllegalArgumentException(
+ "Illegal value array type $valueType for key \"$key\"")
+ }
+ }
+ }
+
+ else -> {
+ val valueType = value.javaClass.canonicalName
+ throw IllegalArgumentException("Illegal value type $valueType for key \"$key\"")
+ }
+ }
+ }
+}
diff --git a/core/ktx/src/main/java/androidx/core/os/Trace.kt b/core/ktx/src/main/java/androidx/core/os/Trace.kt
new file mode 100644
index 0000000..43bf85e
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/os/Trace.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.os
+
+import android.os.Trace
+
+/**
+ * Wrap the specified [block] in calls to [Trace.beginSection] (with the supplied [sectionName])
+ * and [Trace.endSection].
+ */
+inline fun <T> trace(sectionName: String, block: () -> T): T {
+ TraceCompat.beginSection(sectionName)
+ try {
+ return block()
+ } finally {
+ TraceCompat.endSection()
+ }
+}
diff --git a/core/ktx/src/main/java/androidx/core/preference/PreferenceGroup.kt b/core/ktx/src/main/java/androidx/core/preference/PreferenceGroup.kt
new file mode 100644
index 0000000..2efab64
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/preference/PreferenceGroup.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.preference
+
+import android.preference.Preference
+import android.preference.PreferenceGroup
+
+/**
+ * Returns the preference with `key`.
+ *
+ * @throws NullPointerException if no preference is found with that key.
+ */
+inline operator fun PreferenceGroup.get(key: CharSequence): Preference = findPreference(key)
+
+/**
+ * Returns the preference at `index`.
+ *
+ * @throws IndexOutOfBoundsException if index is less than 0 or greater than or equal to the count.
+ */
+operator fun PreferenceGroup.get(index: Int): Preference = getPreference(index)
+ ?: throw IndexOutOfBoundsException("Index: $index, Size: $preferenceCount")
+
+/** Returns `true` if `preference` is found in this preference group. */
+operator fun PreferenceGroup.contains(preference: Preference): Boolean {
+ for (index in 0 until preferenceCount) {
+ if (getPreference(index) == preference) {
+ return true
+ }
+ }
+ return false
+}
+
+/** Adds `preference` to this preference group. */
+inline operator fun PreferenceGroup.plusAssign(preference: Preference) {
+ addPreference(preference)
+}
+
+/** Removes `preference` from this preference group. */
+inline operator fun PreferenceGroup.minusAssign(preference: Preference) {
+ removePreference(preference)
+}
+
+/** Returns the number of preferences in this preference group. */
+inline val PreferenceGroup.size: Int get() = preferenceCount
+
+/** Returns true if this preference group contains no preferences. */
+inline fun PreferenceGroup.isEmpty(): Boolean = size == 0
+
+/** Returns true if this preference group contains one or more preferences. */
+inline fun PreferenceGroup.isNotEmpty(): Boolean = size != 0
+
+/** Performs the given action on each preference in this preference group. */
+inline fun PreferenceGroup.forEach(action: (preference: Preference) -> Unit) {
+ for (index in 0 until preferenceCount) {
+ action(get(index))
+ }
+}
+
+/** Performs the given action on each preference in this preference group, providing its sequential index. */
+inline fun PreferenceGroup.forEachIndexed(action: (index: Int, preference: Preference) -> Unit) {
+ for (index in 0 until preferenceCount) {
+ action(index, get(index))
+ }
+}
+
+/** Returns a [MutableIterator] over the preferences in this preference group. */
+operator fun PreferenceGroup.iterator() = object : MutableIterator<Preference> {
+ private var index = 0
+ override fun hasNext() = index < size
+ override fun next() = getPreference(index++) ?: throw IndexOutOfBoundsException()
+ override fun remove() {
+ removePreference(getPreference(--index))
+ }
+}
+
+/** Returns a [Sequence] over the preferences in this preference group. */
+val PreferenceGroup.children: Sequence<Preference>
+ get() = object : Sequence<Preference> {
+ override fun iterator() = this@children.iterator()
+ }
diff --git a/core/ktx/src/main/java/androidx/core/text/CharSequence.kt b/core/ktx/src/main/java/androidx/core/text/CharSequence.kt
new file mode 100644
index 0000000..d41aed2
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/text/CharSequence.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.text
+
+import android.text.TextUtils
+
+/**
+ * Returns whether the given [CharSequence] contains only digits.
+ *
+ * @see TextUtils.isDigitsOnly
+ */
+inline fun CharSequence.isDigitsOnly() = TextUtils.isDigitsOnly(this)
+
+/**
+ * Returns the length that the specified [CharSequence] would have if spaces and ASCII control
+ * characters were trimmed from the start and end, as by [String.trim].
+ *
+ * @see TextUtils.getTrimmedLength
+ */
+inline fun CharSequence.trimmedLength() = TextUtils.getTrimmedLength(this)
diff --git a/core/ktx/src/main/java/androidx/core/text/Html.kt b/core/ktx/src/main/java/androidx/core/text/Html.kt
new file mode 100644
index 0000000..d2636ee
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/text/Html.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.text
+
+import android.annotation.SuppressLint
+import android.text.Html
+import android.text.Html.FROM_HTML_MODE_LEGACY
+import android.text.Html.ImageGetter
+import android.text.Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE
+import android.text.Html.TagHandler
+import android.text.Spanned
+
+/**
+ * Returns a [Spanned] from parsing this string as HTML.
+ *
+ * @param flags Additional option to set the behavior of the HTML parsing. Default is set to
+ * [Html.FROM_HTML_MODE_LEGACY] which was introduced in API 24.
+ * @param imageGetter Returns displayable styled text from the provided HTML string.
+ * @param tagHandler Notified when HTML tags are encountered a tag the parser does
+ * not know how to interpret.
+ *
+ * @see Html.fromHtml
+ */
+fun String.parseAsHtml(
+ @SuppressLint("InlinedApi") flags: Int = FROM_HTML_MODE_LEGACY,
+ imageGetter: ImageGetter? = null,
+ tagHandler: TagHandler? = null
+): Spanned = HtmlCompat.fromHtml(this, flags, imageGetter, tagHandler)
+
+/**
+ * Returns a string of HTML from the spans in this [Spanned].
+ *
+ * @see Html.toHtml
+ */
+fun Spanned.toHtml(
+ @SuppressLint("InlinedApi") option: Int = TO_HTML_PARAGRAPH_LINES_CONSECUTIVE
+): String = HtmlCompat.toHtml(this, option)
diff --git a/core/ktx/src/main/java/androidx/core/text/SpannableString.kt b/core/ktx/src/main/java/androidx/core/text/SpannableString.kt
new file mode 100644
index 0000000..29aa9c2
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/text/SpannableString.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.text
+
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.Spanned.SPAN_INCLUSIVE_EXCLUSIVE
+
+/**
+ * Returns a new [Spannable] from [CharSequence],
+ * or the source itself if it is already an instance of [SpannableString].
+ */
+inline fun CharSequence.toSpannable(): Spannable = SpannableString.valueOf(this)
+
+/** Adds [span] to the entire text. */
+inline operator fun Spannable.plusAssign(span: Any) =
+ setSpan(span, 0, length, SPAN_INCLUSIVE_EXCLUSIVE)
+
+/** Removes [span] from this text. */
+inline operator fun Spannable.minusAssign(span: Any) = removeSpan(span)
+
+/** Clear all spans from this text. */
+inline fun Spannable.clearSpans() = getSpans<Any>().forEach { removeSpan(it) }
+
+/**
+ * Add [span] to the range [start]…[end] of the text.
+ *
+ * ```
+ * val s = "Hello, World!".toSpannable()
+ * s[0, 5] = UnderlineSpan()
+ * ```
+ *
+ * Note: The [end] value is exclusive.
+ *
+ * @see Spannable.setSpan
+ */
+inline operator fun Spannable.set(start: Int, end: Int, span: Any) {
+ setSpan(span, start, end, SPAN_INCLUSIVE_EXCLUSIVE)
+}
+
+/**
+ * Add [span] to the [range] of the text.
+ *
+ * ```
+ * val s = "Hello, World!".toSpannable()
+ * s[0..5] = UnderlineSpan()
+ * ```
+ *
+ * Note: The range end value is exclusive.
+ *
+ * @see Spannable.setSpan
+ */
+inline operator fun Spannable.set(range: IntRange, span: Any) {
+ // This looks weird, but endInclusive is just the exact upper value.
+ setSpan(span, range.start, range.endInclusive, SPAN_INCLUSIVE_EXCLUSIVE)
+}
diff --git a/core/ktx/src/main/java/androidx/core/text/SpannableStringBuilder.kt b/core/ktx/src/main/java/androidx/core/text/SpannableStringBuilder.kt
new file mode 100644
index 0000000..e49ccd2
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/text/SpannableStringBuilder.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.text
+
+import android.graphics.Typeface.BOLD
+import android.graphics.Typeface.ITALIC
+import android.text.Spannable.SPAN_INCLUSIVE_EXCLUSIVE
+import android.text.SpannableStringBuilder
+import android.text.SpannedString
+import android.text.style.BackgroundColorSpan
+import android.text.style.ForegroundColorSpan
+import android.text.style.RelativeSizeSpan
+import android.text.style.StrikethroughSpan
+import android.text.style.StyleSpan
+import android.text.style.SubscriptSpan
+import android.text.style.SuperscriptSpan
+import android.text.style.UnderlineSpan
+import androidx.annotation.ColorInt
+
+/**
+ * Builds new string by populating a newly created [SpannableStringBuilder] using the provided
+ * [builderAction] and then converting it to [SpannedString].
+ */
+inline fun buildSpannedString(builderAction: SpannableStringBuilder.() -> Unit): SpannedString {
+ val builder = SpannableStringBuilder()
+ builder.builderAction()
+ return SpannedString(builder)
+}
+
+/**
+ * Wrap appended text in [builderAction] in [spans].
+ *
+ * Note: the spans will only have the correct position if the [builderAction] only appends or
+ * replaces text. Inserting, deleting, or clearing the text will cause the span to be placed at
+ * an incorrect position.
+ */
+inline fun SpannableStringBuilder.inSpans(
+ vararg spans: Any,
+ builderAction: SpannableStringBuilder.() -> Unit
+): SpannableStringBuilder {
+ val start = length
+ builderAction()
+ for (span in spans) setSpan(span, start, length, SPAN_INCLUSIVE_EXCLUSIVE)
+ return this
+}
+
+/**
+ * Wrap appended text in [builderAction] in [span].
+ *
+ * Note: the span will only have the correct position if the `builderAction` only appends or
+ * replaces text. Inserting, deleting, or clearing the text will cause the span to be placed at
+ * an incorrect position.
+ */
+inline fun SpannableStringBuilder.inSpans(
+ span: Any,
+ builderAction: SpannableStringBuilder.() -> Unit
+): SpannableStringBuilder {
+ val start = length
+ builderAction()
+ setSpan(span, start, length, SPAN_INCLUSIVE_EXCLUSIVE)
+ return this
+}
+
+/**
+ * Wrap appended text in [builderAction] in a bold [StyleSpan].
+ *
+ * @see SpannableStringBuilder.inSpans
+ */
+inline fun SpannableStringBuilder.bold(builderAction: SpannableStringBuilder.() -> Unit) =
+ inSpans(StyleSpan(BOLD), builderAction = builderAction)
+
+/**
+ * Wrap appended text in [builderAction] in an italic [StyleSpan].
+ *
+ * @see SpannableStringBuilder.inSpans
+ */
+inline fun SpannableStringBuilder.italic(builderAction: SpannableStringBuilder.() -> Unit) =
+ inSpans(StyleSpan(ITALIC), builderAction = builderAction)
+
+/**
+ * Wrap appended text in [builderAction] in an [UnderlineSpan].
+ *
+ * @see SpannableStringBuilder.inSpans
+ */
+inline fun SpannableStringBuilder.underline(builderAction: SpannableStringBuilder.() -> Unit) =
+ inSpans(UnderlineSpan(), builderAction = builderAction)
+
+/**
+ * Wrap appended text in [builderAction] in a [ForegroundColorSpan].
+ *
+ * @see SpannableStringBuilder.inSpans
+ */
+inline fun SpannableStringBuilder.color(
+ @ColorInt color: Int,
+ builderAction: SpannableStringBuilder.() -> Unit
+) = inSpans(ForegroundColorSpan(color), builderAction = builderAction)
+
+/**
+ * Wrap appended text in [builderAction] in a [BackgroundColorSpan].
+ *
+ * @see SpannableStringBuilder.inSpans
+ */
+inline fun SpannableStringBuilder.backgroundColor(
+ @ColorInt color: Int,
+ builderAction: SpannableStringBuilder.() -> Unit
+) = inSpans(BackgroundColorSpan(color), builderAction = builderAction)
+
+/**
+ * Wrap appended text in [builderAction] in a [StrikethroughSpan].
+ *
+ * @see SpannableStringBuilder.inSpans
+ */
+inline fun SpannableStringBuilder.strikeThrough(builderAction: SpannableStringBuilder.() -> Unit) =
+ inSpans(StrikethroughSpan(), builderAction = builderAction)
+
+/**
+ * Wrap appended text in [builderAction] in a [RelativeSizeSpan].
+ *
+ * @see SpannableStringBuilder.inSpans
+ */
+inline fun SpannableStringBuilder.scale(
+ proportion: Float,
+ builderAction: SpannableStringBuilder.() -> Unit
+) = inSpans(RelativeSizeSpan(proportion), builderAction = builderAction)
+
+/**
+ * Wrap appended text in [builderAction] in a [SuperscriptSpan].
+ *
+ * @see SpannableStringBuilder.inSpans
+ */
+inline fun SpannableStringBuilder.superscript(builderAction: SpannableStringBuilder.() -> Unit) =
+ inSpans(SuperscriptSpan(), builderAction = builderAction)
+
+/**
+ * Wrap appended text in [builderAction] in a [SubscriptSpan].
+ *
+ * @see SpannableStringBuilder.inSpans
+ */
+inline fun SpannableStringBuilder.subscript(builderAction: SpannableStringBuilder.() -> Unit) =
+ inSpans(SubscriptSpan(), builderAction = builderAction)
diff --git a/core/ktx/src/main/java/androidx/core/text/SpannedString.kt b/core/ktx/src/main/java/androidx/core/text/SpannedString.kt
new file mode 100644
index 0000000..db27511
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/text/SpannedString.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.text
+
+import android.text.Spanned
+import android.text.SpannedString
+
+/**
+ * Returns a new [Spanned] from [CharSequence],
+ * or the source itself if it is already an instance of [SpannedString].
+ */
+inline fun CharSequence.toSpanned(): Spanned = SpannedString.valueOf(this)
+
+/** Get all spans that are instance of [T]. */
+inline fun <reified T : Any> Spanned.getSpans(start: Int = 0, end: Int = length): Array<out T> =
+ getSpans(start, end, T::class.java)
diff --git a/core/ktx/src/main/java/androidx/core/text/String.kt b/core/ktx/src/main/java/androidx/core/text/String.kt
new file mode 100644
index 0000000..8bffdd0
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/text/String.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.text
+
+import android.text.TextUtils
+
+/**
+ * Html-encode the string.
+ *
+ * @see TextUtils.htmlEncode
+ */
+inline fun String.htmlEncode(): String = TextUtils.htmlEncode(this)
diff --git a/core/ktx/src/main/java/androidx/core/transition/Transition.kt b/core/ktx/src/main/java/androidx/core/transition/Transition.kt
new file mode 100644
index 0000000..b79f2cb
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/transition/Transition.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.transition
+
+import android.transition.Transition
+import androidx.annotation.RequiresApi
+
+/**
+ * Add an action which will be invoked when this transition has ended.
+ */
+@RequiresApi(19)
+fun Transition.doOnEnd(action: (transition: Transition) -> Unit) {
+ addListener(onEnd = action)
+}
+
+/**
+ * Add an action which will be invoked when this transition has started.
+ */
+@RequiresApi(19)
+fun Transition.doOnStart(action: (transition: Transition) -> Unit) {
+ addListener(onStart = action)
+}
+
+/**
+ * Add an action which will be invoked when this transition has been cancelled.
+ */
+@RequiresApi(19)
+fun Transition.doOnCancel(action: (transition: Transition) -> Unit) {
+ addListener(onCancel = action)
+}
+
+/**
+ * Add an action which will be invoked when this transition has resumed after a pause.
+ */
+@RequiresApi(19)
+fun Transition.doOnResume(action: (transition: Transition) -> Unit) {
+ addListener(onResume = action)
+}
+
+/**
+ * Add an action which will be invoked when this transition has been paused.
+ */
+@RequiresApi(19)
+fun Transition.doOnPause(action: (transition: Transition) -> Unit) {
+ addListener(onPause = action)
+}
+
+/**
+ * Add a listener to this Transition using the provided actions.
+ */
+@RequiresApi(19)
+fun Transition.addListener(
+ onEnd: ((transition: Transition) -> Unit)? = null,
+ onStart: ((transition: Transition) -> Unit)? = null,
+ onCancel: ((transition: Transition) -> Unit)? = null,
+ onResume: ((transition: Transition) -> Unit)? = null,
+ onPause: ((transition: Transition) -> Unit)? = null
+) {
+ addListener(object : Transition.TransitionListener {
+ override fun onTransitionEnd(transition: Transition) {
+ onEnd?.invoke(transition)
+ }
+
+ override fun onTransitionResume(transition: Transition) {
+ onResume?.invoke(transition)
+ }
+
+ override fun onTransitionPause(transition: Transition) {
+ onPause?.invoke(transition)
+ }
+
+ override fun onTransitionCancel(transition: Transition) {
+ onCancel?.invoke(transition)
+ }
+
+ override fun onTransitionStart(transition: Transition) {
+ onStart?.invoke(transition)
+ }
+ })
+}
diff --git a/core/ktx/src/main/java/androidx/core/util/ArrayMap.kt b/core/ktx/src/main/java/androidx/core/util/ArrayMap.kt
new file mode 100644
index 0000000..b676403
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/ArrayMap.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.util
+
+import android.util.ArrayMap
+import androidx.annotation.RequiresApi
+import kotlin.Pair
+
+/** Returns an empty new [ArrayMap]. */
+@RequiresApi(19)
+inline fun <K, V> arrayMapOf(): ArrayMap<K, V> = ArrayMap()
+
+/**
+ * Returns a new [ArrayMap] with the specified contents, given as a list of pairs where the first
+ * component is the key and the second component is the value.
+ *
+ * If multiple pairs have the same key, the resulting map will contain the value from the last of
+ * those pairs.
+ */
+@RequiresApi(19)
+fun <K, V> arrayMapOf(vararg pairs: Pair<K, V>): ArrayMap<K, V> {
+ val map = ArrayMap<K, V>(pairs.size)
+ for (pair in pairs) {
+ map[pair.first] = pair.second
+ }
+ return map
+}
diff --git a/core/ktx/src/main/java/androidx/core/util/ArraySet.kt b/core/ktx/src/main/java/androidx/core/util/ArraySet.kt
new file mode 100644
index 0000000..6773d23
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/ArraySet.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.util
+
+import android.util.ArraySet
+import androidx.annotation.RequiresApi
+
+/** Returns an empty new [ArraySet]. */
+@RequiresApi(23)
+inline fun <T> arraySetOf(): ArraySet<T> = ArraySet()
+
+/** Returns a new [ArraySet] with the specified contents. */
+@RequiresApi(23)
+fun <T> arraySetOf(vararg values: T): ArraySet<T> {
+ val set = ArraySet<T>(values.size)
+ @Suppress("LoopToCallChain") // Causes needless copy to a list.
+ for (value in values) {
+ set.add(value)
+ }
+ return set
+}
diff --git a/core/ktx/src/main/java/androidx/core/util/AtomicFile.kt b/core/ktx/src/main/java/androidx/core/util/AtomicFile.kt
new file mode 100644
index 0000000..b8fbba7
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/AtomicFile.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to other public API.
+
+package androidx.core.util
+
+import android.util.AtomicFile
+import androidx.annotation.RequiresApi
+import java.io.FileOutputStream
+import java.nio.charset.Charset
+
+/**
+ * Perform the write operations inside [block] on this file. If [block] throws an exception the
+ * write will be failed. Otherwise the write will be applied atomically to the file.
+ */
+@RequiresApi(17)
+inline fun AtomicFile.tryWrite(block: (out: FileOutputStream) -> Unit) {
+ val stream = startWrite()
+ var success = false
+ try {
+ block(stream)
+ success = true
+ } finally {
+ if (success) {
+ finishWrite(stream)
+ } else {
+ failWrite(stream)
+ }
+ }
+}
+
+/**
+ * Sets the content of this file as an [array] of bytes.
+ */
+@RequiresApi(17)
+fun AtomicFile.writeBytes(array: ByteArray) {
+ tryWrite {
+ it.write(array)
+ }
+}
+
+/**
+ * Sets the content of this file as [text] encoded using UTF-8 or specified [charset].
+ * If this file exists, it becomes overwritten.
+ */
+@RequiresApi(17)
+fun AtomicFile.writeText(text: String, charset: Charset = Charsets.UTF_8) {
+ writeBytes(text.toByteArray(charset))
+}
+
+/**
+ * Gets the entire content of this file as a byte array.
+ *
+ * This method is not recommended on huge files. It has an internal limitation of 2 GB file size.
+ */
+@RequiresApi(17)
+inline fun AtomicFile.readBytes(): ByteArray = readFully()
+
+/**
+ * Gets the entire content of this file as a String using UTF-8 or specified [charset].
+ *
+ * This method is not recommended on huge files. It has an internal limitation of 2 GB file size.
+ */
+@RequiresApi(17)
+fun AtomicFile.readText(charset: Charset = Charsets.UTF_8): String {
+ return readFully().toString(charset)
+}
diff --git a/core/ktx/src/main/java/androidx/core/util/Half.kt b/core/ktx/src/main/java/androidx/core/util/Half.kt
new file mode 100644
index 0000000..409c01e
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/Half.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to other public API.
+
+package androidx.core.util
+
+import android.util.Half
+import androidx.annotation.HalfFloat
+import androidx.annotation.RequiresApi
+
+/**
+ * Returns a [Half] instance representing given [Short].
+ *
+ * @see Half.valueOf
+ */
+// TODO https://youtrack.jetbrains.com/issue/KT-21696
+@Suppress("WRONG_ANNOTATION_TARGET_WITH_USE_SITE_TARGET_ON_TYPE")
+@RequiresApi(26)
+inline fun @receiver:HalfFloat Short.toHalf(): Half = Half.valueOf(this)
+
+/**
+ * Returns a [Half] instance representing given [Float].
+ *
+ * @see Half.valueOf
+ */
+@RequiresApi(26)
+inline fun Float.toHalf(): Half = Half.valueOf(this)
+
+/**
+ * Returns a [Half] instance representing given [Double].
+ *
+ * @see Half.valueOf
+ */
+@RequiresApi(26)
+inline fun Double.toHalf(): Half = toFloat().toHalf()
+
+/**
+ * Returns a [Half] instance representing given [String].
+ *
+ * @see Half.valueOf
+ */
+@RequiresApi(26)
+inline fun String.toHalf(): Half = Half.valueOf(this)
diff --git a/core/ktx/src/main/java/androidx/core/util/Locale.kt b/core/ktx/src/main/java/androidx/core/util/Locale.kt
new file mode 100644
index 0000000..2b4312e
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/Locale.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.text.TextUtils
+import androidx.annotation.RequiresApi
+import java.util.Locale
+
+/**
+ * Returns layout direction for a given locale.
+ * @see TextUtils.getLayoutDirectionFromLocale
+ */
+val Locale.layoutDirection: Int
+ @RequiresApi(17)
+ get() = TextUtils.getLayoutDirectionFromLocale(this)
diff --git a/core/ktx/src/main/java/androidx/core/util/LongSparseArray.kt b/core/ktx/src/main/java/androidx/core/util/LongSparseArray.kt
new file mode 100644
index 0000000..cd9eb4c
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/LongSparseArray.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.util
+
+import android.util.LongSparseArray
+import androidx.annotation.RequiresApi
+
+/** Returns the number of key/value pairs in the collection. */
+@get:RequiresApi(16)
+inline val <T> LongSparseArray<T>.size get() = size()
+
+/** Returns true if the collection contains [key]. */
+@RequiresApi(16)
+inline operator fun <T> LongSparseArray<T>.contains(key: Long) = indexOfKey(key) >= 0
+
+/** Allows the use of the index operator for storing values in the collection. */
+@RequiresApi(16)
+inline operator fun <T> LongSparseArray<T>.set(key: Long, value: T) = put(key, value)
+
+/** Creates a new collection by adding or replacing entries from [other]. */
+@RequiresApi(16)
+operator fun <T> LongSparseArray<T>.plus(other: LongSparseArray<T>): LongSparseArray<T> {
+ val new = LongSparseArray<T>(size() + other.size())
+ new.putAll(this)
+ new.putAll(other)
+ return new
+}
+
+/** Returns true if the collection contains [key]. */
+@RequiresApi(16)
+inline fun <T> LongSparseArray<T>.containsKey(key: Long) = indexOfKey(key) >= 0
+
+/** Returns true if the collection contains [value]. */
+@RequiresApi(16)
+inline fun <T> LongSparseArray<T>.containsValue(value: T) = indexOfValue(value) != -1
+
+/** Return the value corresponding to [key], or [defaultValue] when not present. */
+@RequiresApi(16)
+inline fun <T> LongSparseArray<T>.getOrDefault(key: Long, defaultValue: T) =
+ get(key) ?: defaultValue
+
+/** Return the value corresponding to [key], or from [defaultValue] when not present. */
+@RequiresApi(16)
+inline fun <T> LongSparseArray<T>.getOrElse(key: Long, defaultValue: () -> T) =
+ get(key) ?: defaultValue()
+
+/** Return true when the collection contains no elements. */
+@RequiresApi(16)
+inline fun <T> LongSparseArray<T>.isEmpty() = size() == 0
+
+/** Return true when the collection contains elements. */
+@RequiresApi(16)
+inline fun <T> LongSparseArray<T>.isNotEmpty() = size() != 0
+
+/** Removes the entry for [key] only if it is mapped to [value]. */
+@RequiresApi(16)
+fun <T> LongSparseArray<T>.remove(key: Long, value: T): Boolean {
+ val index = indexOfKey(key)
+ if (index != -1 && value == valueAt(index)) {
+ removeAt(index)
+ return true
+ }
+ return false
+}
+
+/** Update this collection by adding or replacing entries from [other]. */
+@RequiresApi(16)
+fun <T> LongSparseArray<T>.putAll(other: LongSparseArray<T>) = other.forEach(::put)
+
+/** Performs the given [action] for each key/value entry. */
+@RequiresApi(16)
+inline fun <T> LongSparseArray<T>.forEach(action: (key: Long, value: T) -> Unit) {
+ for (index in 0 until size()) {
+ action(keyAt(index), valueAt(index))
+ }
+}
+
+/** Return an iterator over the collection's keys. */
+@RequiresApi(16)
+fun <T> LongSparseArray<T>.keyIterator(): LongIterator = object : LongIterator() {
+ var index = 0
+ override fun hasNext() = index < size()
+ override fun nextLong() = keyAt(index++)
+}
+
+/** Return an iterator over the collection's values. */
+@RequiresApi(16)
+fun <T> LongSparseArray<T>.valueIterator(): Iterator<T> = object : Iterator<T> {
+ var index = 0
+ override fun hasNext() = index < size()
+ override fun next() = valueAt(index++)
+}
diff --git a/core/ktx/src/main/java/androidx/core/util/LruCache.kt b/core/ktx/src/main/java/androidx/core/util/LruCache.kt
new file mode 100644
index 0000000..44c2f8d
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/LruCache.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.util
+
+import android.util.LruCache
+
+/**
+ * Creates an [LruCache] with the given parameters.
+ *
+ * @param maxSize for caches that do not specify [sizeOf], this is
+ * the maximum number of entries in the cache. For all other caches,
+ * this is the maximum sum of the sizes of the entries in this cache.
+ * @param sizeOf function that returns the size of the entry for key and value in
+ * user-defined units. The default implementation returns 1.
+ * @param create a create called after a cache miss to compute a value for the corresponding key.
+ * Returns the computed value or null if no value can be computed. The default implementation
+ * returns null.
+ * @param onEntryRemoved a function called for entries that have been evicted or removed.
+ *
+ * @see LruCache.sizeOf
+ * @see LruCache.create
+ * @see LruCache.entryRemoved
+ */
+inline fun <K : Any, V : Any> lruCache(
+ maxSize: Int,
+ crossinline sizeOf: (key: K, value: V) -> Int = { _, _ -> 1 },
+ @Suppress("USELESS_CAST") // https://youtrack.jetbrains.com/issue/KT-21946
+ crossinline create: (key: K) -> V? = { null as V? },
+ crossinline onEntryRemoved: (evicted: Boolean, key: K, oldValue: V, newValue: V?) -> Unit =
+ { _, _, _, _ -> }
+): LruCache<K, V> {
+ return object : LruCache<K, V>(maxSize) {
+ override fun sizeOf(key: K, value: V) = sizeOf(key, value)
+ override fun create(key: K) = create(key)
+ override fun entryRemoved(evicted: Boolean, key: K, oldValue: V, newValue: V?) {
+ onEntryRemoved(evicted, key, oldValue, newValue)
+ }
+ }
+}
diff --git a/core/ktx/src/main/java/androidx/core/util/Pair.kt b/core/ktx/src/main/java/androidx/core/util/Pair.kt
new file mode 100644
index 0000000..cf12156
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/Pair.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.util
+
+import android.util.Pair
+
+/**
+ * Returns the first component of the pair.
+ *
+ * This method allows to use destructuring declarations when working with pairs, for example:
+ * ```
+ * val (first, second) = myPair
+ * ```
+ */
+@Suppress("HasPlatformType") // Intentionally propagating platform type with unknown nullability.
+inline operator fun <F, S> Pair<F, S>.component1() = first
+
+/**
+ * Returns the second component of the pair.
+ *
+ * This method allows to use destructuring declarations when working with pairs, for example:
+ * ```
+ * val (first, second) = myPair
+ * ```
+ */
+@Suppress("HasPlatformType") // Intentionally propagating platform type with unknown nullability.
+inline operator fun <F, S> Pair<F, S>.component2() = second
+
+/** Returns this [Pair] as a [kotlin.Pair]. */
+inline fun <F, S> Pair<F, S>.toKotlinPair() = kotlin.Pair(first, second)
+
+/** Returns this [kotlin.Pair] as an Android [Pair]. */
+// Note: the return type is explicitly specified here to prevent always seeing platform types.
+inline fun <F, S> kotlin.Pair<F, S>.toAndroidPair(): Pair<F, S> = Pair(first, second)
diff --git a/core/ktx/src/main/java/androidx/core/util/Range.kt b/core/ktx/src/main/java/androidx/core/util/Range.kt
new file mode 100644
index 0000000..b5f7eb6
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/Range.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.util
+
+import android.util.Range
+import androidx.annotation.RequiresApi
+
+/**
+ * Creates a range from this [Comparable] value to [that].
+ *
+ * @throws IllegalArgumentException if this value is comparatively smaller than [that].
+ */
+@RequiresApi(21)
+inline infix fun <T : Comparable<T>> T.rangeTo(that: T): Range<T> = Range(this, that)
+
+/** Return the smallest range that includes this and [value]. */
+@RequiresApi(21)
+inline operator fun <T : Comparable<T>> Range<T>.plus(value: T): Range<T> = extend(value)
+
+/** Return the smallest range that includes this and [other]. */
+@RequiresApi(21)
+inline operator fun <T : Comparable<T>> Range<T>.plus(other: Range<T>): Range<T> = extend(other)
+
+/**
+ * Return the intersection of this range and [other].
+ *
+ * @throws IllegalArgumentException if this is disjoint from [other].
+ */
+@RequiresApi(21)
+inline infix fun <T : Comparable<T>> Range<T>.and(other: Range<T>): Range<T> = intersect(other)
+
+/** Returns this [Range] as a [ClosedRange]. */
+@RequiresApi(21)
+fun <T : Comparable<T>> Range<T>.toClosedRange(): ClosedRange<T> = object : ClosedRange<T> {
+ override val endInclusive get() = upper
+ override val start get() = lower
+}
+
+/** Returns this [ClosedRange] as a [Range]. */
+@RequiresApi(21)
+fun <T : Comparable<T>> ClosedRange<T>.toRange(): Range<T> = Range(start, endInclusive)
diff --git a/core/ktx/src/main/java/androidx/core/util/Size.kt b/core/ktx/src/main/java/androidx/core/util/Size.kt
new file mode 100644
index 0000000..34f6854
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/Size.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.util
+
+import android.util.Size
+import android.util.SizeF
+import androidx.annotation.RequiresApi
+
+/**
+ * Returns "width", the first component of this [Size].
+ *
+ * This method allows to use destructuring declarations when working with
+ * sizes, for example:
+ * ```
+ * val (w, h) = mySize
+ * ```
+ */
+@RequiresApi(21)
+inline operator fun Size.component1() = width
+
+/**
+ * Returns "height", the second component of this [Size].
+ *
+ * This method allows to use destructuring declarations when working with
+ * sizes, for example:
+ * ```
+ * val (w, h) = mySize
+ * ```
+ */
+@RequiresApi(21)
+inline operator fun Size.component2() = height
+
+/**
+ * Returns "width", the first component of this [SizeF].
+ *
+ * This method allows to use destructuring declarations when working with
+ * sizes, for example:
+ * ```
+ * val (w, h) = mySize
+ * ```
+ */
+@RequiresApi(21)
+inline operator fun SizeF.component1() = width
+
+/**
+ * Returns "height", the second component of this [SizeF].
+ *
+ * This method allows to use destructuring declarations when working with
+ * sizes, for example:
+ * ```
+ * val (w, h) = mySize
+ * ```
+ */
+@RequiresApi(21)
+inline operator fun SizeF.component2() = height
diff --git a/core/ktx/src/main/java/androidx/core/util/SparseArray.kt b/core/ktx/src/main/java/androidx/core/util/SparseArray.kt
new file mode 100644
index 0000000..2b34a46
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/SparseArray.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.util
+
+import android.util.SparseArray
+
+/** Returns the number of key/value pairs in the collection. */
+inline val <T> SparseArray<T>.size get() = size()
+
+/** Returns true if the collection contains [key]. */
+inline operator fun <T> SparseArray<T>.contains(key: Int) = indexOfKey(key) >= 0
+
+/** Allows the use of the index operator for storing values in the collection. */
+inline operator fun <T> SparseArray<T>.set(key: Int, value: T) = put(key, value)
+
+/** Creates a new collection by adding or replacing entries from [other]. */
+operator fun <T> SparseArray<T>.plus(other: SparseArray<T>): SparseArray<T> {
+ val new = SparseArray<T>(size() + other.size())
+ new.putAll(this)
+ new.putAll(other)
+ return new
+}
+
+/** Returns true if the collection contains [key]. */
+inline fun <T> SparseArray<T>.containsKey(key: Int) = indexOfKey(key) >= 0
+
+/** Returns true if the collection contains [value]. */
+inline fun <T> SparseArray<T>.containsValue(value: T) = indexOfValue(value) != -1
+
+/** Return the value corresponding to [key], or [defaultValue] when not present. */
+inline fun <T> SparseArray<T>.getOrDefault(key: Int, defaultValue: T) = get(key) ?: defaultValue
+
+/** Return the value corresponding to [key], or from [defaultValue] when not present. */
+inline fun <T> SparseArray<T>.getOrElse(key: Int, defaultValue: () -> T) =
+ get(key) ?: defaultValue()
+
+/** Return true when the collection contains no elements. */
+inline fun <T> SparseArray<T>.isEmpty() = size() == 0
+
+/** Return true when the collection contains elements. */
+inline fun <T> SparseArray<T>.isNotEmpty() = size() != 0
+
+/** Removes the entry for [key] only if it is mapped to [value]. */
+fun <T> SparseArray<T>.remove(key: Int, value: T): Boolean {
+ val index = indexOfKey(key)
+ if (index != -1 && value == valueAt(index)) {
+ removeAt(index)
+ return true
+ }
+ return false
+}
+
+/** Update this collection by adding or replacing entries from [other]. */
+fun <T> SparseArray<T>.putAll(other: SparseArray<T>) = other.forEach(::put)
+
+/** Performs the given [action] for each key/value entry. */
+inline fun <T> SparseArray<T>.forEach(action: (key: Int, value: T) -> Unit) {
+ for (index in 0 until size()) {
+ action(keyAt(index), valueAt(index))
+ }
+}
+
+/** Return an iterator over the collection's keys. */
+fun <T> SparseArray<T>.keyIterator(): IntIterator = object : IntIterator() {
+ var index = 0
+ override fun hasNext() = index < size()
+ override fun nextInt() = keyAt(index++)
+}
+
+/** Return an iterator over the collection's values. */
+fun <T> SparseArray<T>.valueIterator(): Iterator<T> = object : Iterator<T> {
+ var index = 0
+ override fun hasNext() = index < size()
+ override fun next() = valueAt(index++)
+}
diff --git a/core/ktx/src/main/java/androidx/core/util/SparseBooleanArray.kt b/core/ktx/src/main/java/androidx/core/util/SparseBooleanArray.kt
new file mode 100644
index 0000000..b349326
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/SparseBooleanArray.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.util
+
+import android.util.SparseBooleanArray
+
+/** Returns the number of key/value pairs in the collection. */
+inline val SparseBooleanArray.size get() = size()
+
+/** Returns true if the collection contains [key]. */
+inline operator fun SparseBooleanArray.contains(key: Int) = indexOfKey(key) >= 0
+
+/** Allows the use of the index operator for storing values in the collection. */
+inline operator fun SparseBooleanArray.set(key: Int, value: Boolean) = put(key, value)
+
+/** Creates a new collection by adding or replacing entries from [other]. */
+operator fun SparseBooleanArray.plus(other: SparseBooleanArray): SparseBooleanArray {
+ val new = SparseBooleanArray(size() + other.size())
+ new.putAll(this)
+ new.putAll(other)
+ return new
+}
+
+/** Returns true if the collection contains [key]. */
+inline fun SparseBooleanArray.containsKey(key: Int) = indexOfKey(key) >= 0
+
+/** Returns true if the collection contains [value]. */
+inline fun SparseBooleanArray.containsValue(value: Boolean) = indexOfValue(value) != -1
+
+/** Return the value corresponding to [key], or [defaultValue] when not present. */
+inline fun SparseBooleanArray.getOrDefault(key: Int, defaultValue: Boolean) = get(key, defaultValue)
+
+/** Return the value corresponding to [key], or from [defaultValue] when not present. */
+inline fun SparseBooleanArray.getOrElse(key: Int, defaultValue: () -> Boolean) =
+ indexOfKey(key).let { if (it != -1) valueAt(it) else defaultValue() }
+
+/** Return true when the collection contains no elements. */
+inline fun SparseBooleanArray.isEmpty() = size() == 0
+
+/** Return true when the collection contains elements. */
+inline fun SparseBooleanArray.isNotEmpty() = size() != 0
+
+/** Removes the entry for [key] only if it is mapped to [value]. */
+fun SparseBooleanArray.remove(key: Int, value: Boolean): Boolean {
+ val index = indexOfKey(key)
+ if (index != -1 && value == valueAt(index)) {
+ // Delete by key because of https://issuetracker.google.com/issues/70934959.
+ delete(key)
+ return true
+ }
+ return false
+}
+
+/** Update this collection by adding or replacing entries from [other]. */
+fun SparseBooleanArray.putAll(other: SparseBooleanArray) = other.forEach(::put)
+
+/** Performs the given [action] for each key/value entry. */
+inline fun SparseBooleanArray.forEach(action: (key: Int, value: Boolean) -> Unit) {
+ for (index in 0 until size()) {
+ action(keyAt(index), valueAt(index))
+ }
+}
+
+/** Return an iterator over the collection's keys. */
+fun SparseBooleanArray.keyIterator(): IntIterator = object : IntIterator() {
+ var index = 0
+ override fun hasNext() = index < size()
+ override fun nextInt() = keyAt(index++)
+}
+
+/** Return an iterator over the collection's values. */
+fun SparseBooleanArray.valueIterator(): BooleanIterator = object : BooleanIterator() {
+ var index = 0
+ override fun hasNext() = index < size()
+ override fun nextBoolean() = valueAt(index++)
+}
diff --git a/core/ktx/src/main/java/androidx/core/util/SparseIntArray.kt b/core/ktx/src/main/java/androidx/core/util/SparseIntArray.kt
new file mode 100644
index 0000000..bba57c6
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/SparseIntArray.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.util
+
+import android.util.SparseIntArray
+
+/** Returns the number of key/value pairs in the collection. */
+inline val SparseIntArray.size get() = size()
+
+/** Returns true if the collection contains [key]. */
+inline operator fun SparseIntArray.contains(key: Int) = indexOfKey(key) >= 0
+
+/** Allows the use of the index operator for storing values in the collection. */
+inline operator fun SparseIntArray.set(key: Int, value: Int) = put(key, value)
+
+/** Creates a new collection by adding or replacing entries from [other]. */
+operator fun SparseIntArray.plus(other: SparseIntArray): SparseIntArray {
+ val new = SparseIntArray(size() + other.size())
+ new.putAll(this)
+ new.putAll(other)
+ return new
+}
+
+/** Returns true if the collection contains [key]. */
+inline fun SparseIntArray.containsKey(key: Int) = indexOfKey(key) >= 0
+
+/** Returns true if the collection contains [value]. */
+inline fun SparseIntArray.containsValue(value: Int) = indexOfValue(value) != -1
+
+/** Return the value corresponding to [key], or [defaultValue] when not present. */
+inline fun SparseIntArray.getOrDefault(key: Int, defaultValue: Int) = get(key, defaultValue)
+
+/** Return the value corresponding to [key], or from [defaultValue] when not present. */
+inline fun SparseIntArray.getOrElse(key: Int, defaultValue: () -> Int) =
+ indexOfKey(key).let { if (it != -1) valueAt(it) else defaultValue() }
+
+/** Return true when the collection contains no elements. */
+inline fun SparseIntArray.isEmpty() = size() == 0
+
+/** Return true when the collection contains elements. */
+inline fun SparseIntArray.isNotEmpty() = size() != 0
+
+/** Removes the entry for [key] only if it is mapped to [value]. */
+fun SparseIntArray.remove(key: Int, value: Int): Boolean {
+ val index = indexOfKey(key)
+ if (index != -1 && value == valueAt(index)) {
+ removeAt(index)
+ return true
+ }
+ return false
+}
+
+/** Update this collection by adding or replacing entries from [other]. */
+fun SparseIntArray.putAll(other: SparseIntArray) = other.forEach(::put)
+
+/** Performs the given [action] for each key/value entry. */
+inline fun SparseIntArray.forEach(action: (key: Int, value: Int) -> Unit) {
+ for (index in 0 until size()) {
+ action(keyAt(index), valueAt(index))
+ }
+}
+
+/** Return an iterator over the collection's keys. */
+fun SparseIntArray.keyIterator(): IntIterator = object : IntIterator() {
+ var index = 0
+ override fun hasNext() = index < size()
+ override fun nextInt() = keyAt(index++)
+}
+
+/** Return an iterator over the collection's values. */
+fun SparseIntArray.valueIterator(): IntIterator = object : IntIterator() {
+ var index = 0
+ override fun hasNext() = index < size()
+ override fun nextInt() = valueAt(index++)
+}
diff --git a/core/ktx/src/main/java/androidx/core/util/SparseLongArray.kt b/core/ktx/src/main/java/androidx/core/util/SparseLongArray.kt
new file mode 100644
index 0000000..68b4763
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/util/SparseLongArray.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.core.util
+
+import android.util.SparseLongArray
+import androidx.annotation.RequiresApi
+
+/** Returns the number of key/value entries in the collection. */
+@get:RequiresApi(18)
+inline val SparseLongArray.size get() = size()
+
+/** Returns true if the collection contains [key]. */
+@RequiresApi(18)
+inline operator fun SparseLongArray.contains(key: Int) = indexOfKey(key) >= 0
+
+/** Allows the use of the index operator for storing values in the collection. */
+@RequiresApi(18)
+inline operator fun SparseLongArray.set(key: Int, value: Long) = put(key, value)
+
+/** Creates a new collection by adding or replacing entries from [other]. */
+@RequiresApi(18)
+operator fun SparseLongArray.plus(other: SparseLongArray): SparseLongArray {
+ val new = SparseLongArray(size() + other.size())
+ new.putAll(this)
+ new.putAll(other)
+ return new
+}
+
+/** Returns true if the collection contains [key]. */
+@RequiresApi(18)
+inline fun SparseLongArray.containsKey(key: Int) = indexOfKey(key) >= 0
+
+/** Returns true if the collection contains [value]. */
+@RequiresApi(18)
+inline fun SparseLongArray.containsValue(value: Long) = indexOfValue(value) != -1
+
+/** Return the value corresponding to [key], or [defaultValue] when not present. */
+@RequiresApi(18)
+inline fun SparseLongArray.getOrDefault(key: Int, defaultValue: Long) = get(key, defaultValue)
+
+/** Return the value corresponding to [key], or from [defaultValue] when not present. */
+@RequiresApi(18)
+inline fun SparseLongArray.getOrElse(key: Int, defaultValue: () -> Long) =
+ indexOfKey(key).let { if (it != -1) valueAt(it) else defaultValue() }
+
+/** Return true when the collection contains no elements. */
+@RequiresApi(18)
+inline fun SparseLongArray.isEmpty() = size() == 0
+
+/** Return true when the collection contains elements. */
+@RequiresApi(18)
+inline fun SparseLongArray.isNotEmpty() = size() != 0
+
+/** Removes the entry for [key] only if it is set to [value]. */
+@RequiresApi(18)
+fun SparseLongArray.remove(key: Int, value: Long): Boolean {
+ val index = indexOfKey(key)
+ if (index != -1 && value == valueAt(index)) {
+ removeAt(index)
+ return true
+ }
+ return false
+}
+
+/** Update this collection by adding or replacing entries from [other]. */
+@RequiresApi(18)
+fun SparseLongArray.putAll(other: SparseLongArray) = other.forEach(::put)
+
+/** Performs the given [action] for each key/value entry. */
+@RequiresApi(18)
+inline fun SparseLongArray.forEach(action: (key: Int, value: Long) -> Unit) {
+ for (index in 0 until size()) {
+ action(keyAt(index), valueAt(index))
+ }
+}
+
+/** Return an iterator over the collection's keys. */
+@RequiresApi(18)
+fun SparseLongArray.keyIterator(): IntIterator = object : IntIterator() {
+ var index = 0
+ override fun hasNext() = index < size()
+ override fun nextInt() = keyAt(index++)
+}
+
+/** Return an iterator over the collection's values. */
+@RequiresApi(18)
+fun SparseLongArray.valueIterator(): LongIterator = object : LongIterator() {
+ var index = 0
+ override fun hasNext() = index < size()
+ override fun nextLong() = valueAt(index++)
+}
diff --git a/core/ktx/src/main/java/androidx/core/view/Menu.kt b/core/ktx/src/main/java/androidx/core/view/Menu.kt
new file mode 100644
index 0000000..069e61d
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/view/Menu.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.view
+
+import android.view.Menu
+import android.view.MenuItem
+
+/**
+ * Returns the menu at [index].
+ *
+ * @throws IndexOutOfBoundsException if index is less than 0 or greater than or equal to the count.
+ */
+inline operator fun Menu.get(index: Int): MenuItem = getItem(index)
+
+/** Returns `true` if [item] is found in this menu. */
+operator fun Menu.contains(item: MenuItem): Boolean {
+ @Suppress("LoopToCallChain")
+ for (index in 0 until size()) {
+ if (getItem(index) == item) {
+ return true
+ }
+ }
+ return false
+}
+
+/** Removes [item] from this menu. */
+inline operator fun Menu.minusAssign(item: MenuItem) = removeItem(item.itemId)
+
+/** Returns the number of items in this menu. */
+inline val Menu.size get() = size()
+
+/** Returns true if this menu contains no items. */
+inline fun Menu.isEmpty() = size() == 0
+
+/** Returns true if this menu contains one or more items. */
+inline fun Menu.isNotEmpty() = size() != 0
+
+/** Performs the given action on each item in this menu. */
+inline fun Menu.forEach(action: (item: MenuItem) -> Unit) {
+ for (index in 0 until size()) {
+ action(getItem(index))
+ }
+}
+
+/** Performs the given action on each item in this menu, providing its sequential index. */
+inline fun Menu.forEachIndexed(action: (index: Int, item: MenuItem) -> Unit) {
+ for (index in 0 until size()) {
+ action(index, getItem(index))
+ }
+}
+
+/** Returns a [MutableIterator] over the items in this menu. */
+operator fun Menu.iterator() = object : MutableIterator<MenuItem> {
+ private var index = 0
+ override fun hasNext() = index < size()
+ override fun next() = getItem(index++) ?: throw IndexOutOfBoundsException()
+ override fun remove() = removeItem(--index)
+}
+
+/** Returns a [Sequence] over the items in this menu. */
+val Menu.children: Sequence<MenuItem>
+ get() = object : Sequence<MenuItem> {
+ override fun iterator() = this@children.iterator()
+ }
diff --git a/core/ktx/src/main/java/androidx/core/view/View.kt b/core/ktx/src/main/java/androidx/core/view/View.kt
new file mode 100644
index 0000000..bc2e34d
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/view/View.kt
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to other public API.
+
+package androidx.core.view
+
+import android.graphics.Bitmap
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewTreeObserver
+import android.view.accessibility.AccessibilityEvent
+import androidx.annotation.Px
+import androidx.annotation.RequiresApi
+import androidx.annotation.StringRes
+import androidx.core.graphics.applyCanvas
+
+/**
+ * Performs the given action when this view is next laid out.
+ *
+ * @see doOnLayout
+ */
+inline fun View.doOnNextLayout(crossinline action: (view: View) -> Unit) {
+ addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
+ override fun onLayoutChange(
+ view: View,
+ left: Int,
+ top: Int,
+ right: Int,
+ bottom: Int,
+ oldLeft: Int,
+ oldTop: Int,
+ oldRight: Int,
+ oldBottom: Int
+ ) {
+ view.removeOnLayoutChangeListener(this)
+ action(view)
+ }
+ })
+}
+
+/**
+ * Performs the given action when this view is laid out. If the view has been laid out and it
+ * has not requested a layout, the action will be performed straight away, otherwise the
+ * action will be performed after the view is next laid out.
+ *
+ * @see doOnNextLayout
+ */
+inline fun View.doOnLayout(crossinline action: (view: View) -> Unit) {
+ if (ViewCompat.isLaidOut(this) && !isLayoutRequested) {
+ action(this)
+ } else {
+ doOnNextLayout {
+ action(it)
+ }
+ }
+}
+
+/**
+ * Performs the given action when the view tree is about to be drawn.
+ */
+inline fun View.doOnPreDraw(crossinline action: (view: View) -> Unit) {
+ val vto = viewTreeObserver
+ vto.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
+ override fun onPreDraw(): Boolean {
+ action(this@doOnPreDraw)
+ when {
+ vto.isAlive -> vto.removeOnPreDrawListener(this)
+ else -> viewTreeObserver.removeOnPreDrawListener(this)
+ }
+ return true
+ }
+ })
+}
+
+/**
+ * Sends [AccessibilityEvent] of type [AccessibilityEvent.TYPE_ANNOUNCEMENT].
+ *
+ * @see View.announceForAccessibility
+ */
+@RequiresApi(16)
+inline fun View.announceForAccessibility(@StringRes resource: Int) {
+ val announcement = resources.getString(resource)
+ announceForAccessibility(announcement)
+}
+
+/**
+ * Updates this view's relative padding. This version of the method allows using named parameters
+ * to just set one or more axes.
+ *
+ * @see View.setPaddingRelative
+ */
+@RequiresApi(17)
+inline fun View.updatePaddingRelative(
+ @Px start: Int = paddingStart,
+ @Px top: Int = paddingTop,
+ @Px end: Int = paddingEnd,
+ @Px bottom: Int = paddingBottom
+) {
+ setPaddingRelative(start, top, end, bottom)
+}
+
+/**
+ * Updates this view's padding. This version of the method allows using named parameters
+ * to just set one or more axes.
+ *
+ * @see View.setPadding
+ */
+inline fun View.updatePadding(
+ @Px left: Int = paddingLeft,
+ @Px top: Int = paddingTop,
+ @Px right: Int = paddingRight,
+ @Px bottom: Int = paddingBottom
+) {
+ setPadding(left, top, right, bottom)
+}
+
+/**
+ * Sets the view's padding. This version of the method sets all axes to the provided size.
+ *
+ * @see View.setPadding
+ */
+inline fun View.setPadding(@Px size: Int) {
+ setPadding(size, size, size, size)
+}
+
+/**
+ * Version of [View.postDelayed] which re-orders the parameters, allowing the action to be placed
+ * outside of parentheses.
+ *
+ * ```
+ * view.postDelayed(200) {
+ * doSomething()
+ * }
+ * ```
+ *
+ * @return the created Runnable
+ */
+inline fun View.postDelayed(delayInMillis: Long, crossinline action: () -> Unit): Runnable {
+ val runnable = Runnable { action() }
+ postDelayed(runnable, delayInMillis)
+ return runnable
+}
+
+/**
+ * Version of [View.postOnAnimationDelayed] which re-orders the parameters, allowing the action
+ * to be placed outside of parentheses.
+ *
+ * ```
+ * view.postOnAnimationDelayed(16) {
+ * doSomething()
+ * }
+ * ```
+ *
+ * @return the created Runnable
+ */
+@RequiresApi(16)
+inline fun View.postOnAnimationDelayed(
+ delayInMillis: Long,
+ crossinline action: () -> Unit
+): Runnable {
+ val runnable = Runnable { action() }
+ postOnAnimationDelayed(runnable, delayInMillis)
+ return runnable
+}
+
+/**
+ * Return a [Bitmap] representation of this [View].
+ *
+ * The resulting bitmap will be the same width and height as this view's current layout
+ * dimensions. This does not take into account any transformations such as scale or translation.
+ *
+ * Note, this will use the software rendering pipeline to draw the view to the bitmap. This may
+ * result with different drawing to what is rendered on a hardware accelerated canvas (such as
+ * the device screen).
+ *
+ * If this view has not been laid out this method will throw a [IllegalStateException].
+ *
+ * @param config Bitmap config of the desired bitmap. Defaults to [Bitmap.Config.ARGB_8888].
+ */
+fun View.toBitmap(config: Bitmap.Config = Bitmap.Config.ARGB_8888): Bitmap {
+ if (!ViewCompat.isLaidOut(this)) {
+ throw IllegalStateException("View needs to be laid out before calling toBitmap()")
+ }
+ return Bitmap.createBitmap(width, height, config).applyCanvas {
+ translate(-scrollX.toFloat(), -scrollY.toFloat())
+ draw(this)
+ }
+}
+
+/**
+ * Returns true when this view's visibility is [View.VISIBLE], false otherwise.
+ *
+ * ```
+ * if (view.isVisible) {
+ * // Behavior...
+ * }
+ * ```
+ *
+ * Setting this property to true sets the visibility to [View.VISIBLE], false to [View.GONE].
+ *
+ * ```
+ * view.isVisible = true
+ * ```
+ */
+inline var View.isVisible: Boolean
+ get() = visibility == View.VISIBLE
+ set(value) {
+ visibility = if (value) View.VISIBLE else View.GONE
+ }
+
+/**
+ * Returns true when this view's visibility is [View.INVISIBLE], false otherwise.
+ *
+ * ```
+ * if (view.isInvisible) {
+ * // Behavior...
+ * }
+ * ```
+ *
+ * Setting this property to true sets the visibility to [View.INVISIBLE], false to [View.VISIBLE].
+ *
+ * ```
+ * view.isInvisible = true
+ * ```
+ */
+inline var View.isInvisible: Boolean
+ get() = visibility == View.INVISIBLE
+ set(value) {
+ visibility = if (value) View.INVISIBLE else View.VISIBLE
+ }
+
+/**
+ * Returns true when this view's visibility is [View.GONE], false otherwise.
+ *
+ * ```
+ * if (view.isGone) {
+ * // Behavior...
+ * }
+ * ```
+ *
+ * Setting this property to true sets the visibility to [View.GONE], false to [View.VISIBLE].
+ *
+ * ```
+ * view.isGone = true
+ * ```
+ */
+inline var View.isGone: Boolean
+ get() = visibility == View.GONE
+ set(value) {
+ visibility = if (value) View.GONE else View.VISIBLE
+ }
+
+/**
+ * Executes [block] with the View's layoutParams and reassigns the layoutParams with the
+ * updated version.
+ *
+ * @see View.getLayoutParams
+ * @see View.setLayoutParams
+ **/
+inline fun View.updateLayoutParams(block: ViewGroup.LayoutParams.() -> Unit) {
+ updateLayoutParams<ViewGroup.LayoutParams>(block)
+}
+
+/**
+ * Executes [block] with a typed version of the View's layoutParams and reassigns the
+ * layoutParams with the updated version.
+ *
+ * @see View.getLayoutParams
+ * @see View.setLayoutParams
+ **/
+@JvmName("updateLayoutParamsTyped")
+inline fun <reified T : ViewGroup.LayoutParams> View.updateLayoutParams(block: T.() -> Unit) {
+ val params = layoutParams as T
+ block(params)
+ layoutParams = params
+}
diff --git a/core/ktx/src/main/java/androidx/core/view/ViewGroup.kt b/core/ktx/src/main/java/androidx/core/view/ViewGroup.kt
new file mode 100644
index 0000000..8a7152a
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/view/ViewGroup.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to other public API.
+
+package androidx.core.view
+
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.Px
+import androidx.annotation.RequiresApi
+
+/**
+ * Returns the view at [index].
+ *
+ * @throws IndexOutOfBoundsException if index is less than 0 or greater than or equal to the count.
+ */
+operator fun ViewGroup.get(index: Int) =
+ getChildAt(index) ?: throw IndexOutOfBoundsException("Index: $index, Size: $childCount")
+
+/** Returns `true` if [view] is found in this view group. */
+inline operator fun ViewGroup.contains(view: View) = indexOfChild(view) != -1
+
+/** Adds [view] to this view group. */
+inline operator fun ViewGroup.plusAssign(view: View) = addView(view)
+
+/** Removes [view] from this view group. */
+inline operator fun ViewGroup.minusAssign(view: View) = removeView(view)
+
+/** Returns the number of views in this view group. */
+inline val ViewGroup.size get() = childCount
+
+/** Returns true if this view group contains no views. */
+inline fun ViewGroup.isEmpty() = childCount == 0
+
+/** Returns true if this view group contains one or more views. */
+inline fun ViewGroup.isNotEmpty() = childCount != 0
+
+/** Performs the given action on each view in this view group. */
+inline fun ViewGroup.forEach(action: (view: View) -> Unit) {
+ for (index in 0 until childCount) {
+ action(getChildAt(index))
+ }
+}
+
+/** Performs the given action on each view in this view group, providing its sequential index. */
+inline fun ViewGroup.forEachIndexed(action: (index: Int, view: View) -> Unit) {
+ for (index in 0 until childCount) {
+ action(index, getChildAt(index))
+ }
+}
+
+/** Returns a [MutableIterator] over the views in this view group. */
+operator fun ViewGroup.iterator() = object : MutableIterator<View> {
+ private var index = 0
+ override fun hasNext() = index < childCount
+ override fun next() = getChildAt(index++) ?: throw IndexOutOfBoundsException()
+ override fun remove() = removeViewAt(--index)
+}
+
+/** Returns a [Sequence] over the child views in this view group. */
+val ViewGroup.children: Sequence<View>
+ get() = object : Sequence<View> {
+ override fun iterator() = this@children.iterator()
+ }
+
+/**
+ * Sets the margins in the ViewGroup's MarginLayoutParams. This version of the method sets all axes
+ * to the provided size.
+ *
+ * @see ViewGroup.MarginLayoutParams.setMargins
+ */
+inline fun ViewGroup.MarginLayoutParams.setMargins(@Px size: Int) {
+ setMargins(size, size, size, size)
+}
+
+/**
+ * Updates the margins in the [ViewGroup]'s [ViewGroup.MarginLayoutParams].
+ * This version of the method allows using named parameters to just set one or more axes.
+ *
+ * @see ViewGroup.MarginLayoutParams.setMargins
+ */
+inline fun ViewGroup.MarginLayoutParams.updateMargins(
+ @Px left: Int = leftMargin,
+ @Px top: Int = topMargin,
+ @Px right: Int = rightMargin,
+ @Px bottom: Int = bottomMargin
+) {
+ setMargins(left, top, right, bottom)
+}
+
+/**
+ * Updates the relative margins in the ViewGroup's MarginLayoutParams.
+ * This version of the method allows using named parameters to just set one or more axes.
+ *
+ * @see ViewGroup.MarginLayoutParams.setMargins
+ */
+@RequiresApi(17)
+inline fun ViewGroup.MarginLayoutParams.updateMarginsRelative(
+ @Px start: Int = marginStart,
+ @Px top: Int = topMargin,
+ @Px end: Int = marginEnd,
+ @Px bottom: Int = bottomMargin
+) {
+ marginStart = start
+ topMargin = top
+ marginEnd = end
+ bottomMargin = bottom
+}
diff --git a/core/ktx/src/main/java/androidx/core/widget/Toast.kt b/core/ktx/src/main/java/androidx/core/widget/Toast.kt
new file mode 100644
index 0000000..b0ddfd1
--- /dev/null
+++ b/core/ktx/src/main/java/androidx/core/widget/Toast.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.core.widget
+
+import android.content.Context
+import android.widget.Toast
+import androidx.annotation.StringRes
+
+/**
+ * Creates and shows a [Toast] with the given [text]
+ *
+ * @param duration Toast duration, defaults to [Toast.LENGTH_SHORT]
+ */
+inline fun Context.toast(text: CharSequence, duration: Int = Toast.LENGTH_SHORT): Toast {
+ return Toast.makeText(this, text, duration).apply { show() }
+}
+
+/**
+ * Creates and shows a [Toast] with text from a resource
+ *
+ * @param resId Resource id of the string resource to use
+ * @param duration Toast duration, defaults to [Toast.LENGTH_SHORT]
+ */
+inline fun Context.toast(@StringRes resId: Int, duration: Int = Toast.LENGTH_SHORT): Toast {
+ return Toast.makeText(this, resId, duration).apply { show() }
+}
diff --git a/customview/src/main/java/androidx/customview/widget/ExploreByTouchHelper.java b/customview/src/main/java/androidx/customview/widget/ExploreByTouchHelper.java
index b49afc8..dbe8455 100644
--- a/customview/src/main/java/androidx/customview/widget/ExploreByTouchHelper.java
+++ b/customview/src/main/java/androidx/customview/widget/ExploreByTouchHelper.java
@@ -188,7 +188,7 @@
updateHoveredVirtualView(virtualViewId);
return (virtualViewId != INVALID_ID);
case MotionEvent.ACTION_HOVER_EXIT:
- if (mAccessibilityFocusedVirtualViewId != INVALID_ID) {
+ if (mHoveredVirtualViewId != INVALID_ID) {
updateHoveredVirtualView(INVALID_ID);
return true;
}
diff --git a/emoji/core/api/current.txt b/emoji/core/api/current.txt
index 785f44a..7b343a1 100644
--- a/emoji/core/api/current.txt
+++ b/emoji/core/api/current.txt
@@ -9,6 +9,7 @@
method public boolean hasEmojiGlyph(java.lang.CharSequence);
method public boolean hasEmojiGlyph(java.lang.CharSequence, int);
method public static androidx.emoji.text.EmojiCompat init(androidx.emoji.text.EmojiCompat.Config);
+ method public void load();
method public java.lang.CharSequence process(java.lang.CharSequence);
method public java.lang.CharSequence process(java.lang.CharSequence, int, int);
method public java.lang.CharSequence process(java.lang.CharSequence, int, int, int);
@@ -17,9 +18,12 @@
method public void unregisterInitCallback(androidx.emoji.text.EmojiCompat.InitCallback);
field public static final java.lang.String EDITOR_INFO_METAVERSION_KEY = "android.support.text.emoji.emojiCompat_metadataVersion";
field public static final java.lang.String EDITOR_INFO_REPLACE_ALL_KEY = "android.support.text.emoji.emojiCompat_replaceAll";
+ field public static final int LOAD_STATE_DEFAULT = 3; // 0x3
field public static final int LOAD_STATE_FAILED = 2; // 0x2
field public static final int LOAD_STATE_LOADING = 0; // 0x0
field public static final int LOAD_STATE_SUCCEEDED = 1; // 0x1
+ field public static final int LOAD_STRATEGY_DEFAULT = 0; // 0x0
+ field public static final int LOAD_STRATEGY_MANUAL = 1; // 0x1
field public static final int REPLACE_STRATEGY_ALL = 1; // 0x1
field public static final int REPLACE_STRATEGY_DEFAULT = 0; // 0x0
field public static final int REPLACE_STRATEGY_NON_EXISTENT = 2; // 0x2
@@ -31,6 +35,7 @@
method public androidx.emoji.text.EmojiCompat.Config registerInitCallback(androidx.emoji.text.EmojiCompat.InitCallback);
method public androidx.emoji.text.EmojiCompat.Config setEmojiSpanIndicatorColor(int);
method public androidx.emoji.text.EmojiCompat.Config setEmojiSpanIndicatorEnabled(boolean);
+ method public androidx.emoji.text.EmojiCompat.Config setMetadataLoadStrategy(int);
method public androidx.emoji.text.EmojiCompat.Config setReplaceAll(boolean);
method public androidx.emoji.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean);
method public androidx.emoji.text.EmojiCompat.Config setUseEmojiAsDefaultStyle(boolean, java.util.List<java.lang.Integer>);
diff --git a/emoji/core/src/androidTest/java/androidx/emoji/text/ConfigTest.java b/emoji/core/src/androidTest/java/androidx/emoji/text/ConfigTest.java
index 34f31fd..c8d3dad 100644
--- a/emoji/core/src/androidTest/java/androidx/emoji/text/ConfigTest.java
+++ b/emoji/core/src/androidTest/java/androidx/emoji/text/ConfigTest.java
@@ -26,6 +26,7 @@
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -148,9 +149,25 @@
assertTrue(emojiCompat.isEmojiSpanIndicatorEnabled());
}
+ @Test
+ public void testBuild_manualLoadStrategy_doesNotCallMetadataLoaderLoad() {
+ final EmojiCompat.MetadataRepoLoader loader = mock(EmojiCompat.MetadataRepoLoader.class);
+ final EmojiCompat.Config config = new ValidTestConfig(loader)
+ .setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
+
+ EmojiCompat.reset(config);
+
+ verify(loader, never()).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+ assertEquals(EmojiCompat.LOAD_STATE_DEFAULT, EmojiCompat.get().getLoadState());
+ }
+
private static class ValidTestConfig extends EmojiCompat.Config {
ValidTestConfig() {
super(new TestConfigBuilder.TestEmojiDataLoader());
}
+
+ ValidTestConfig(EmojiCompat.MetadataRepoLoader loader) {
+ super(loader);
+ }
}
}
diff --git a/emoji/core/src/androidTest/java/androidx/emoji/text/EmojiCompatTest.java b/emoji/core/src/androidTest/java/androidx/emoji/text/EmojiCompatTest.java
index dd8f3b9..fd9df56 100644
--- a/emoji/core/src/androidTest/java/androidx/emoji/text/EmojiCompatTest.java
+++ b/emoji/core/src/androidTest/java/androidx/emoji/text/EmojiCompatTest.java
@@ -53,6 +53,7 @@
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
@@ -691,6 +692,97 @@
metadataLoader.getLoaderLatch().countDown();
}
+ @Test(expected = IllegalStateException.class)
+ public void testLoad_throwsException_whenLoadStrategyDefault() {
+ final EmojiCompat.MetadataRepoLoader loader = mock(EmojiCompat.MetadataRepoLoader.class);
+ final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader);
+ EmojiCompat.reset(config);
+
+ EmojiCompat.get().load();
+ }
+
+ @Test
+ @SdkSuppress(maxSdkVersion = 18)
+ public void testLoad_pre19() {
+ final EmojiCompat.MetadataRepoLoader loader = spy(new TestConfigBuilder
+ .TestEmojiDataLoader());
+ final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader)
+ .setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
+
+ EmojiCompat.reset(config);
+
+ verify(loader, never()).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+ assertEquals(EmojiCompat.LOAD_STATE_DEFAULT, EmojiCompat.get().getLoadState());
+
+ EmojiCompat.get().load();
+ assertEquals(EmojiCompat.LOAD_STATE_SUCCEEDED, EmojiCompat.get().getLoadState());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 19)
+ public void testLoad_startsLoading() {
+ final EmojiCompat.MetadataRepoLoader loader = spy(new TestConfigBuilder
+ .TestEmojiDataLoader());
+ final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader)
+ .setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
+
+ EmojiCompat.reset(config);
+
+ verify(loader, never()).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+ assertEquals(EmojiCompat.LOAD_STATE_DEFAULT, EmojiCompat.get().getLoadState());
+
+ EmojiCompat.get().load();
+ verify(loader, times(1)).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+ assertEquals(EmojiCompat.LOAD_STATE_SUCCEEDED, EmojiCompat.get().getLoadState());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 19)
+ public void testLoad_onceSuccessDoesNotStartLoading() {
+ final EmojiCompat.MetadataRepoLoader loader = spy(new TestConfigBuilder
+ .TestEmojiDataLoader());
+ final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader)
+ .setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
+
+ EmojiCompat.reset(config);
+
+ EmojiCompat.get().load();
+ verify(loader, times(1)).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+ assertEquals(EmojiCompat.LOAD_STATE_SUCCEEDED, EmojiCompat.get().getLoadState());
+
+ reset(loader);
+ EmojiCompat.get().load();
+ verify(loader, never()).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+ assertEquals(EmojiCompat.LOAD_STATE_SUCCEEDED, EmojiCompat.get().getLoadState());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 19)
+ public void testLoad_onceLoadingDoesNotStartLoading() throws InterruptedException {
+ final TestConfigBuilder.WaitingDataLoader loader = spy(
+ new TestConfigBuilder.WaitingDataLoader(true /*success*/));
+ final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader)
+ .setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
+
+ EmojiCompat.reset(config);
+
+ verify(loader, never()).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+
+ EmojiCompat.get().load();
+ verify(loader, times(1)).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+ assertEquals(EmojiCompat.get().getLoadState(), EmojiCompat.LOAD_STATE_LOADING);
+
+ reset(loader);
+
+ EmojiCompat.get().load();
+ verify(loader, never()).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+
+ loader.getLoaderLatch().countDown();
+ loader.getTestLatch().await();
+
+ assertEquals(EmojiCompat.get().getLoadState(), EmojiCompat.LOAD_STATE_SUCCEEDED);
+ }
+
@Test
@SdkSuppress(maxSdkVersion = 18)
public void testGetAssetSignature() {
diff --git a/emoji/core/src/androidTest/java/androidx/emoji/widget/EmojiInputFilterTest.java b/emoji/core/src/androidTest/java/androidx/emoji/widget/EmojiInputFilterTest.java
index 27ea7c6..2882858 100644
--- a/emoji/core/src/androidTest/java/androidx/emoji/widget/EmojiInputFilterTest.java
+++ b/emoji/core/src/androidTest/java/androidx/emoji/widget/EmojiInputFilterTest.java
@@ -114,4 +114,16 @@
verify(mEmojiCompat, times(0)).process(any(Spannable.class), anyInt(), anyInt());
verify(mEmojiCompat, times(0)).registerInitCallback(any(EmojiCompat.InitCallback.class));
}
+
+ @Test
+ public void testFilter_withManualLoadStrategy() {
+ final Spannable testString = new SpannableString("abc");
+ when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_DEFAULT);
+
+ final CharSequence result = mInputFilter.filter(testString, 0, 1, null, 0, 1);
+
+ assertNotNull(result);
+ verify(mEmojiCompat, times(0)).process(any(Spannable.class), anyInt(), anyInt());
+ verify(mEmojiCompat, times(1)).registerInitCallback(any(EmojiCompat.InitCallback.class));
+ }
}
diff --git a/emoji/core/src/androidTest/java/androidx/emoji/widget/EmojiTextWatcherTest.java b/emoji/core/src/androidTest/java/androidx/emoji/widget/EmojiTextWatcherTest.java
index fade6f7..b8f0154 100644
--- a/emoji/core/src/androidTest/java/androidx/emoji/widget/EmojiTextWatcherTest.java
+++ b/emoji/core/src/androidTest/java/androidx/emoji/widget/EmojiTextWatcherTest.java
@@ -109,4 +109,15 @@
verify(mEmojiCompat, times(1)).process(any(Spannable.class), anyInt(), anyInt(), anyInt(),
eq(EmojiCompat.REPLACE_STRATEGY_ALL));
}
+
+ @Test
+ public void testFilter_withManualLoadStrategy() {
+ final Spannable testString = new SpannableString("abc");
+ when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_DEFAULT);
+
+ mTextWatcher.onTextChanged(testString, 0, 0, 1);
+
+ verify(mEmojiCompat, times(0)).process(any(Spannable.class), anyInt(), anyInt());
+ verify(mEmojiCompat, times(1)).registerInitCallback(any(EmojiCompat.InitCallback.class));
+ }
}
diff --git a/emoji/core/src/androidTest/java/androidx/emoji/widget/EmojiTransformationMethodTest.java b/emoji/core/src/androidTest/java/androidx/emoji/widget/EmojiTransformationMethodTest.java
new file mode 100644
index 0000000..cf74331
--- /dev/null
+++ b/emoji/core/src/androidTest/java/androidx/emoji/widget/EmojiTransformationMethodTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.emoji.widget;
+
+import static junit.framework.TestCase.assertSame;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import static androidx.emoji.util.EmojiMatcher.sameCharSequence;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.method.TransformationMethod;
+import android.view.View;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import androidx.emoji.text.EmojiCompat;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class EmojiTransformationMethodTest {
+
+ private EmojiTransformationMethod mTransformationMethod;
+ private TransformationMethod mWrappedTransformationMethod;
+ private View mView;
+ private EmojiCompat mEmojiCompat;
+ private final String mTestString = "abc";
+
+ @Before
+ public void setup() {
+ mEmojiCompat = mock(EmojiCompat.class);
+ when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_SUCCEEDED);
+ when(mEmojiCompat.process(any(CharSequence.class))).thenAnswer(new Answer<CharSequence>() {
+ @Override
+ public CharSequence answer(InvocationOnMock invocation) {
+ Object[] args = invocation.getArguments();
+ return new SpannableString((String) args[0]);
+ }
+ });
+ EmojiCompat.reset(mEmojiCompat);
+
+ mView = mock(View.class);
+ when(mView.isInEditMode()).thenReturn(false);
+
+ mWrappedTransformationMethod = mock(TransformationMethod.class);
+ when(mWrappedTransformationMethod.getTransformation(any(CharSequence.class),
+ any(View.class))).thenAnswer(new Answer<CharSequence>() {
+ @Override
+ public CharSequence answer(InvocationOnMock invocation) {
+ Object[] args = invocation.getArguments();
+ return (String) args[0];
+ }
+ });
+
+ mTransformationMethod = new EmojiTransformationMethod(mWrappedTransformationMethod);
+ }
+
+ @Test
+ public void testFilter_withNullSource() {
+ assertNull(mTransformationMethod.getTransformation(null, mView));
+ verify(mEmojiCompat, never()).process(any(CharSequence.class));
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void testFilter_withNullView() {
+ mTransformationMethod.getTransformation("", null);
+ }
+
+ @Test
+ public void testFilter_withNullTransformationMethod() {
+ mTransformationMethod = new EmojiTransformationMethod(null);
+
+ final CharSequence result = mTransformationMethod.getTransformation(mTestString, mView);
+
+ assertTrue(TextUtils.equals(new SpannableString(mTestString), result));
+ verify(mEmojiCompat, times(1)).process(sameCharSequence(mTestString));
+ }
+
+ @Test
+ public void testFilter() {
+ final CharSequence result = mTransformationMethod.getTransformation(mTestString, mView);
+
+ assertTrue(TextUtils.equals(new SpannableString(mTestString), result));
+ assertTrue(result instanceof Spannable);
+ verify(mWrappedTransformationMethod, times(1)).getTransformation(
+ sameCharSequence(mTestString), same(mView));
+ verify(mEmojiCompat, times(1)).process(sameCharSequence(mTestString));
+ verify(mEmojiCompat, never()).registerInitCallback(any(EmojiCompat.InitCallback.class));
+ }
+
+ @Test
+ public void testFilter_whenEmojiCompatLoading() {
+ when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_LOADING);
+
+ final CharSequence result = mTransformationMethod.getTransformation(mTestString, mView);
+
+ assertSame(mTestString, result);
+ verify(mWrappedTransformationMethod, times(1)).getTransformation(
+ sameCharSequence(mTestString), same(mView));
+ verify(mEmojiCompat, never()).process(sameCharSequence(mTestString));
+ verify(mEmojiCompat, never()).registerInitCallback(any(EmojiCompat.InitCallback.class));
+ }
+
+ @Test
+ public void testFilter_whenEmojiCompatLoadFailed() {
+ when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_FAILED);
+
+ final CharSequence result = mTransformationMethod.getTransformation(mTestString, mView);
+
+ assertSame(mTestString, result);
+ verify(mWrappedTransformationMethod, times(1)).getTransformation(
+ sameCharSequence(mTestString), same(mView));
+ verify(mEmojiCompat, never()).process(sameCharSequence(mTestString));
+ verify(mEmojiCompat, never()).registerInitCallback(any(EmojiCompat.InitCallback.class));
+ }
+
+ @Test
+ public void testFilter_withManualLoadStrategy() {
+ when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_DEFAULT);
+
+ final CharSequence result = mTransformationMethod.getTransformation(mTestString, mView);
+
+ assertSame(mTestString, result);
+ verify(mWrappedTransformationMethod, times(1)).getTransformation(
+ sameCharSequence(mTestString), same(mView));
+ verify(mEmojiCompat, never()).process(sameCharSequence(mTestString));
+ verify(mEmojiCompat, never()).registerInitCallback(any(EmojiCompat.InitCallback.class));
+ }
+}
diff --git a/emoji/core/src/main/java/androidx/emoji/text/EmojiCompat.java b/emoji/core/src/main/java/androidx/emoji/text/EmojiCompat.java
index 31d359d..572606f 100644
--- a/emoji/core/src/main/java/androidx/emoji/text/EmojiCompat.java
+++ b/emoji/core/src/main/java/androidx/emoji/text/EmojiCompat.java
@@ -61,7 +61,9 @@
* <pre><code>EmojiCompat.init(/* a config instance */);</code></pre>
* <p/>
* It is suggested to make the initialization as early as possible in your app. Please check {@link
- * EmojiCompat.Config} for more configuration parameters.
+ * EmojiCompat.Config} for more configuration parameters. Once {@link #init(EmojiCompat.Config)} is
+ * called a singleton instance will be created. Any call after that will not create a new instance
+ * and will return immediately.
* <p/>
* During initialization information about emojis is loaded on a background thread. Before the
* EmojiCompat instance is initialized, calls to functions such as {@link
@@ -96,6 +98,11 @@
"android.support.text.emoji.emojiCompat_replaceAll";
/**
+ * EmojiCompat instance is constructed, however the initialization did not start yet.
+ */
+ public static final int LOAD_STATE_DEFAULT = 3;
+
+ /**
* EmojiCompat is initializing.
*/
public static final int LOAD_STATE_LOADING = 0;
@@ -115,7 +122,7 @@
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
- @IntDef({LOAD_STATE_LOADING, LOAD_STATE_SUCCEEDED, LOAD_STATE_FAILED})
+ @IntDef({LOAD_STATE_DEFAULT, LOAD_STATE_LOADING, LOAD_STATE_SUCCEEDED, LOAD_STATE_FAILED})
@Retention(RetentionPolicy.SOURCE)
public @interface LoadState {
}
@@ -145,6 +152,26 @@
}
/**
+ * {@link EmojiCompat} will start loading metadata when {@link #init(Config)} is called.
+ */
+ public static final int LOAD_STRATEGY_DEFAULT = 0;
+
+ /**
+ * {@link EmojiCompat} will wait for {@link #load()} to be called by developer in order to
+ * start loading metadata.
+ */
+ public static final int LOAD_STRATEGY_MANUAL = 1;
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @IntDef({LOAD_STRATEGY_DEFAULT, LOAD_STRATEGY_MANUAL})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface LoadStrategy {
+ }
+
+ /**
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
@@ -205,18 +232,25 @@
private final int mEmojiSpanIndicatorColor;
/**
+ * @see Config#setMetadataLoadStrategy(int)
+ */
+ @LoadStrategy private final int mMetadataLoadStrategy;
+
+ /**
* Private constructor for singleton instance.
*
* @see #init(Config)
*/
private EmojiCompat(@NonNull final Config config) {
mInitLock = new ReentrantReadWriteLock();
+ mLoadState = LOAD_STATE_DEFAULT;
mReplaceAll = config.mReplaceAll;
mUseEmojiAsDefaultStyle = config.mUseEmojiAsDefaultStyle;
mEmojiAsDefaultStyleExceptions = config.mEmojiAsDefaultStyleExceptions;
mEmojiSpanIndicatorEnabled = config.mEmojiSpanIndicatorEnabled;
mEmojiSpanIndicatorColor = config.mEmojiSpanIndicatorColor;
mMetadataLoader = config.mMetadataLoader;
+ mMetadataLoadStrategy = config.mMetadataLoadStrategy;
mMainHandler = new Handler(Looper.getMainLooper());
mInitCallbacks = new ArraySet<>();
if (config.mInitCallbacks != null && !config.mInitCallbacks.isEmpty()) {
@@ -230,7 +264,9 @@
/**
* Initialize the singleton instance with a configuration. When used on devices running API 18
* or below, the singleton instance is immediately moved into {@link #LOAD_STATE_SUCCEEDED}
- * state without loading any metadata.
+ * state without loading any metadata. When called for the first time, the library will create
+ * the singleton instance and any call after that will not create a new instance and return
+ * immediately.
*
* @see EmojiCompat.Config
*/
@@ -304,9 +340,30 @@
}
}
- private void loadMetadata() {
+ /**
+ * When {@link Config#setMetadataLoadStrategy(int)} is set to {@link #LOAD_STRATEGY_MANUAL},
+ * this function starts loading the metadata. Calling the function when
+ * {@link Config#setMetadataLoadStrategy(int)} is {@code not} set to
+ * {@link #LOAD_STRATEGY_MANUAL} will throw an exception. The load will {@code not} start if:
+ * <ul>
+ * <li>the metadata is already loaded successfully and {@link #getLoadState()} is
+ * {@link #LOAD_STATE_SUCCEEDED}.
+ * </li>
+ * <li>a previous load attempt is not finished yet and {@link #getLoadState()} is
+ * {@link #LOAD_STATE_LOADING}.</li>
+ * </ul>
+ *
+ * @throws IllegalStateException when {@link Config#setMetadataLoadStrategy(int)} is not set
+ * to {@link #LOAD_STRATEGY_MANUAL}
+ */
+ public void load() {
+ Preconditions.checkState(mMetadataLoadStrategy == LOAD_STRATEGY_MANUAL,
+ "Set metadataLoadStrategy to LOAD_STRATEGY_MANUAL to execute manual loading");
+ if (isInitialized()) return;
+
mInitLock.writeLock().lock();
try {
+ if (mLoadState == LOAD_STATE_LOADING) return;
mLoadState = LOAD_STATE_LOADING;
} finally {
mInitLock.writeLock().unlock();
@@ -315,6 +372,21 @@
mHelper.loadMetadata();
}
+ private void loadMetadata() {
+ mInitLock.writeLock().lock();
+ try {
+ if (mMetadataLoadStrategy == LOAD_STRATEGY_DEFAULT) {
+ mLoadState = LOAD_STATE_LOADING;
+ }
+ } finally {
+ mInitLock.writeLock().unlock();
+ }
+
+ if (getLoadState() == LOAD_STATE_LOADING) {
+ mHelper.loadMetadata();
+ }
+ }
+
private void onMetadataLoadSuccess() {
final Collection<InitCallback> initCallbacks = new ArrayList<>();
mInitLock.writeLock().lock();
@@ -389,8 +461,8 @@
* Returns loading state of the EmojiCompat instance. When used on devices running API 18 or
* below always returns {@link #LOAD_STATE_SUCCEEDED}.
*
- * @return one of {@link #LOAD_STATE_LOADING}, {@link #LOAD_STATE_SUCCEEDED},
- * {@link #LOAD_STATE_FAILED}
+ * @return one of {@link #LOAD_STATE_DEFAULT}, {@link #LOAD_STATE_LOADING},
+ * {@link #LOAD_STATE_SUCCEEDED}, {@link #LOAD_STATE_FAILED}
*/
public @LoadState int getLoadState() {
mInitLock.readLock().lock();
@@ -805,6 +877,7 @@
private Set<InitCallback> mInitCallbacks;
private boolean mEmojiSpanIndicatorEnabled;
private int mEmojiSpanIndicatorColor = Color.GREEN;
+ @LoadStrategy private int mMetadataLoadStrategy = LOAD_STRATEGY_DEFAULT;
/**
* Default constructor.
@@ -938,6 +1011,21 @@
}
/**
+ * Determines the strategy to start loading the metadata. By default {@link EmojiCompat}
+ * will start loading the metadata during {@link EmojiCompat#init(Config)}. When set to
+ * {@link EmojiCompat#LOAD_STRATEGY_MANUAL}, you should call {@link EmojiCompat#load()} to
+ * initiate metadata loading.
+ *
+ * @param strategy one of {@link EmojiCompat#LOAD_STRATEGY_DEFAULT},
+ * {@link EmojiCompat#LOAD_STRATEGY_MANUAL}
+ *
+ */
+ public Config setMetadataLoadStrategy(@LoadStrategy int strategy) {
+ mMetadataLoadStrategy = strategy;
+ return this;
+ }
+
+ /**
* Returns the {@link MetadataRepoLoader}.
*/
protected final MetadataRepoLoader getMetadataRepoLoader() {
diff --git a/emoji/core/src/main/java/androidx/emoji/widget/EmojiInputFilter.java b/emoji/core/src/main/java/androidx/emoji/widget/EmojiInputFilter.java
index 22837a6..b44e859 100644
--- a/emoji/core/src/main/java/androidx/emoji/widget/EmojiInputFilter.java
+++ b/emoji/core/src/main/java/androidx/emoji/widget/EmojiInputFilter.java
@@ -79,6 +79,7 @@
return source;
case EmojiCompat.LOAD_STATE_LOADING:
+ case EmojiCompat.LOAD_STATE_DEFAULT:
EmojiCompat.get().registerInitCallback(getInitCallback());
return source;
diff --git a/emoji/core/src/main/java/androidx/emoji/widget/EmojiTextWatcher.java b/emoji/core/src/main/java/androidx/emoji/widget/EmojiTextWatcher.java
index 5d0e552..2197aff 100644
--- a/emoji/core/src/main/java/androidx/emoji/widget/EmojiTextWatcher.java
+++ b/emoji/core/src/main/java/androidx/emoji/widget/EmojiTextWatcher.java
@@ -80,6 +80,7 @@
mEmojiReplaceStrategy);
break;
case EmojiCompat.LOAD_STATE_LOADING:
+ case EmojiCompat.LOAD_STATE_DEFAULT:
EmojiCompat.get().registerInitCallback(getInitCallback());
break;
case EmojiCompat.LOAD_STATE_FAILED:
diff --git a/emoji/core/src/main/java/androidx/emoji/widget/EmojiTransformationMethod.java b/emoji/core/src/main/java/androidx/emoji/widget/EmojiTransformationMethod.java
index c082c83..81fe615 100644
--- a/emoji/core/src/main/java/androidx/emoji/widget/EmojiTransformationMethod.java
+++ b/emoji/core/src/main/java/androidx/emoji/widget/EmojiTransformationMethod.java
@@ -57,6 +57,7 @@
return EmojiCompat.get().process(source);
case EmojiCompat.LOAD_STATE_LOADING:
case EmojiCompat.LOAD_STATE_FAILED:
+ case EmojiCompat.LOAD_STATE_DEFAULT:
default:
break;
}
diff --git a/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java b/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
index f697f24..d685bf1 100644
--- a/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
+++ b/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
@@ -3920,7 +3920,7 @@
break;
}
case IFD_FORMAT_USHORT: {
- final String[] values = value.split(",");
+ final String[] values = value.split(",", -1);
final int[] intArray = new int[values.length];
for (int j = 0; j < values.length; ++j) {
intArray[j] = Integer.parseInt(values[j]);
@@ -3930,7 +3930,7 @@
break;
}
case IFD_FORMAT_SLONG: {
- final String[] values = value.split(",");
+ final String[] values = value.split(",", -1);
final int[] intArray = new int[values.length];
for (int j = 0; j < values.length; ++j) {
intArray[j] = Integer.parseInt(values[j]);
@@ -3940,7 +3940,7 @@
break;
}
case IFD_FORMAT_ULONG: {
- final String[] values = value.split(",");
+ final String[] values = value.split(",", -1);
final long[] longArray = new long[values.length];
for (int j = 0; j < values.length; ++j) {
longArray[j] = Long.parseLong(values[j]);
@@ -3950,10 +3950,10 @@
break;
}
case IFD_FORMAT_URATIONAL: {
- final String[] values = value.split(",");
+ final String[] values = value.split(",", -1);
final Rational[] rationalArray = new Rational[values.length];
for (int j = 0; j < values.length; ++j) {
- final String[] numbers = values[j].split("/");
+ final String[] numbers = values[j].split("/", -1);
rationalArray[j] = new Rational((long) Double.parseDouble(numbers[0]),
(long) Double.parseDouble(numbers[1]));
}
@@ -3962,10 +3962,10 @@
break;
}
case IFD_FORMAT_SRATIONAL: {
- final String[] values = value.split(",");
+ final String[] values = value.split(",", -1);
final Rational[] rationalArray = new Rational[values.length];
for (int j = 0; j < values.length; ++j) {
- final String[] numbers = values[j].split("/");
+ final String[] numbers = values[j].split("/", -1);
rationalArray[j] = new Rational((long) Double.parseDouble(numbers[0]),
(long) Double.parseDouble(numbers[1]));
}
@@ -3974,7 +3974,7 @@
break;
}
case IFD_FORMAT_DOUBLE: {
- final String[] values = value.split(",");
+ final String[] values = value.split(",", -1);
final double[] doubleArray = new double[values.length];
for (int j = 0; j < values.length; ++j) {
doubleArray[j] = Double.parseDouble(values[j]);
@@ -4510,7 +4510,7 @@
setAttribute(TAG_GPS_SPEED_REF, "K");
setAttribute(TAG_GPS_SPEED, new Rational(location.getSpeed()
* TimeUnit.HOURS.toSeconds(1) / 1000).toString());
- String[] dateTime = sFormatter.format(new Date(location.getTime())).split("\\s+");
+ String[] dateTime = sFormatter.format(new Date(location.getTime())).split("\\s+", -1);
setAttribute(ExifInterface.TAG_GPS_DATESTAMP, dateTime[0]);
setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, dateTime[1]);
}
@@ -4643,18 +4643,18 @@
private static double convertRationalLatLonToDouble(String rationalString, String ref) {
try {
- String [] parts = rationalString.split(",");
+ String [] parts = rationalString.split(",", -1);
String [] pair;
- pair = parts[0].split("/");
+ pair = parts[0].split("/", -1);
double degrees = Double.parseDouble(pair[0].trim())
/ Double.parseDouble(pair[1].trim());
- pair = parts[1].split("/");
+ pair = parts[1].split("/", -1);
double minutes = Double.parseDouble(pair[0].trim())
/ Double.parseDouble(pair[1].trim());
- pair = parts[2].split("/");
+ pair = parts[2].split("/", -1);
double seconds = Double.parseDouble(pair[0].trim())
/ Double.parseDouble(pair[1].trim());
@@ -6017,7 +6017,7 @@
// See TIFF 6.0 Section 2, "Image File Directory".
// Take the first component if there are more than one component.
if (entryValue.contains(",")) {
- String[] entryValues = entryValue.split(",");
+ String[] entryValues = entryValue.split(",", -1);
Pair<Integer, Integer> dataFormat = guessDataFormat(entryValues[0]);
if (dataFormat.first == IFD_FORMAT_STRING) {
return dataFormat;
@@ -6049,7 +6049,7 @@
}
if (entryValue.contains("/")) {
- String[] rationalNumber = entryValue.split("/");
+ String[] rationalNumber = entryValue.split("/", -1);
if (rationalNumber.length == 2) {
try {
long numerator = (long) Double.parseDouble(rationalNumber[0]);
diff --git a/fragment/ktx/OWNERS b/fragment/ktx/OWNERS
new file mode 100644
index 0000000..e450f4c
--- /dev/null
+++ b/fragment/ktx/OWNERS
@@ -0,0 +1 @@
+jakew@google.com
diff --git a/fragment/ktx/build.gradle b/fragment/ktx/build.gradle
new file mode 100644
index 0000000..52cb265
--- /dev/null
+++ b/fragment/ktx/build.gradle
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ buildTypes {
+ debug {
+ testCoverageEnabled = false // Breaks Kotlin compiler.
+ }
+ }
+}
+
+dependencies {
+ api(project(":fragment"))
+ api(KOTLIN_STDLIB)
+ androidTestImplementation(JUNIT)
+ androidTestImplementation(TRUTH)
+ androidTestImplementation(TEST_RUNNER_TMP, libs.exclude_for_espresso)
+ androidTestImplementation(TEST_RULES_TMP, libs.exclude_for_espresso)
+}
+
+supportLibrary {
+ name = "Fragment Kotlin Extensions"
+ publish = true
+ mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+ mavenGroup = LibraryGroups.FRAGMENT
+ inceptionYear = "2018"
+ description = "Kotlin extensions for 'fragment' artifact"
+}
diff --git a/fragment/ktx/src/androidTest/AndroidManifest.xml b/fragment/ktx/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..0a62e3e
--- /dev/null
+++ b/fragment/ktx/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.fragment.ktx">
+ <application>
+ <activity android:name="androidx.fragment.app.TestActivity"/>
+ </application>
+</manifest>
diff --git a/fragment/ktx/src/androidTest/java/androidx/fragment/app/FragmentManagerTest.kt b/fragment/ktx/src/androidTest/java/androidx/fragment/app/FragmentManagerTest.kt
new file mode 100644
index 0000000..7097955
--- /dev/null
+++ b/fragment/ktx/src/androidTest/java/androidx/fragment/app/FragmentManagerTest.kt
@@ -0,0 +1,59 @@
+package androidx.fragment.app
+
+import android.support.test.annotation.UiThreadTest
+import android.support.test.filters.MediumTest
+import android.support.test.rule.ActivityTestRule
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+
+@MediumTest
+class FragmentManagerTest {
+ @get:Rule val activityRule = ActivityTestRule<TestActivity>(TestActivity::class.java)
+ private val fragmentManager get() = activityRule.activity.supportFragmentManager
+
+ @UiThreadTest
+ @Test fun transaction() {
+ val fragment = TestFragment()
+ fragmentManager.transaction {
+ add(fragment, null)
+ }
+ assertThat(fragmentManager.fragments).doesNotContain(fragment)
+ fragmentManager.executePendingTransactions()
+ assertThat(fragmentManager.fragments).contains(fragment)
+ }
+
+ @UiThreadTest
+ @Test fun transactionNow() {
+ val fragment = TestFragment()
+ fragmentManager.transaction(now = true) {
+ add(fragment, null)
+ }
+ assertThat(fragmentManager.fragments).contains(fragment)
+ }
+
+ @UiThreadTest
+ @Test fun transactionAllowingStateLoss() {
+ // Use a detached FragmentManager to ensure state loss.
+ val fragmentManager = FragmentManagerImpl()
+
+ fragmentManager.transaction(allowStateLoss = true) {
+ add(TestFragment(), null)
+ }
+ assertThat(fragmentManager.fragments).isEmpty()
+ }
+
+ @UiThreadTest
+ @Test fun transactionNowAllowingStateLoss() {
+ // Use a detached FragmentManager to ensure state loss.
+ val fragmentManager = FragmentManagerImpl()
+
+ fragmentManager.transaction(now = true, allowStateLoss = true) {
+ add(TestFragment(), null)
+ }
+ assertThat(fragmentManager.fragments).isEmpty()
+ }
+}
+
+class TestActivity : FragmentActivity()
+class TestFragment : Fragment()
diff --git a/fragment/ktx/src/main/AndroidManifest.xml b/fragment/ktx/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..cd21eb1
--- /dev/null
+++ b/fragment/ktx/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest package="androidx.fragment.ktx"/>
diff --git a/fragment/ktx/src/main/java/androidx/fragment/app/FragmentManager.kt b/fragment/ktx/src/main/java/androidx/fragment/app/FragmentManager.kt
new file mode 100644
index 0000000..d9e8e96
--- /dev/null
+++ b/fragment/ktx/src/main/java/androidx/fragment/app/FragmentManager.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.fragment.app
+
+/**
+ * Run [body] in a [FragmentTransaction] which is automatically committed if it completes without
+ * exception.
+ *
+ * One of four commit functions will be used based on the values of `now` and `allowStateLoss`:
+ *
+ * | `now` | `allowStateLoss` | Method |
+ * | ----- | ---------------- | ------------------------------ |
+ * | false | false | `commit()` |
+ * | false | true | `commitAllowingStateLoss()` |
+ * | true | false | `commitNow()` |
+ * | true | true | `commitNowAllowingStateLoss()` |
+ */
+inline fun FragmentManager.transaction(
+ now: Boolean = false,
+ allowStateLoss: Boolean = false,
+ body: FragmentTransaction.() -> Unit
+) {
+ val transaction = beginTransaction()
+ transaction.body()
+ if (now) {
+ if (allowStateLoss) {
+ transaction.commitNowAllowingStateLoss()
+ } else {
+ transaction.commitNow()
+ }
+ } else {
+ if (allowStateLoss) {
+ transaction.commitAllowingStateLoss()
+ } else {
+ transaction.commit()
+ }
+ }
+}
diff --git a/heifwriter/api/current.txt b/heifwriter/api/current.txt
index c7311ba..3c39776 100644
--- a/heifwriter/api/current.txt
+++ b/heifwriter/api/current.txt
@@ -1,8 +1,6 @@
package androidx.heifwriter {
public final class HeifWriter implements java.lang.AutoCloseable {
- ctor public HeifWriter(java.lang.String, int, int, boolean, int, int, int, int, android.os.Handler) throws java.io.IOException;
- ctor public HeifWriter(java.io.FileDescriptor, int, int, boolean, int, int, int, int, android.os.Handler) throws java.io.IOException;
method public void addBitmap(android.graphics.Bitmap);
method public void addYuvBuffer(int, byte[]);
method public void close();
@@ -15,5 +13,17 @@
field public static final int INPUT_MODE_SURFACE = 1; // 0x1
}
+ public static final class HeifWriter.Builder {
+ ctor public HeifWriter.Builder(java.lang.String, int, int, int);
+ ctor public HeifWriter.Builder(java.io.FileDescriptor, int, int, int);
+ method public androidx.heifwriter.HeifWriter build() throws java.io.IOException;
+ method public androidx.heifwriter.HeifWriter.Builder setGridEnabled(boolean);
+ method public androidx.heifwriter.HeifWriter.Builder setHandler(android.os.Handler);
+ method public androidx.heifwriter.HeifWriter.Builder setMaxImages(int);
+ method public androidx.heifwriter.HeifWriter.Builder setPrimaryIndex(int);
+ method public androidx.heifwriter.HeifWriter.Builder setQuality(int);
+ method public androidx.heifwriter.HeifWriter.Builder setRotation(int);
+ }
+
}
diff --git a/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java b/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
index 99f47d2..127ba1c 100644
--- a/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
+++ b/heifwriter/src/androidTest/java/androidx/heifwriter/HeifWriterTest.java
@@ -18,12 +18,6 @@
import static android.support.test.InstrumentationRegistry.getContext;
-import static androidx.heifwriter.HeifWriter.INPUT_MODE_BITMAP;
-import static androidx.heifwriter.HeifWriter.INPUT_MODE_BUFFER;
-import static androidx.heifwriter.HeifWriter.INPUT_MODE_SURFACE;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-
import android.graphics.Bitmap;
import android.graphics.ImageFormat;
import android.media.MediaExtractor;
@@ -34,13 +28,21 @@
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Process;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import android.support.test.filters.LargeTest;
import android.support.test.runner.AndroidJUnit4;
import android.util.Log;
+
+import static androidx.heifwriter.HeifWriter.INPUT_MODE_BITMAP;
+import static androidx.heifwriter.HeifWriter.INPUT_MODE_BUFFER;
+import static androidx.heifwriter.HeifWriter.INPUT_MODE_SURFACE;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.heifwriter.test.R;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -222,7 +224,11 @@
}
private void doTestForVariousNumberImages(TestConfig.Builder builder) throws Exception {
- doTest(builder.setNumImages(4).build());
+ builder.setNumImages(4);
+ doTest(builder.setRotation(270).build());
+ doTest(builder.setRotation(180).build());
+ doTest(builder.setRotation(90).build());
+ doTest(builder.setRotation(0).build());
doTest(builder.setNumImages(1).build());
doTest(builder.setNumImages(8).build());
}
@@ -250,102 +256,109 @@
}
private static class TestConfig {
- final int inputMode;
- final boolean useGrid;
- final boolean useHandler;
- final int maxNumImages;
- final int numImages;
- final int width;
- final int height;
- final int quality;
- final String inputPath;
- final String outputPath;
- final Bitmap[] bitmaps;
+ final int mInputMode;
+ final boolean mUseGrid;
+ final boolean mUseHandler;
+ final int mMaxNumImages;
+ final int mNumImages;
+ final int mWidth;
+ final int mHeight;
+ final int mRotation;
+ final int mQuality;
+ final String mInputPath;
+ final String mOutputPath;
+ final Bitmap[] mBitmaps;
- TestConfig(int _inputMode, boolean _useGrid, boolean _useHandler,
- int _maxNumImage, int _numImages, int _width, int _height, int _quality,
- String _inputPath, String _outputPath, Bitmap[] _bitmaps) {
- inputMode = _inputMode;
- useGrid = _useGrid;
- useHandler = _useHandler;
- maxNumImages = _maxNumImage;
- numImages = _numImages;
- width = _width;
- height = _height;
- quality = _quality;
- inputPath = _inputPath;
- outputPath = _outputPath;
- bitmaps = _bitmaps;
+ TestConfig(int inputMode, boolean useGrid, boolean useHandler,
+ int maxNumImages, int numImages, int width, int height,
+ int rotation, int quality,
+ String inputPath, String outputPath, Bitmap[] bitmaps) {
+ mInputMode = inputMode;
+ mUseGrid = useGrid;
+ mUseHandler = useHandler;
+ mMaxNumImages = maxNumImages;
+ mNumImages = numImages;
+ mWidth = width;
+ mHeight = height;
+ mRotation = rotation;
+ mQuality = quality;
+ mInputPath = inputPath;
+ mOutputPath = outputPath;
+ mBitmaps = bitmaps;
}
static class Builder {
- final int inputMode;
- final boolean useGrid;
- final boolean useHandler;
- int maxNumImages;
- int numImages;
- int width;
- int height;
- int quality;
- String inputPath;
- final String outputPath;
- Bitmap[] bitmaps;
-
- boolean numImagesSetExplicitly;
+ final int mInputMode;
+ final boolean mUseGrid;
+ final boolean mUseHandler;
+ int mMaxNumImages;
+ int mNumImages;
+ int mWidth;
+ int mHeight;
+ int mRotation;
+ final int mQuality;
+ String mInputPath;
+ final String mOutputPath;
+ Bitmap[] mBitmaps;
+ boolean mNumImagesSetExplicitly;
- Builder(int _inputMode, boolean _useGrids, boolean _useHandler) {
- inputMode = _inputMode;
- useGrid = _useGrids;
- useHandler = _useHandler;
- maxNumImages = numImages = 4;
- width = 1920;
- height = 1080;
- quality = 100;
- outputPath = new File(Environment.getExternalStorageDirectory(),
+ Builder(int inputMode, boolean useGrids, boolean useHandler) {
+ mInputMode = inputMode;
+ mUseGrid = useGrids;
+ mUseHandler = useHandler;
+ mMaxNumImages = mNumImages = 4;
+ mWidth = 1920;
+ mHeight = 1080;
+ mRotation = 0;
+ mQuality = 100;
+ mOutputPath = new File(Environment.getExternalStorageDirectory(),
OUTPUT_FILENAME).getAbsolutePath();
}
- Builder setInputPath(String _inputPath) {
- inputPath = (inputMode == INPUT_MODE_BITMAP) ? _inputPath : null;
+ Builder setInputPath(String inputPath) {
+ mInputPath = (mInputMode == INPUT_MODE_BITMAP) ? inputPath : null;
return this;
}
- Builder setNumImages(int _numImages) {
- numImagesSetExplicitly = true;
- numImages = _numImages;
+ Builder setNumImages(int numImages) {
+ mNumImagesSetExplicitly = true;
+ mNumImages = numImages;
+ return this;
+ }
+
+ Builder setRotation(int rotation) {
+ mRotation = rotation;
return this;
}
private void loadBitmapInputs() {
- if (inputMode != INPUT_MODE_BITMAP) {
+ if (mInputMode != INPUT_MODE_BITMAP) {
return;
}
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
- retriever.setDataSource(inputPath);
+ retriever.setDataSource(mInputPath);
String hasImage = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE);
if (!"yes".equals(hasImage)) {
throw new IllegalArgumentException("no bitmap found!");
}
- width = Integer.parseInt(retriever.extractMetadata(
- MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH));
- height = Integer.parseInt(retriever.extractMetadata(
- MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT));
- maxNumImages = Math.min(maxNumImages, Integer.parseInt(retriever.extractMetadata(
+ mMaxNumImages = Math.min(mMaxNumImages, Integer.parseInt(retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT)));
- if (!numImagesSetExplicitly) {
- numImages = maxNumImages;
+ if (!mNumImagesSetExplicitly) {
+ mNumImages = mMaxNumImages;
}
- bitmaps = new Bitmap[maxNumImages];
- for (int i = 0; i < bitmaps.length; i++) {
- bitmaps[i] = retriever.getImageAtIndex(i);
+ mBitmaps = new Bitmap[mMaxNumImages];
+ for (int i = 0; i < mBitmaps.length; i++) {
+ mBitmaps[i] = retriever.getImageAtIndex(i);
}
+ mWidth = mBitmaps[0].getWidth();
+ mHeight = mBitmaps[0].getHeight();
retriever.release();
}
private void cleanupStaleOutputs() {
- File outputFile = new File(outputPath);
+ File outputFile = new File(mOutputPath);
if (outputFile.exists()) {
outputFile.delete();
}
@@ -355,58 +368,61 @@
cleanupStaleOutputs();
loadBitmapInputs();
- return new TestConfig(inputMode, useGrid, useHandler, maxNumImages, numImages,
- width, height, quality, inputPath, outputPath, bitmaps);
+ return new TestConfig(mInputMode, mUseGrid, mUseHandler, mMaxNumImages, mNumImages,
+ mWidth, mHeight, mRotation, mQuality, mInputPath, mOutputPath, mBitmaps);
}
}
@Override
public String toString() {
- return "TestConfig" +
- ": inputMode " + inputMode +
- ", useGrid " + useGrid +
- ", useHandler " + useHandler +
- ", maxNumImages " + maxNumImages +
- ", numImages " + numImages +
- ", width " + width +
- ", height " + height +
- ", quality " + quality +
- ", inputPath " + inputPath +
- ", outputPath " + outputPath;
+ return "TestConfig"
+ + ": mInputMode " + mInputMode
+ + ", mUseGrid " + mUseGrid
+ + ", mUseHandler " + mUseHandler
+ + ", mMaxNumImages " + mMaxNumImages
+ + ", mNumImages " + mNumImages
+ + ", mWidth " + mWidth
+ + ", mHeight " + mHeight
+ + ", mRotation " + mRotation
+ + ", mQuality " + mQuality
+ + ", mInputPath " + mInputPath
+ + ", mOutputPath " + mOutputPath;
}
}
- private void doTest(TestConfig testConfig) throws Exception {
- int width = testConfig.width;
- int height = testConfig.height;
- int numImages = testConfig.numImages;
+ private void doTest(TestConfig config) throws Exception {
+ int width = config.mWidth;
+ int height = config.mHeight;
+ int numImages = config.mNumImages;
mInputIndex = 0;
HeifWriter heifWriter = null;
FileInputStream inputStream = null;
FileOutputStream outputStream = null;
try {
- if (DEBUG) Log.d(TAG, "started: " + testConfig);
+ if (DEBUG) Log.d(TAG, "started: " + config);
- heifWriter = new HeifWriter(testConfig.outputPath, width, height,
- testConfig.useGrid,
- testConfig.quality,
- testConfig.maxNumImages,
- testConfig.maxNumImages - 1,
- testConfig.inputMode,
- testConfig.useHandler ? mHandler : null);
+ heifWriter = new HeifWriter.Builder(
+ config.mOutputPath, width, height, config.mInputMode)
+ .setRotation(config.mRotation)
+ .setGridEnabled(config.mUseGrid)
+ .setMaxImages(config.mMaxNumImages)
+ .setQuality(config.mQuality)
+ .setPrimaryIndex(config.mMaxNumImages - 1)
+ .setHandler(config.mUseHandler ? mHandler : null)
+ .build();
- if (testConfig.inputMode == INPUT_MODE_SURFACE) {
+ if (config.mInputMode == INPUT_MODE_SURFACE) {
mInputEglSurface = new EglWindowSurface(heifWriter.getInputSurface());
}
heifWriter.start();
- if (testConfig.inputMode == INPUT_MODE_BUFFER) {
+ if (config.mInputMode == INPUT_MODE_BUFFER) {
byte[] data = new byte[width * height * 3 / 2];
- if (testConfig.inputPath != null) {
- inputStream = new FileInputStream(testConfig.inputPath);
+ if (config.mInputPath != null) {
+ inputStream = new FileInputStream(config.mInputPath);
}
if (DUMP_YUV_INPUT) {
@@ -424,7 +440,7 @@
}
heifWriter.addYuvBuffer(ImageFormat.YUV_420_888, data);
}
- } else if (testConfig.inputMode == INPUT_MODE_SURFACE) {
+ } else if (config.mInputMode == INPUT_MODE_SURFACE) {
// The input surface is a surface texture using single buffer mode, draws will be
// blocked until onFrameAvailable is done with the buffer, which is dependant on
// how fast MediaCodec processes them, which is further dependent on how fast the
@@ -436,8 +452,8 @@
}
heifWriter.setInputEndOfStreamTimestamp(
1000 * computePresentationTime(numImages - 1));
- } else if (testConfig.inputMode == INPUT_MODE_BITMAP) {
- Bitmap[] bitmaps = testConfig.bitmaps;
+ } else if (config.mInputMode == INPUT_MODE_BITMAP) {
+ Bitmap[] bitmaps = config.mBitmaps;
for (int i = 0; i < Math.min(bitmaps.length, numImages); i++) {
if (DEBUG) Log.d(TAG, "addBitmap: " + i);
heifWriter.addBitmap(bitmaps[i]);
@@ -446,8 +462,8 @@
}
heifWriter.stop(3000);
- verifyResult(testConfig.outputPath, width, height, testConfig.useGrid,
- Math.min(numImages, testConfig.maxNumImages));
+ verifyResult(config.mOutputPath, width, height, config.mRotation, config.mUseGrid,
+ Math.min(numImages, config.mMaxNumImages));
if (DEBUG) Log.d(TAG, "finished: PASS");
} finally {
try {
@@ -530,7 +546,7 @@
}
private void verifyResult(
- String filename, int width, int height, boolean useGrid, int numImages)
+ String filename, int width, int height, int rotation, boolean useGrid, int numImages)
throws Exception {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(filename);
@@ -540,23 +556,30 @@
}
assertEquals("Wrong image count", numImages,
Integer.parseInt(retriever.extractMetadata(
- MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT)));
+ MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT)));
assertEquals("Wrong width", width,
Integer.parseInt(retriever.extractMetadata(
- MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH)));
+ MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH)));
assertEquals("Wrong height", height,
Integer.parseInt(retriever.extractMetadata(
- MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT)));
+ MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT)));
+ assertEquals("Wrong rotation", rotation,
+ Integer.parseInt(retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION)));
retriever.release();
if (useGrid) {
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(filename);
MediaFormat format = extractor.getTrackFormat(0);
- int gridWidth = format.getInteger(MediaFormat.KEY_GRID_WIDTH);
- int gridHeight = format.getInteger(MediaFormat.KEY_GRID_HEIGHT);
- assertEquals("Wrong grid width", 512, gridWidth);
- assertEquals("Wrong grid height", 512, gridHeight);
+ int gridWidth = format.getInteger(MediaFormat.KEY_TILE_WIDTH);
+ int gridHeight = format.getInteger(MediaFormat.KEY_TILE_HEIGHT);
+ int gridRows = format.getInteger(MediaFormat.KEY_GRID_ROWS);
+ int gridCols = format.getInteger(MediaFormat.KEY_GRID_COLUMNS);
+ assertTrue("Wrong grid width or cols",
+ ((width + gridWidth - 1) / gridWidth) == gridCols);
+ assertTrue("Wrong grid height or rows",
+ ((height + gridHeight - 1) / gridHeight) == gridRows);
extractor.release();
}
}
diff --git a/heifwriter/src/main/java/androidx/heifwriter/EglWindowSurface.java b/heifwriter/src/main/java/androidx/heifwriter/EglWindowSurface.java
index 9b5df45..35d34d4 100644
--- a/heifwriter/src/main/java/androidx/heifwriter/EglWindowSurface.java
+++ b/heifwriter/src/main/java/androidx/heifwriter/EglWindowSurface.java
@@ -25,6 +25,8 @@
import android.util.Log;
import android.view.Surface;
+import java.util.Objects;
+
/**
* Holds state associated with a Surface used for MediaCodec encoder input.
* <p>
@@ -63,7 +65,7 @@
*/
private void eglSetup() {
mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
- if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
+ if (Objects.equals(mEGLDisplay, EGL14.EGL_NO_DISPLAY)) {
throw new RuntimeException("unable to get EGL14 display");
}
int[] version = new int[2];
@@ -130,7 +132,7 @@
}
private void releaseEGLSurface() {
- if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
+ if (!Objects.equals(mEGLDisplay, EGL14.EGL_NO_DISPLAY)) {
EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface);
mEGLSurface = EGL14.EGL_NO_SURFACE;
}
@@ -141,7 +143,7 @@
* Surface that was passed to our constructor.
*/
public void release() {
- if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
+ if (!Objects.equals(mEGLDisplay, EGL14.EGL_NO_DISPLAY)) {
EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface);
EGL14.eglDestroyContext(mEGLDisplay, mEGLContext);
EGL14.eglReleaseThread();
diff --git a/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java b/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java
index 85aa925..be5425a 100644
--- a/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java
+++ b/heifwriter/src/main/java/androidx/heifwriter/HeifEncoder.java
@@ -17,14 +17,9 @@
package androidx.heifwriter;
import android.graphics.Bitmap;
+import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
-import android.opengl.GLES20;
-import android.opengl.GLUtils;
-import android.os.Looper;
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import android.media.Image;
import android.media.MediaCodec;
import android.media.MediaCodec.BufferInfo;
@@ -32,13 +27,19 @@
import android.media.MediaCodecInfo;
import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaFormat;
+import android.opengl.GLES20;
import android.os.Handler;
import android.os.HandlerThread;
+import android.os.Looper;
import android.os.Process;
import android.util.Log;
import android.util.Range;
import android.view.Surface;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -179,25 +180,29 @@
throw new IllegalArgumentException("invalid encoder inputs");
}
- mEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_HEVC);
+ // Disable grid if the image is too small
+ useGrid &= (width > GRID_WIDTH || height > GRID_HEIGHT);
- mWidth = width;
- mHeight = height;
-
- if (useGrid) {
- mGridWidth = GRID_WIDTH;
- mGridHeight = GRID_HEIGHT;
- mGridRows = (height + GRID_HEIGHT - 1) / GRID_HEIGHT;
- mGridCols = (width + GRID_WIDTH - 1) / GRID_WIDTH;
- } else {
- mGridWidth = mWidth;
- mGridHeight = mHeight;
- mGridRows = 1;
- mGridCols = 1;
+ boolean useHeicEncoder = false;
+ MediaCodecInfo.CodecCapabilities caps = null;
+ try {
+ mEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC);
+ caps = mEncoder.getCodecInfo().getCapabilitiesForType(
+ MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC);
+ // If the HEIC encoder can't support the size, fall back to HEVC encoder.
+ if (!caps.getVideoCapabilities().isSizeSupported(width, height)) {
+ mEncoder.release();
+ mEncoder = null;
+ throw new Exception();
+ }
+ useHeicEncoder = true;
+ } catch (Exception e) {
+ mEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_HEVC);
+ caps = mEncoder.getCodecInfo().getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_HEVC);
+ // Always enable grid if the size is too large for the HEVC encoder
+ useGrid |= !caps.getVideoCapabilities().isSizeSupported(width, height);
}
- mNumTiles = mGridRows * mGridCols;
-
mInputMode = inputMode;
mCallback = cb;
@@ -217,18 +222,59 @@
int colorFormat = useSurfaceInternally ? CodecCapabilities.COLOR_FormatSurface :
CodecCapabilities.COLOR_FormatYUV420Flexible;
- // TODO: determine how to set bitrate and framerate, or use constant quality
- MediaFormat codecFormat = MediaFormat.createVideoFormat(
- MediaFormat.MIMETYPE_VIDEO_HEVC, mGridWidth, mGridHeight);
+ mWidth = width;
+ mHeight = height;
+
+ int gridWidth, gridHeight, gridRows, gridCols;
+
+ if (useGrid) {
+ gridWidth = GRID_WIDTH;
+ gridHeight = GRID_HEIGHT;
+ gridRows = (height + GRID_HEIGHT - 1) / GRID_HEIGHT;
+ gridCols = (width + GRID_WIDTH - 1) / GRID_WIDTH;
+ } else {
+ gridWidth = mWidth;
+ gridHeight = mHeight;
+ gridRows = 1;
+ gridCols = 1;
+ }
+
+ MediaFormat codecFormat;
+ if (useHeicEncoder) {
+ codecFormat = MediaFormat.createVideoFormat(
+ MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC, mWidth, mHeight);
+ } else {
+ codecFormat = MediaFormat.createVideoFormat(
+ MediaFormat.MIMETYPE_VIDEO_HEVC, gridWidth, gridHeight);
+ }
+
+ if (useGrid) {
+ codecFormat.setInteger(MediaFormat.KEY_TILE_WIDTH, gridWidth);
+ codecFormat.setInteger(MediaFormat.KEY_TILE_HEIGHT, gridHeight);
+ codecFormat.setInteger(MediaFormat.KEY_GRID_COLUMNS, gridCols);
+ codecFormat.setInteger(MediaFormat.KEY_GRID_ROWS, gridRows);
+ }
+
+ if (useHeicEncoder) {
+ mGridWidth = width;
+ mGridHeight = height;
+ mGridRows = 1;
+ mGridCols = 1;
+ } else {
+ mGridWidth = gridWidth;
+ mGridHeight = gridHeight;
+ mGridRows = gridRows;
+ mGridCols = gridCols;
+ }
+ mNumTiles = mGridRows * mGridCols;
+
codecFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 0);
codecFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
-
- MediaCodecInfo.CodecCapabilities caps =
- mEncoder.getCodecInfo().getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_HEVC);
- MediaCodecInfo.EncoderCapabilities encoderCaps = caps.getEncoderCapabilities();
-
codecFormat.setInteger(MediaFormat.KEY_FRAME_RATE, mNumTiles);
codecFormat.setInteger(MediaFormat.KEY_CAPTURE_RATE, mNumTiles * 30);
+
+ MediaCodecInfo.EncoderCapabilities encoderCaps = caps.getEncoderCapabilities();
+
if (encoderCaps.isBitrateModeSupported(
MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ)) {
Log.d(TAG, "Setting bitrate mode to constant quality");
@@ -240,14 +286,14 @@
(qualityRange.getUpper() - qualityRange.getLower()) * quality / 100.0));
} else {
if (encoderCaps.isBitrateModeSupported(
- MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR)) {
- Log.d(TAG, "Setting bitrate mode to variable bitrate");
- codecFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
- MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
- } else { // assume CBR
+ MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR)) {
Log.d(TAG, "Setting bitrate mode to constant bitrate");
codecFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
+ } else { // assume VBR
+ Log.d(TAG, "Setting bitrate mode to variable bitrate");
+ codecFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
+ MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
}
// Calculate the bitrate based on image dimension, max compression ratio and quality.
// Note that we set the frame rate to the number of tiles, so the bitrate would be the
@@ -262,34 +308,36 @@
if (useSurfaceInternally) {
mEncoderSurface = mEncoder.createInputSurface();
- boolean useGLCopy = (mNumTiles > 1) || (inputMode == INPUT_MODE_BITMAP);
- mEOSTracker = new SurfaceEOSTracker(useGLCopy);
+ boolean copyTiles = (mNumTiles > 1);
+ mEOSTracker = new SurfaceEOSTracker(copyTiles);
- if (useGLCopy) {
- mEncoderEglSurface = new EglWindowSurface(mEncoderSurface);
- mEncoderEglSurface.makeCurrent();
+ if (inputMode == INPUT_MODE_SURFACE) {
+ if (copyTiles) {
+ mEncoderEglSurface = new EglWindowSurface(mEncoderSurface);
+ mEncoderEglSurface.makeCurrent();
- mRectBlt = new EglRectBlt(
- new Texture2dProgram((inputMode == INPUT_MODE_BITMAP) ?
- Texture2dProgram.TEXTURE_2D :
- Texture2dProgram.TEXTURE_EXT),
- mWidth, mHeight);
+ mRectBlt = new EglRectBlt(
+ new Texture2dProgram((inputMode == INPUT_MODE_BITMAP)
+ ? Texture2dProgram.TEXTURE_2D
+ : Texture2dProgram.TEXTURE_EXT),
+ mWidth, mHeight);
- mTextureId = mRectBlt.createTextureObject();
+ mTextureId = mRectBlt.createTextureObject();
- if (inputMode == INPUT_MODE_SURFACE) {
- // use single buffer mode to block on input
- mInputTexture = new SurfaceTexture(mTextureId, true);
- mInputTexture.setOnFrameAvailableListener(this);
- mInputTexture.setDefaultBufferSize(mWidth, mHeight);
- mInputSurface = new Surface(mInputTexture);
+ if (inputMode == INPUT_MODE_SURFACE) {
+ // use single buffer mode to block on input
+ mInputTexture = new SurfaceTexture(mTextureId, true);
+ mInputTexture.setOnFrameAvailableListener(this);
+ mInputTexture.setDefaultBufferSize(mWidth, mHeight);
+ mInputSurface = new Surface(mInputTexture);
+ }
+
+ // make uncurrent since onFrameAvailable could be called on arbituray thread.
+ // making the context current on a different thread will cause error.
+ mEncoderEglSurface.makeUnCurrent();
+ } else {
+ mInputSurface = mEncoderSurface;
}
-
- // make uncurrent since the onFrameAvailable could be called on arbituray thread.
- // making the context current on a different thread will cause error.
- mEncoderEglSurface.makeUnCurrent();
- } else {
- mInputSurface = mEncoderSurface;
}
} else {
for (int i = 0; i < INPUT_BUFFER_POOL_SIZE; i++) {
@@ -321,7 +369,20 @@
computePresentationTime(mInputIndex + mNumTiles - 1));
if (takeFrame) {
- copyTilesGL(mTmpMatrix);
+ // Copies from surface texture to encoder inputs using GL.
+ GLES20.glViewport(0, 0, mGridWidth, mGridHeight);
+
+ for (int row = 0; row < mGridRows; row++) {
+ for (int col = 0; col < mGridCols; col++) {
+ int left = col * mGridWidth;
+ int top = row * mGridHeight;
+ mSrcRect.set(left, top, left + mGridWidth, top + mGridHeight);
+ mRectBlt.copyRect(mTextureId, mTmpMatrix, mSrcRect);
+ mEncoderEglSurface.setPresentationTime(
+ 1000 * computePresentationTime(mInputIndex++));
+ mEncoderEglSurface.swapBuffers();
+ }
+ }
}
surfaceTexture.releaseTexImage();
@@ -407,21 +468,16 @@
if (!takeFrame) return;
synchronized (this) {
- if (mEncoderEglSurface == null) {
- return;
+ for (int row = 0; row < mGridRows; row++) {
+ for (int col = 0; col < mGridCols; col++) {
+ int left = col * mGridWidth;
+ int top = row * mGridHeight;
+ mSrcRect.set(left, top, left + mGridWidth, top + mGridHeight);
+ Canvas canvas = mEncoderSurface.lockCanvas(null);
+ canvas.drawBitmap(bitmap, mSrcRect, mDstRect, null);
+ mEncoderSurface.unlockCanvasAndPost(canvas);
+ }
}
-
- mEncoderEglSurface.makeCurrent();
-
- // load the bitmap to texture
- GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId);
- GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
-
- copyTilesGL(Texture2dProgram.V_FLIP_MATRIX);
-
- // make uncurrent since the onFrameAvailable could be called on arbituray thread.
- // making the context current on a different thread will cause error.
- mEncoderEglSurface.makeUnCurrent();
}
}
@@ -594,28 +650,6 @@
}
/**
- * Copies from source frame to encoder inputs using GL. The source could be either
- * client's input surface, or the input bitmap loaded to texture.
- *
- * @param texMatrix The texture matrix to use. See the shader program in
- * {@link Texture2dProgram} as well as {@link SurfaceTexture} for more details.
- */
- private void copyTilesGL(float[] texMatrix) {
- GLES20.glViewport(0, 0, mGridWidth, mGridHeight);
-
- for (int row = 0; row < mGridRows; row++) {
- for (int col = 0; col < mGridCols; col++) {
- int left = col * mGridWidth;
- int top = row * mGridHeight;
- mSrcRect.set(left, top, left + mGridWidth, top + mGridHeight);
- mRectBlt.copyRect(mTextureId, texMatrix, mSrcRect);
- mEncoderEglSurface.setPresentationTime(1000 * computePresentationTime(mInputIndex++));
- mEncoderEglSurface.swapBuffers();
- }
- }
- }
-
- /**
* Routine to release all resources. Must be run on the same looper that
* handles the MediaCodec callbacks.
*/
@@ -677,7 +711,7 @@
private class SurfaceEOSTracker {
private static final boolean DEBUG_EOS = false;
- final boolean mUseGLCopy;
+ final boolean mCopyTiles;
long mInputEOSTimeNs = -1;
long mLastInputTimeNs = -1;
long mEncoderEOSTimeUs = -1;
@@ -685,14 +719,14 @@
long mLastOutputTimeUs = -1;
boolean mSignaled;
- SurfaceEOSTracker(boolean useGLCopy) {
- mUseGLCopy = useGLCopy;
+ SurfaceEOSTracker(boolean copyTiles) {
+ mCopyTiles = copyTiles;
}
synchronized void updateInputEOSTime(long timestampNs) {
if (DEBUG_EOS) Log.d(TAG, "updateInputEOSTime: " + timestampNs);
- if (mUseGLCopy) {
+ if (mCopyTiles) {
if (mInputEOSTimeNs < 0) {
mInputEOSTimeNs = timestampNs;
}
@@ -773,15 +807,18 @@
if (DEBUG) Log.d(TAG, "onOutputFormatChanged: " + format);
- format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC);
- format.setInteger(MediaFormat.KEY_WIDTH, mWidth);
- format.setInteger(MediaFormat.KEY_HEIGHT, mHeight);
+ if (!MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC.equals(
+ format.getString(MediaFormat.KEY_MIME))) {
+ format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC);
+ format.setInteger(MediaFormat.KEY_WIDTH, mWidth);
+ format.setInteger(MediaFormat.KEY_HEIGHT, mHeight);
- if (mNumTiles > 1) {
- format.setInteger(MediaFormat.KEY_GRID_WIDTH, mGridWidth);
- format.setInteger(MediaFormat.KEY_GRID_HEIGHT, mGridHeight);
- format.setInteger(MediaFormat.KEY_GRID_ROWS, mGridRows);
- format.setInteger(MediaFormat.KEY_GRID_COLS, mGridCols);
+ if (mNumTiles > 1) {
+ format.setInteger(MediaFormat.KEY_TILE_WIDTH, mGridWidth);
+ format.setInteger(MediaFormat.KEY_TILE_HEIGHT, mGridHeight);
+ format.setInteger(MediaFormat.KEY_GRID_ROWS, mGridRows);
+ format.setInteger(MediaFormat.KEY_GRID_COLUMNS, mGridCols);
+ }
}
mCallback.onOutputFormatChanged(HeifEncoder.this, format);
@@ -800,8 +837,12 @@
public void onOutputBufferAvailable(MediaCodec codec, int index, BufferInfo info) {
if (codec != mEncoder || mOutputEOS) return;
- if (DEBUG) Log.d(TAG, "onInputBufferAvailable: " + index + ", time "
- + info.presentationTimeUs + ", size " + info.size + ", flags " + info.flags);
+ if (DEBUG) {
+ Log.d(TAG, "onOutputBufferAvailable: " + index
+ + ", time " + info.presentationTimeUs
+ + ", size " + info.size
+ + ", flags " + info.flags);
+ }
if ((info.size > 0) && ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0)) {
ByteBuffer outputBuffer = codec.getOutputBuffer(index);
diff --git a/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java b/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java
index be7dffb..bc657c4 100644
--- a/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java
+++ b/heifwriter/src/main/java/androidx/heifwriter/HeifWriter.java
@@ -16,6 +16,8 @@
package androidx.heifwriter;
+import static android.media.MediaMuxer.OutputFormat.MUXER_OUTPUT_HEIF;
+
import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.media.MediaCodec;
@@ -25,11 +27,12 @@
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Process;
+import android.util.Log;
+import android.view.Surface;
+
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import android.util.Log;
-import android.view.Surface;
import java.io.FileDescriptor;
import java.io.IOException;
@@ -38,8 +41,6 @@
import java.nio.ByteBuffer;
import java.util.concurrent.TimeoutException;
-import static android.media.MediaMuxer.OutputFormat.MUXER_OUTPUT_HEIF;
-
/**
* This class writes one or more still images (of the same dimensions) into
* a heif file.
@@ -79,7 +80,8 @@
private final HandlerThread mHandlerThread;
private final Handler mHandler;
private int mNumTiles;
- private final int mNumImages;
+ private final int mRotation;
+ private final int mMaxImages;
private final int mPrimaryIndex;
private final ResultWaiter mResultWaiter = new ResultWaiter();
@@ -119,96 +121,200 @@
public @interface InputMode {}
/**
- * Construct a heif writer that writes to a file specified by its path.
- *
- * @param path Path of the file to be written.
- * @param width Width of the image.
- * @param height Height of the image.
- * @param useGrid Whether to encode image into tiles. If enabled, the tile size will be
- * automatically chosen.
- * @param quality A number between 0 and 100 (inclusive), with 100 indicating the best quality
- * supported by this implementation (which often results in larger file size).
- * @param numImages Max number of images to write. Frames exceeding this number will not be
- * written to file. The writing can be stopped earlier before this number of
- * images are written by {@link #stop(long)}, except for the input mode of
- * {@link #INPUT_MODE_SURFACE}, where the EOS timestamp must be specified (via
- * {@link #setInputEndOfStreamTimestamp(long)} and reached.
- * @param primaryIndex Index of the image that should be marked as primary, must be within range
- * [0, numImages - 1] inclusive.
- * @param inputMode Input mode for this writer, must be one of {@link #INPUT_MODE_BUFFER},
- * {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
- * @param handler If not null, client will receive all callbacks on the handler's looper.
- * Otherwise, client will receive callbacks on a looper created by the writer.
- *
- * @throws IOException if failed to construct MediaMuxer or HeifEncoder.
+ * Builder class for constructing a HeifWriter object from specified parameters.
*/
- @SuppressLint("WrongConstant")
- public HeifWriter(@NonNull String path,
- int width, int height, boolean useGrid,
- int quality, int numImages, int primaryIndex,
- @InputMode int inputMode,
- @Nullable Handler handler) throws IOException {
- this(width, height, useGrid, quality, numImages, primaryIndex, inputMode, handler,
- new MediaMuxer(path, MUXER_OUTPUT_HEIF));
+ public static final class Builder {
+ private final String mPath;
+ private final FileDescriptor mFd;
+ private final int mWidth;
+ private final int mHeight;
+ private final @InputMode int mInputMode;
+ private boolean mGridEnabled = true;
+ private int mQuality = 100;
+ private int mMaxImages = 1;
+ private int mPrimaryIndex = 0;
+ private int mRotation = 0;
+ private Handler mHandler;
+
+ /**
+ * Construct a Builder with output specified by its path.
+ *
+ * @param path Path of the file to be written.
+ * @param width Width of the image.
+ * @param height Height of the image.
+ * @param inputMode Input mode for this writer, must be one of {@link #INPUT_MODE_BUFFER},
+ * {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
+ */
+ public Builder(@NonNull String path,
+ int width, int height, @InputMode int inputMode) {
+ this(path, null, width, height, inputMode);
+ }
+
+ /**
+ * Construct a Builder with output specified by its file descriptor.
+ *
+ * @param fd File descriptor of the file to be written.
+ * @param width Width of the image.
+ * @param height Height of the image.
+ * @param inputMode Input mode for this writer, must be one of {@link #INPUT_MODE_BUFFER},
+ * {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
+ */
+ public Builder(@NonNull FileDescriptor fd,
+ int width, int height, @InputMode int inputMode) {
+ this(null, fd, width, height, inputMode);
+ }
+
+ private Builder(String path, FileDescriptor fd,
+ int width, int height, @InputMode int inputMode) {
+ if (width <= 0 || height <= 0) {
+ throw new IllegalArgumentException("Invalid image size: " + width + "x" + height);
+ }
+ mPath = path;
+ mFd = fd;
+ mWidth = width;
+ mHeight = height;
+ mInputMode = inputMode;
+ }
+
+ /**
+ * Set the image rotation in degrees.
+ *
+ * @param rotation Rotation angle (clockwise) of the image, must be 0, 90, 180 or 270.
+ * Default is 0.
+ * @return this Builder object.
+ */
+ public Builder setRotation(int rotation) {
+ if (rotation != 0 && rotation != 90 && rotation != 180 && rotation != 270) {
+ throw new IllegalArgumentException("Invalid rotation angle: " + rotation);
+ }
+ mRotation = rotation;
+ return this;
+ }
+
+ /**
+ * Set whether to enable grid option.
+ *
+ * @param gridEnabled Whether to enable grid option. If enabled, the tile size will be
+ * automatically chosen. Default is to enable.
+ * @return this Builder object.
+ */
+ public Builder setGridEnabled(boolean gridEnabled) {
+ mGridEnabled = gridEnabled;
+ return this;
+ }
+
+ /**
+ * Set the quality for encoding images.
+ *
+ * @param quality A number between 0 and 100 (inclusive), with 100 indicating the best
+ * quality supported by this implementation. Default is 100.
+ * @return this Builder object.
+ */
+ public Builder setQuality(int quality) {
+ if (quality < 0 || quality > 100) {
+ throw new IllegalArgumentException("Invalid quality: " + quality);
+ }
+ mQuality = quality;
+ return this;
+ }
+
+ /**
+ * Set the maximum number of images to write.
+ *
+ * @param maxImages Max number of images to write. Frames exceeding this number will not be
+ * written to file. The writing can be stopped earlier before this number
+ * of images are written by {@link #stop(long)}, except for the input mode
+ * of {@link #INPUT_MODE_SURFACE}, where the EOS timestamp must be
+ * specified (via {@link #setInputEndOfStreamTimestamp(long)} and reached.
+ * Default is 1.
+ * @return this Builder object.
+ */
+ public Builder setMaxImages(int maxImages) {
+ if (maxImages <= 0) {
+ throw new IllegalArgumentException("Invalid maxImage: " + maxImages);
+ }
+ mMaxImages = maxImages;
+ return this;
+ }
+
+ /**
+ * Set the primary image index.
+ *
+ * @param primaryIndex Index of the image that should be marked as primary, must be within
+ * range [0, maxImages - 1] inclusive. Default is 0.
+ * @return this Builder object.
+ */
+ public Builder setPrimaryIndex(int primaryIndex) {
+ if (primaryIndex < 0) {
+ throw new IllegalArgumentException("Invalid primaryIndex: " + primaryIndex);
+ }
+ mPrimaryIndex = primaryIndex;
+ return this;
+ }
+
+ /**
+ * Provide a handler for the HeifWriter to use.
+ *
+ * @param handler If not null, client will receive all callbacks on the handler's looper.
+ * Otherwise, client will receive callbacks on a looper created by the
+ * writer. Default is null.
+ * @return this Builder object.
+ */
+ public Builder setHandler(@Nullable Handler handler) {
+ mHandler = handler;
+ return this;
+ }
+
+ /**
+ * Build a HeifWriter object.
+ *
+ * @return a HeifWriter object built according to the specifications.
+ * @throws IOException if failed to create the writer, possibly due to failure to create
+ * {@link android.media.MediaMuxer} or {@link android.media.MediaCodec}.
+ */
+ public HeifWriter build() throws IOException {
+ return new HeifWriter(mPath, mFd, mWidth, mHeight, mRotation, mGridEnabled, mQuality,
+ mMaxImages, mPrimaryIndex, mInputMode, mHandler);
+ }
}
- /**
- * Construct a heif writer that writes to a file specified by file descriptor.
- *
- * @param fd File descriptor of the file to be written.
- * @param width Width of the image.
- * @param height Height of the image.
- * @param useGrid Whether to encode image into tiles. If enabled, the tile size will be
- * automatically chosen.
- * @param quality A number between 0 and 100 (inclusive), with 100 indicating the best quality
- * supported by this implementation (which often results in larger file size).
- * @param numImages Max number of images to write. Frames exceeding this number will not be
- * written to file. The writing can be stopped earlier before this number of
- * images are written by {@link #stop(long)}, except for the input mode of
- * {@link #INPUT_MODE_SURFACE}, where the EOS timestamp must be specified (via
- * {@link #setInputEndOfStreamTimestamp(long)} and reached.
- * @param primaryIndex Index of the image that should be marked as primary, must be within range
- * [0, numImages - 1] inclusive.
- * @param inputMode Input mode for this writer, must be one of {@link #INPUT_MODE_BUFFER},
- * {@link #INPUT_MODE_SURFACE}, or {@link #INPUT_MODE_BITMAP}.
- * @param handler If not null, client will receive all callbacks on the handler's looper.
- * Otherwise, client will receive callbacks on a looper created by the writer.
- *
- * @throws IOException if failed to construct MediaMuxer or HeifEncoder.
- */
@SuppressLint("WrongConstant")
- public HeifWriter(@NonNull FileDescriptor fd,
- int width, int height, boolean useGrid,
- int quality, int numImages, int primaryIndex,
- @InputMode int inputMode,
- @Nullable Handler handler) throws IOException {
- this(width, height, useGrid, quality, numImages, primaryIndex, inputMode, handler,
- new MediaMuxer(fd, MUXER_OUTPUT_HEIF));
- }
-
- private HeifWriter(int width, int height, boolean useGrid,
- int quality, int numImages, int primaryIndex,
+ private HeifWriter(@NonNull String path,
+ @NonNull FileDescriptor fd,
+ int width,
+ int height,
+ int rotation,
+ boolean gridEnabled,
+ int quality,
+ int maxImages,
+ int primaryIndex,
@InputMode int inputMode,
- @Nullable Handler handler,
- @NonNull MediaMuxer muxer) throws IOException {
- if (numImages <= 0 || primaryIndex < 0 || primaryIndex >= numImages) {
+ @Nullable Handler handler) throws IOException {
+ if (primaryIndex >= maxImages) {
throw new IllegalArgumentException(
- "Invalid numImages (" + numImages + ") or primaryIndex (" + primaryIndex + ")");
+ "Invalid maxImages (" + maxImages + ") or primaryIndex (" + primaryIndex + ")");
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "width: " + width
+ + ", height: " + height
+ + ", rotation: " + rotation
+ + ", gridEnabled: " + gridEnabled
+ + ", quality: " + quality
+ + ", maxImages: " + maxImages
+ + ", primaryIndex: " + primaryIndex
+ + ", inputMode: " + inputMode);
}
MediaFormat format = MediaFormat.createVideoFormat(
MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC, width, height);
- if (DEBUG) {
- Log.d(TAG, "format: " + format + ", inputMode: " + inputMode +
- ", numImage: " + numImages + ", primaryIndex: " + primaryIndex);
- }
-
// set to 1 initially, and wait for output format to know for sure
mNumTiles = 1;
+ mRotation = rotation;
mInputMode = inputMode;
- mNumImages = numImages;
+ mMaxImages = maxImages;
mPrimaryIndex = primaryIndex;
Looper looper = (handler != null) ? handler.getLooper() : null;
@@ -222,9 +328,10 @@
}
mHandler = new Handler(looper);
- mMuxer = muxer;
+ mMuxer = (path != null) ? new MediaMuxer(path, MUXER_OUTPUT_HEIF)
+ : new MediaMuxer(fd, MUXER_OUTPUT_HEIF);
- mHeifEncoder = new HeifEncoder(width, height, useGrid, quality,
+ mHeifEncoder = new HeifEncoder(width, height, gridEnabled, quality,
mInputMode, mHandler, new HeifCallback());
}
@@ -400,19 +507,23 @@
try {
int gridRows = format.getInteger(MediaFormat.KEY_GRID_ROWS);
- int gridCols = format.getInteger(MediaFormat.KEY_GRID_COLS);
+ int gridCols = format.getInteger(MediaFormat.KEY_GRID_COLUMNS);
mNumTiles = gridRows * gridCols;
} catch (NullPointerException | ClassCastException e) {
mNumTiles = 1;
}
- // add mNumImages image tracks of the same format
- mTrackIndexArray = new int[mNumImages];
+ // add mMaxImages image tracks of the same format
+ mTrackIndexArray = new int[mMaxImages];
+
+ // set rotation angle
+ if (mRotation > 0) {
+ Log.d(TAG, "setting rotation: " + mRotation);
+ mMuxer.setOrientationHint(mRotation);
+ }
for (int i = 0; i < mTrackIndexArray.length; i++) {
// mark primary
- if (i == mPrimaryIndex) {
- format.setInteger(MediaFormat.KEY_IS_DEFAULT, 1);
- }
+ format.setInteger(MediaFormat.KEY_IS_DEFAULT, (i == mPrimaryIndex) ? 1 : 0);
mTrackIndexArray[i] = mMuxer.addTrack(format);
}
mMuxer.start();
@@ -436,7 +547,7 @@
return;
}
- if (mOutputIndex < mNumImages * mNumTiles) {
+ if (mOutputIndex < mMaxImages * mNumTiles) {
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
info.set(byteBuffer.position(), byteBuffer.remaining(), 0, 0);
mMuxer.writeSampleData(
@@ -446,7 +557,7 @@
mOutputIndex++;
// post EOS if reached max number of images allowed.
- if (mOutputIndex == mNumImages * mNumTiles) {
+ if (mOutputIndex == mMaxImages * mNumTiles) {
stopAndNotify(null);
}
}
diff --git a/jetifier/jetifier/processor/src/main/kotlin/com/android/tools/build/jetifier/processor/transform/bytecode/CoreRemapperImpl.kt b/jetifier/jetifier/processor/src/main/kotlin/com/android/tools/build/jetifier/processor/transform/bytecode/CoreRemapperImpl.kt
index 7019d78..e3766b6 100644
--- a/jetifier/jetifier/processor/src/main/kotlin/com/android/tools/build/jetifier/processor/transform/bytecode/CoreRemapperImpl.kt
+++ b/jetifier/jetifier/processor/src/main/kotlin/com/android/tools/build/jetifier/processor/transform/bytecode/CoreRemapperImpl.kt
@@ -66,11 +66,11 @@
return value
}
- val result = context.config.typesMap.mapType(type)
- if (result != null) {
- changesDone = changesDone || result != type
- Log.i(TAG, "Map string: '%s' -> '%s'", type, result)
- return result.toDotNotation()
+ val mappedType = context.config.typesMap.mapType(type)
+ if (mappedType != null) {
+ changesDone = changesDone || mappedType != type
+ Log.i(TAG, "Map string: '%s' -> '%s'", type, mappedType)
+ return mappedType.toDotNotation()
}
// We might be working with an internal type or field reference, e.g.
@@ -86,10 +86,10 @@
// Try rewrite rules
if (context.useFallbackIfTypeIsMissing) {
- val result = context.config.rulesMap.rewriteType(type)
- if (result != null) {
- Log.i(TAG, "Map string: '%s' -> '%s' via fallback", value, result)
- return result.toDotNotation()
+ val rewrittenType = context.config.rulesMap.rewriteType(type)
+ if (rewrittenType != null) {
+ Log.i(TAG, "Map string: '%s' -> '%s' via fallback", value, rewrittenType)
+ return rewrittenType.toDotNotation()
}
}
diff --git a/jetifier/jetifier/source-transformer/rewritePackageNames.py b/jetifier/jetifier/source-transformer/rewritePackageNames.py
index 16c99cc..2d82b86 100755
--- a/jetifier/jetifier/source-transformer/rewritePackageNames.py
+++ b/jetifier/jetifier/source-transformer/rewritePackageNames.py
@@ -95,7 +95,7 @@
rewriterTextBuilder = StringBuilder()
rewriteRules = executionConfig.jetifierConfig.getTypesMap()
for rule in rewriteRules:
- rewriterTextBuilder.add("s|").add(rule.fromName).add("|").add(rule.toName).add("|g\n")
+ rewriterTextBuilder.add("s|").add(rule.fromName.replace(".", "\.")).add("|").add(rule.toName).add("|g\n")
for rule in HARDCODED_RULES_REVERSE:
rewriterTextBuilder.add(rule)
scriptPath = "/tmp/jetifier-sed-script.txt"
diff --git a/leanback/src/main/res/values-as/strings.xml b/leanback/src/main/res/values-as/strings.xml
new file mode 100644
index 0000000..eb3b2b0
--- /dev/null
+++ b/leanback/src/main/res/values-as/strings.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="lb_navigation_menu_contentDescription" msgid="8126335323963415494">"নেভিগেশ্বন মেনু"</string>
+ <string name="orb_search_action" msgid="7534843523462177008">"সন্ধান সম্পৰ্কীয় কাৰ্য"</string>
+ <string name="lb_search_bar_hint" msgid="4819380969103509861">"সন্ধান"</string>
+ <string name="lb_search_bar_hint_speech" msgid="2795474673510974502">"সন্ধান কৰিবলৈ কথা কওক"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="7453744869467668159">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> সন্ধান কৰক"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="5851694095153624617">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> সন্ধান কৰিবলৈ কথা কওক"</string>
+ <string name="lb_control_display_fast_forward_multiplier" msgid="2721825378927619928">"%1$dX"</string>
+ <string name="lb_control_display_rewind_multiplier" msgid="6173753802428649303">"%1$dX"</string>
+ <string name="lb_playback_controls_play" msgid="1590369760862605402">"প্লে কৰক"</string>
+ <string name="lb_playback_controls_pause" msgid="1769131316742618433">"পজ কৰক"</string>
+ <string name="lb_playback_controls_fast_forward" msgid="8966769845721269304">"ফাষ্ট ফৰৱাৰ্ড"</string>
+ <string name="lb_playback_controls_fast_forward_multiplier" msgid="801276177839339511">"ফাষ্ট ফৰৱার্ড কৰক %1$dX"</string>
+ <string name="lb_playback_controls_rewind" msgid="1412664391757869774">"ৰিৱাইণ্ড কৰক"</string>
+ <string name="lb_playback_controls_rewind_multiplier" msgid="8651612807713092781">"ৰিৱাইণ্ড কৰক %1$dX"</string>
+ <string name="lb_playback_controls_skip_next" msgid="4877009494447817003">"পৰৱৰ্তীটোলৈ এৰি যাওক"</string>
+ <string name="lb_playback_controls_skip_previous" msgid="3147124289285911980">"আগৰটোলৈ এৰি যাওক"</string>
+ <string name="lb_playback_controls_more_actions" msgid="2827883329510404797">"অধিক কাৰ্য"</string>
+ <string name="lb_playback_controls_thumb_up" msgid="8332816524260995892">"থাম্ব আপ বাছনি নাইকিয়া কৰক"</string>
+ <string name="lb_playback_controls_thumb_up_outline" msgid="1038344559734334272">"থাম্ব আপ বাছনি কৰক"</string>
+ <string name="lb_playback_controls_thumb_down" msgid="5075744418630733006">"থাম্ব ডাউন বাছনি নাইকিয়া কৰক"</string>
+ <string name="lb_playback_controls_thumb_down_outline" msgid="2847309435442474470">"থাম্ব ডাউন বাছনি কৰক"</string>
+ <string name="lb_playback_controls_repeat_none" msgid="5812341701962930499">"একো পুনৰাই প্লে নকৰিব"</string>
+ <string name="lb_playback_controls_repeat_all" msgid="5164826436271322261">"সকলো পুনৰাই প্লে কৰক"</string>
+ <string name="lb_playback_controls_repeat_one" msgid="7675097479246139440">"এটা পুনৰাই প্লে কৰক"</string>
+ <string name="lb_playback_controls_shuffle_enable" msgid="7809089255981448519">"সান-মিহলি সক্ষম কৰক"</string>
+ <string name="lb_playback_controls_shuffle_disable" msgid="8182435535948303910">"সান-মিহলি অক্ষম কৰক"</string>
+ <string name="lb_playback_controls_high_quality_enable" msgid="1862669142355962638">"উচ্চ মানৰ প্লেবেক সক্ষম কৰক"</string>
+ <string name="lb_playback_controls_high_quality_disable" msgid="3000046054608531995">"উচ্চ মান অক্ষম কৰক"</string>
+ <string name="lb_playback_controls_closed_captioning_enable" msgid="3934392140182327163">"ছাব-টাইটেল সক্ষম কৰক"</string>
+ <string name="lb_playback_controls_closed_captioning_disable" msgid="5508271941331836786">"ছাব-টাইটেল অক্ষম কৰক"</string>
+ <string name="lb_playback_controls_picture_in_picture" msgid="8800305194045609275">"চিত্ৰৰ ভিতৰত চিত্ৰ ম\'ড আৰম্ভ কৰক"</string>
+ <string name="lb_playback_time_separator" msgid="6549544638083578695">"/"</string>
+ <string name="lb_playback_controls_shown" msgid="7794717158616536936">"মিডিয়াৰ নিয়ন্ত্ৰণসমূহ দেখুওৱা হ\'ল"</string>
+ <string name="lb_playback_controls_hidden" msgid="619396299825306757">"মিডিয়াৰ নিয়ন্ত্ৰণসমূহ লুকুৱাই ৰখা হৈছে, দেখুওৱাবলৈ ডি-পেডত টিপক"</string>
+ <string name="lb_guidedaction_finish_title" msgid="7747913934287176843">"সমাপ্ত"</string>
+ <string name="lb_guidedaction_continue_title" msgid="1122271825827282965">"অব্যাহত ৰাখক"</string>
+ <string name="lb_media_player_error" msgid="8748646000835486516">"MediaPlayer ত্ৰুটি ক\'ড %1$d, অতিৰিক্ত %2$d"</string>
+ <string name="lb_onboarding_get_started" msgid="7674487829030291492">"আৰম্ভ কৰক"</string>
+ <string name="lb_onboarding_accessibility_next" msgid="4213611627196077555">"পৰৱৰ্তী"</string>
+</resources>
diff --git a/leanback/src/main/res/values-be/strings.xml b/leanback/src/main/res/values-be/strings.xml
index 2828004..a4038d6 100644
--- a/leanback/src/main/res/values-be/strings.xml
+++ b/leanback/src/main/res/values-be/strings.xml
@@ -21,8 +21,8 @@
<string name="orb_search_action" msgid="7534843523462177008">"Пошук"</string>
<string name="lb_search_bar_hint" msgid="4819380969103509861">"Пошук"</string>
<string name="lb_search_bar_hint_speech" msgid="2795474673510974502">"Прамоўце пошукавы запыт"</string>
- <string name="lb_search_bar_hint_with_title" msgid="7453744869467668159">"Шукаць тут <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
- <string name="lb_search_bar_hint_with_title_speech" msgid="5851694095153624617">"Прамоўце пошукавы запыт тут <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="7453744869467668159">"Шукаць тут: <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="5851694095153624617">"Прамоўце запыт для пошуку тут: <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
<string name="lb_control_display_fast_forward_multiplier" msgid="2721825378927619928">"%1$dX"</string>
<string name="lb_control_display_rewind_multiplier" msgid="6173753802428649303">"%1$dX"</string>
<string name="lb_playback_controls_play" msgid="1590369760862605402">"Прайграць"</string>
@@ -53,7 +53,7 @@
<string name="lb_playback_controls_hidden" msgid="619396299825306757">"Элементы кіравання мультымедыя схаваны. Каб паказаць іх, націсніце d-pad"</string>
<string name="lb_guidedaction_finish_title" msgid="7747913934287176843">"Завяршыць"</string>
<string name="lb_guidedaction_continue_title" msgid="1122271825827282965">"Працягнуць"</string>
- <string name="lb_media_player_error" msgid="8748646000835486516">"Код памылкі MediaPlayer %1$d дадаткова %2$d"</string>
+ <string name="lb_media_player_error" msgid="8748646000835486516">"Код памылкі MediaPlayer: %1$d (дадатковы: %2$d)"</string>
<string name="lb_onboarding_get_started" msgid="7674487829030291492">"ПАЧАЦЬ"</string>
<string name="lb_onboarding_accessibility_next" msgid="4213611627196077555">"Далей"</string>
</resources>
diff --git a/leanback/src/main/res/values-es/strings.xml b/leanback/src/main/res/values-es/strings.xml
index dc33a6b..53e6797 100644
--- a/leanback/src/main/res/values-es/strings.xml
+++ b/leanback/src/main/res/values-es/strings.xml
@@ -22,7 +22,7 @@
<string name="lb_search_bar_hint" msgid="4819380969103509861">"Haz una búsqueda"</string>
<string name="lb_search_bar_hint_speech" msgid="2795474673510974502">"Habla para buscar"</string>
<string name="lb_search_bar_hint_with_title" msgid="7453744869467668159">"Busca <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
- <string name="lb_search_bar_hint_with_title_speech" msgid="5851694095153624617">"Busca <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> por voz"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="5851694095153624617">"Habla para buscar <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
<string name="lb_control_display_fast_forward_multiplier" msgid="2721825378927619928">"%1$dx"</string>
<string name="lb_control_display_rewind_multiplier" msgid="6173753802428649303">"%1$dx"</string>
<string name="lb_playback_controls_play" msgid="1590369760862605402">"Reproducir"</string>
@@ -47,7 +47,7 @@
<string name="lb_playback_controls_high_quality_disable" msgid="3000046054608531995">"Inhabilitar alta calidad"</string>
<string name="lb_playback_controls_closed_captioning_enable" msgid="3934392140182327163">"Habilitar subtítulos"</string>
<string name="lb_playback_controls_closed_captioning_disable" msgid="5508271941331836786">"Inhabilitar subtítulos"</string>
- <string name="lb_playback_controls_picture_in_picture" msgid="8800305194045609275">"Activar modo pantalla en pantalla"</string>
+ <string name="lb_playback_controls_picture_in_picture" msgid="8800305194045609275">"Activar modo imagen en imagen"</string>
<string name="lb_playback_time_separator" msgid="6549544638083578695">"/"</string>
<string name="lb_playback_controls_shown" msgid="7794717158616536936">"Controles multimedia mostrados"</string>
<string name="lb_playback_controls_hidden" msgid="619396299825306757">"Controles multimedia ocultos (pulsa la cruceta para mostrarlos)"</string>
diff --git a/leanback/src/main/res/values-eu/strings.xml b/leanback/src/main/res/values-eu/strings.xml
index 6b7226f..72c433f 100644
--- a/leanback/src/main/res/values-eu/strings.xml
+++ b/leanback/src/main/res/values-eu/strings.xml
@@ -49,8 +49,8 @@
<string name="lb_playback_controls_closed_captioning_disable" msgid="5508271941331836786">"Desgaitu azpitituluak"</string>
<string name="lb_playback_controls_picture_in_picture" msgid="8800305194045609275">"Aktibatu \"Pantaila txiki gainjarri\" modua"</string>
<string name="lb_playback_time_separator" msgid="6549544638083578695">"/"</string>
- <string name="lb_playback_controls_shown" msgid="7794717158616536936">"Multimedia kontrolatzeko aukerak ikusgai"</string>
- <string name="lb_playback_controls_hidden" msgid="619396299825306757">"Ezkutatuta daude multimedia-edukia kontrolatzeko aukerak. Erakusteko, sakatu nabigazio-gurutzea."</string>
+ <string name="lb_playback_controls_shown" msgid="7794717158616536936">"Multimedia-edukia kontrolatzeko aukerak ikusgai"</string>
+ <string name="lb_playback_controls_hidden" msgid="619396299825306757">"Ezkutatuta daude multimedia-edukia kontrolatzeko aukerak. Haiek erakusteko, sakatu nabigazio-gurutzea."</string>
<string name="lb_guidedaction_finish_title" msgid="7747913934287176843">"Amaitu"</string>
<string name="lb_guidedaction_continue_title" msgid="1122271825827282965">"Egin aurrera"</string>
<string name="lb_media_player_error" msgid="8748646000835486516">"MediaPlayer errore-kodea: %1$d (%2$d gehigarria)"</string>
diff --git a/leanback/src/main/res/values-fr-rCA/strings.xml b/leanback/src/main/res/values-fr-rCA/strings.xml
index de3fde5..ad38e94 100644
--- a/leanback/src/main/res/values-fr-rCA/strings.xml
+++ b/leanback/src/main/res/values-fr-rCA/strings.xml
@@ -32,7 +32,7 @@
<string name="lb_playback_controls_rewind" msgid="1412664391757869774">"Retour arrière"</string>
<string name="lb_playback_controls_rewind_multiplier" msgid="8651612807713092781">"Retour rapide à %1$dX"</string>
<string name="lb_playback_controls_skip_next" msgid="4877009494447817003">"Passer à l\'élément suivant"</string>
- <string name="lb_playback_controls_skip_previous" msgid="3147124289285911980">"Passer à l\'élément précédent"</string>
+ <string name="lb_playback_controls_skip_previous" msgid="3147124289285911980">"Retourner à l\'élément précédent"</string>
<string name="lb_playback_controls_more_actions" msgid="2827883329510404797">"Autres actions"</string>
<string name="lb_playback_controls_thumb_up" msgid="8332816524260995892">"Désélectionner la mention « J\'aime »"</string>
<string name="lb_playback_controls_thumb_up_outline" msgid="1038344559734334272">"Sélectionner la mention « J\'aime »"</string>
diff --git a/leanback/src/main/res/values-ja/strings.xml b/leanback/src/main/res/values-ja/strings.xml
index 22980f1..96f27e2 100644
--- a/leanback/src/main/res/values-ja/strings.xml
+++ b/leanback/src/main/res/values-ja/strings.xml
@@ -43,8 +43,8 @@
<string name="lb_playback_controls_repeat_one" msgid="7675097479246139440">"1 曲をリピート"</string>
<string name="lb_playback_controls_shuffle_enable" msgid="7809089255981448519">"シャッフルを有効にする"</string>
<string name="lb_playback_controls_shuffle_disable" msgid="8182435535948303910">"シャッフルを無効にする"</string>
- <string name="lb_playback_controls_high_quality_enable" msgid="1862669142355962638">"高品質を有効にする"</string>
- <string name="lb_playback_controls_high_quality_disable" msgid="3000046054608531995">"高品質を無効にする"</string>
+ <string name="lb_playback_controls_high_quality_enable" msgid="1862669142355962638">"高画質を有効にする"</string>
+ <string name="lb_playback_controls_high_quality_disable" msgid="3000046054608531995">"高画質を無効にする"</string>
<string name="lb_playback_controls_closed_captioning_enable" msgid="3934392140182327163">"クローズド キャプションを有効にする"</string>
<string name="lb_playback_controls_closed_captioning_disable" msgid="5508271941331836786">"クローズド キャプションを無効にする"</string>
<string name="lb_playback_controls_picture_in_picture" msgid="8800305194045609275">"ピクチャー イン ピクチャー モードに移動"</string>
diff --git a/leanback/src/main/res/values-ko/strings.xml b/leanback/src/main/res/values-ko/strings.xml
index 9eacb53..b1587b9 100644
--- a/leanback/src/main/res/values-ko/strings.xml
+++ b/leanback/src/main/res/values-ko/strings.xml
@@ -40,7 +40,7 @@
<string name="lb_playback_controls_thumb_down_outline" msgid="2847309435442474470">"싫어요 선택"</string>
<string name="lb_playback_controls_repeat_none" msgid="5812341701962930499">"반복 안함"</string>
<string name="lb_playback_controls_repeat_all" msgid="5164826436271322261">"전체 반복"</string>
- <string name="lb_playback_controls_repeat_one" msgid="7675097479246139440">"한 항목 반복"</string>
+ <string name="lb_playback_controls_repeat_one" msgid="7675097479246139440">"한 개 반복"</string>
<string name="lb_playback_controls_shuffle_enable" msgid="7809089255981448519">"셔플 사용 설정"</string>
<string name="lb_playback_controls_shuffle_disable" msgid="8182435535948303910">"셔플 사용 중지"</string>
<string name="lb_playback_controls_high_quality_enable" msgid="1862669142355962638">"고품질 사용 설정"</string>
diff --git a/leanback/src/main/res/values-mr/strings.xml b/leanback/src/main/res/values-mr/strings.xml
index b01e273..07b4e14 100644
--- a/leanback/src/main/res/values-mr/strings.xml
+++ b/leanback/src/main/res/values-mr/strings.xml
@@ -29,7 +29,7 @@
<!-- String.format failed for translation -->
<!-- no translation found for lb_control_display_rewind_multiplier (6173753802428649303) -->
<skip />
- <string name="lb_playback_controls_play" msgid="1590369760862605402">"खेळा"</string>
+ <string name="lb_playback_controls_play" msgid="1590369760862605402">"प्ले"</string>
<string name="lb_playback_controls_pause" msgid="1769131316742618433">"विराम द्या"</string>
<string name="lb_playback_controls_fast_forward" msgid="8966769845721269304">"पुढे ढकला"</string>
<string name="lb_playback_controls_fast_forward_multiplier" msgid="801276177839339511">"फास्ट फॉरवर्ड %1$d"</string>
@@ -38,10 +38,10 @@
<string name="lb_playback_controls_skip_next" msgid="4877009494447817003">"पुढील वगळा"</string>
<string name="lb_playback_controls_skip_previous" msgid="3147124289285911980">"मागील वगळा"</string>
<string name="lb_playback_controls_more_actions" msgid="2827883329510404797">"आणखी क्रिया"</string>
- <string name="lb_playback_controls_thumb_up" msgid="8332816524260995892">"वर अंगठा निवड रद्द करा"</string>
- <string name="lb_playback_controls_thumb_up_outline" msgid="1038344559734334272">"वर अंगठा निवडा"</string>
- <string name="lb_playback_controls_thumb_down" msgid="5075744418630733006">"खाली अंगठा निवड रद्द करा"</string>
- <string name="lb_playback_controls_thumb_down_outline" msgid="2847309435442474470">"खाली अंगठा निवडा"</string>
+ <string name="lb_playback_controls_thumb_up" msgid="8332816524260995892">"थंब अप निवड रद्द करा"</string>
+ <string name="lb_playback_controls_thumb_up_outline" msgid="1038344559734334272">"थंब अप निवडा"</string>
+ <string name="lb_playback_controls_thumb_down" msgid="5075744418630733006">"थंब डाउन निवड रद्द करा"</string>
+ <string name="lb_playback_controls_thumb_down_outline" msgid="2847309435442474470">"थंब डाउन निवडा"</string>
<string name="lb_playback_controls_repeat_none" msgid="5812341701962930499">"काहीही रिपीट करू नका"</string>
<string name="lb_playback_controls_repeat_all" msgid="5164826436271322261">"सर्व रिपीट करा"</string>
<string name="lb_playback_controls_repeat_one" msgid="7675097479246139440">"एक रिपीट करा"</string>
diff --git a/leanback/src/main/res/values-or/strings.xml b/leanback/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000..ce6d1ea
--- /dev/null
+++ b/leanback/src/main/res/values-or/strings.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="lb_navigation_menu_contentDescription" msgid="8126335323963415494">"ନେଭିଗେଶନ୍ ମେନୁ"</string>
+ <string name="orb_search_action" msgid="7534843523462177008">"ଖୋଜିବା କାମ"</string>
+ <string name="lb_search_bar_hint" msgid="4819380969103509861">"ସର୍ଚ୍ଚ କରନ୍ତୁ"</string>
+ <string name="lb_search_bar_hint_speech" msgid="2795474673510974502">"ଖୋଜିବା ପାଇଁ କୁହନ୍ତୁ"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="7453744869467668159">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> ଖୋଜନ୍ତୁ"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="5851694095153624617">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> ଖୋଜିବା ପାଇଁ କୁହନ୍ତୁ"</string>
+ <string name="lb_control_display_fast_forward_multiplier" msgid="2721825378927619928">"%1$dX"</string>
+ <string name="lb_control_display_rewind_multiplier" msgid="6173753802428649303">"%1$dX"</string>
+ <string name="lb_playback_controls_play" msgid="1590369760862605402">"ଚଲାନ୍ତୁ"</string>
+ <string name="lb_playback_controls_pause" msgid="1769131316742618433">"ପଜ୍ କରନ୍ତୁ"</string>
+ <string name="lb_playback_controls_fast_forward" msgid="8966769845721269304">"ଫାଷ୍ଟ ଫର୍ୱାର୍ଡ"</string>
+ <string name="lb_playback_controls_fast_forward_multiplier" msgid="801276177839339511">"%1$dX ବେଗରେ ଫାଷ୍ଟ ଫରୱାର୍ଡ"</string>
+ <string name="lb_playback_controls_rewind" msgid="1412664391757869774">"ରିୱାଇଣ୍ଡ"</string>
+ <string name="lb_playback_controls_rewind_multiplier" msgid="8651612807713092781">"%1$dX ବେଗରେ ରିୱାଇଣ୍ଡ କରନ୍ତୁ"</string>
+ <string name="lb_playback_controls_skip_next" msgid="4877009494447817003">"ପରବର୍ତ୍ତୀକୁ ଯାଆନ୍ତୁ"</string>
+ <string name="lb_playback_controls_skip_previous" msgid="3147124289285911980">"ପୂର୍ବଟିକୁ ଛାଡ଼ିଦିଅନ୍ତୁ"</string>
+ <string name="lb_playback_controls_more_actions" msgid="2827883329510404797">"ଅଧିକ ଗତିବିଧି"</string>
+ <string name="lb_playback_controls_thumb_up" msgid="8332816524260995892">"ପସନ୍ଦକୁ ଚୟନ କରନ୍ତୁ ନାହିଁ"</string>
+ <string name="lb_playback_controls_thumb_up_outline" msgid="1038344559734334272">"ପସନ୍ଦକୁ ଚୟନ କରନ୍ତୁ"</string>
+ <string name="lb_playback_controls_thumb_down" msgid="5075744418630733006">"ପସନ୍ଦହୀନକୁ ଚୟନ କରନ୍ତୁ ନାହିଁ"</string>
+ <string name="lb_playback_controls_thumb_down_outline" msgid="2847309435442474470">"ପସନ୍ଦହୀନକୁ ଚୟନ କରନ୍ତୁ"</string>
+ <string name="lb_playback_controls_repeat_none" msgid="5812341701962930499">"କୌଣସିଟି ଦୋହରାନ୍ତୁ ନାହିଁ"</string>
+ <string name="lb_playback_controls_repeat_all" msgid="5164826436271322261">"ସବୁଗୁଡ଼ିକୁ ଦୋହରାନ୍ତୁ"</string>
+ <string name="lb_playback_controls_repeat_one" msgid="7675097479246139440">"ଗୋଟିଏ ଦୋହରାନ୍ତୁ"</string>
+ <string name="lb_playback_controls_shuffle_enable" msgid="7809089255981448519">"ଅଦଳବଦଳକୁ ସକ୍ଷମ କରନ୍ତୁ"</string>
+ <string name="lb_playback_controls_shuffle_disable" msgid="8182435535948303910">"ଅଦଳବଦଳକୁ ଅକ୍ଷମ କରନ୍ତୁ"</string>
+ <string name="lb_playback_controls_high_quality_enable" msgid="1862669142355962638">"ଉଚ୍ଚ କ୍ୱାଲିଟୀକୁ ସକ୍ଷମ କରନ୍ତୁ"</string>
+ <string name="lb_playback_controls_high_quality_disable" msgid="3000046054608531995">"ଉଚ୍ଚ କ୍ୱାଲିଟୀକୁ ଅକ୍ଷମ କରନ୍ତୁ"</string>
+ <string name="lb_playback_controls_closed_captioning_enable" msgid="3934392140182327163">"କ୍ଲୋଜଡ୍ କ୍ୟାପ୍ସନିଙ୍ଗକୁ ସକ୍ଷମ କରନ୍ତୁ"</string>
+ <string name="lb_playback_controls_closed_captioning_disable" msgid="5508271941331836786">"କ୍ଲୋଜଡ୍ କ୍ୟାପ୍ସନିଙ୍ଗକୁ ଅକ୍ଷମ କରନ୍ତୁ"</string>
+ <string name="lb_playback_controls_picture_in_picture" msgid="8800305194045609275">"ଛବି ଭିତରେ ଛବି ମୋଡ୍ରେ ପ୍ରବେଶ କରନ୍ତୁ"</string>
+ <string name="lb_playback_time_separator" msgid="6549544638083578695">"/"</string>
+ <string name="lb_playback_controls_shown" msgid="7794717158616536936">"ମିଡିଆ ନିୟନ୍ତ୍ରଣ ଦେଖାଯାଇଛି"</string>
+ <string name="lb_playback_controls_hidden" msgid="619396299825306757">"ମିଡିଆ ନିୟନ୍ତ୍ରଣ ଲୁଚିଯାଇଛି, ଦେଖାଇବାକୁ ଡି-ପ୍ୟାଡ୍ ଦବାନ୍ତୁ"</string>
+ <string name="lb_guidedaction_finish_title" msgid="7747913934287176843">"ସମାପ୍ତ କରନ୍ତୁ"</string>
+ <string name="lb_guidedaction_continue_title" msgid="1122271825827282965">"ଜାରି ରଖନ୍ତୁ"</string>
+ <string name="lb_media_player_error" msgid="8748646000835486516">"MediaPlayer ତ୍ରୁଟି କୋଡ୍ %1$d ଅତିରିକ୍ତ %2$d"</string>
+ <string name="lb_onboarding_get_started" msgid="7674487829030291492">"ଆରମ୍ଭ କରନ୍ତୁ"</string>
+ <string name="lb_onboarding_accessibility_next" msgid="4213611627196077555">"ପରବର୍ତ୍ତୀ"</string>
+</resources>
diff --git a/leanback/src/main/res/values-uz/strings.xml b/leanback/src/main/res/values-uz/strings.xml
index 50a86af..95e0b67 100644
--- a/leanback/src/main/res/values-uz/strings.xml
+++ b/leanback/src/main/res/values-uz/strings.xml
@@ -21,8 +21,8 @@
<string name="orb_search_action" msgid="7534843523462177008">"Qidiruv amali"</string>
<string name="lb_search_bar_hint" msgid="4819380969103509861">"Qidiruv"</string>
<string name="lb_search_bar_hint_speech" msgid="2795474673510974502">"Qidirish uchun gapiring"</string>
- <string name="lb_search_bar_hint_with_title" msgid="7453744869467668159">"Qidirish: <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
- <string name="lb_search_bar_hint_with_title_speech" msgid="5851694095153624617">"Qidirish uchun ayting: <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="7453744869467668159">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> ichidan qidirish"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="5851694095153624617">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> ichidan qidirish uchun gapiring"</string>
<string name="lb_control_display_fast_forward_multiplier" msgid="2721825378927619928">"%1$dX"</string>
<string name="lb_control_display_rewind_multiplier" msgid="6173753802428649303">"%1$dX"</string>
<string name="lb_playback_controls_play" msgid="1590369760862605402">"Ijro"</string>
@@ -39,14 +39,14 @@
<string name="lb_playback_controls_thumb_down" msgid="5075744418630733006">"Salbiy baho tanlovini bekor qilish"</string>
<string name="lb_playback_controls_thumb_down_outline" msgid="2847309435442474470">"Salbiy bahoni tanlash"</string>
<string name="lb_playback_controls_repeat_none" msgid="5812341701962930499">"Takrorlamaslik"</string>
- <string name="lb_playback_controls_repeat_all" msgid="5164826436271322261">"Barchasini takrorlash"</string>
+ <string name="lb_playback_controls_repeat_all" msgid="5164826436271322261">"Hammasini takrorlash"</string>
<string name="lb_playback_controls_repeat_one" msgid="7675097479246139440">"Bir marta takrorlash"</string>
<string name="lb_playback_controls_shuffle_enable" msgid="7809089255981448519">"Aralashtirish funksiyasini yoqish"</string>
- <string name="lb_playback_controls_shuffle_disable" msgid="8182435535948303910">"Aralashtirish funksiyasini o‘chirish"</string>
+ <string name="lb_playback_controls_shuffle_disable" msgid="8182435535948303910">"Aralashtirmaslik"</string>
<string name="lb_playback_controls_high_quality_enable" msgid="1862669142355962638">"Yuqori sifatni yoqish"</string>
- <string name="lb_playback_controls_high_quality_disable" msgid="3000046054608531995">"Yuqori sifatni o‘chirib qo‘yish"</string>
+ <string name="lb_playback_controls_high_quality_disable" msgid="3000046054608531995">"Yuqori sifatda ijro qilmaslik"</string>
<string name="lb_playback_controls_closed_captioning_enable" msgid="3934392140182327163">"Taglavhalarni yoqish"</string>
- <string name="lb_playback_controls_closed_captioning_disable" msgid="5508271941331836786">"Taglavhalarni o‘chirib qo‘yish"</string>
+ <string name="lb_playback_controls_closed_captioning_disable" msgid="5508271941331836786">"Taglavhalarni chiqarmaslik"</string>
<string name="lb_playback_controls_picture_in_picture" msgid="8800305194045609275">"Tasvir ustida tasvir rejimiga kirish"</string>
<string name="lb_playback_time_separator" msgid="6549544638083578695">"/"</string>
<string name="lb_playback_controls_shown" msgid="7794717158616536936">"Boshqaruv elementlari ochiq"</string>
diff --git a/leanback/src/main/res/values-zh-rTW/strings.xml b/leanback/src/main/res/values-zh-rTW/strings.xml
index bcaf11e..9aea4d2 100644
--- a/leanback/src/main/res/values-zh-rTW/strings.xml
+++ b/leanback/src/main/res/values-zh-rTW/strings.xml
@@ -34,10 +34,10 @@
<string name="lb_playback_controls_skip_next" msgid="4877009494447817003">"跳至下一個項目"</string>
<string name="lb_playback_controls_skip_previous" msgid="3147124289285911980">"跳至上一個項目"</string>
<string name="lb_playback_controls_more_actions" msgid="2827883329510404797">"更多動作"</string>
- <string name="lb_playback_controls_thumb_up" msgid="8332816524260995892">"取消選取喜歡"</string>
- <string name="lb_playback_controls_thumb_up_outline" msgid="1038344559734334272">"選取喜歡"</string>
- <string name="lb_playback_controls_thumb_down" msgid="5075744418630733006">"取消選取不喜歡"</string>
- <string name="lb_playback_controls_thumb_down_outline" msgid="2847309435442474470">"選取不喜歡"</string>
+ <string name="lb_playback_controls_thumb_up" msgid="8332816524260995892">"取消選取「喜歡」"</string>
+ <string name="lb_playback_controls_thumb_up_outline" msgid="1038344559734334272">"選取「喜歡」"</string>
+ <string name="lb_playback_controls_thumb_down" msgid="5075744418630733006">"取消選取「不喜歡」"</string>
+ <string name="lb_playback_controls_thumb_down_outline" msgid="2847309435442474470">"選取「不喜歡」"</string>
<string name="lb_playback_controls_repeat_none" msgid="5812341701962930499">"不重複播放"</string>
<string name="lb_playback_controls_repeat_all" msgid="5164826436271322261">"重複播放所有項目"</string>
<string name="lb_playback_controls_repeat_one" msgid="7675097479246139440">"重複播放單一項目"</string>
diff --git a/lifecycle/reactivestreams/build.gradle b/lifecycle/reactivestreams/build.gradle
index 34390b7..649c526 100644
--- a/lifecycle/reactivestreams/build.gradle
+++ b/lifecycle/reactivestreams/build.gradle
@@ -35,9 +35,7 @@
testImplementation(JUNIT)
testImplementation(RX_JAVA)
- testImplementation(TEST_RUNNER)
-
- androidTestImplementation(SUPPORT_APPCOMPAT, libs.support_exclude_config)
+ testImplementation(project(":arch:core-testing"))
}
supportLibrary {
@@ -48,4 +46,4 @@
inceptionYear = "2017"
description = "Android Lifecycle Reactivestreams"
url = SupportLibraryExtension.ARCHITECTURE_URL
-}
\ No newline at end of file
+}
diff --git a/lifecycle/reactivestreams/ktx/OWNERS b/lifecycle/reactivestreams/ktx/OWNERS
new file mode 100644
index 0000000..e450f4c
--- /dev/null
+++ b/lifecycle/reactivestreams/ktx/OWNERS
@@ -0,0 +1 @@
+jakew@google.com
diff --git a/lifecycle/reactivestreams/ktx/build.gradle b/lifecycle/reactivestreams/ktx/build.gradle
new file mode 100644
index 0000000..2604a8e
--- /dev/null
+++ b/lifecycle/reactivestreams/ktx/build.gradle
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.SupportLibraryExtension
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ api(project(":lifecycle:lifecycle-reactivestreams"))
+ api(KOTLIN_STDLIB)
+
+ testImplementation(JUNIT)
+ testImplementation(RX_JAVA)
+ testImplementation(TRUTH)
+ testImplementation(project(":arch:core-testing"))
+}
+
+supportLibrary {
+ name = "Android Lifecycle ReactiveStreams KTX"
+ publish = true
+ mavenVersion = LibraryVersions.LIFECYCLES_EXT
+ mavenGroup = LibraryGroups.LIFECYCLE
+ inceptionYear = "2018"
+ description = "Kotlin extensions for Lifecycle ReactiveStreams"
+ url = SupportLibraryExtension.ARCHITECTURE_URL
+}
diff --git a/lifecycle/reactivestreams/ktx/src/main/AndroidManifest.xml b/lifecycle/reactivestreams/ktx/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..d2c150c
--- /dev/null
+++ b/lifecycle/reactivestreams/ktx/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<!--
+ ~ Copyright (C) 2018 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest package="androidx.lifecycle.reactivestreams.ktx"/>
diff --git a/lifecycle/reactivestreams/ktx/src/main/java/androidx/lifecycle/LiveDataReactiveSteams.kt b/lifecycle/reactivestreams/ktx/src/main/java/androidx/lifecycle/LiveDataReactiveSteams.kt
new file mode 100644
index 0000000..e4a95e7
--- /dev/null
+++ b/lifecycle/reactivestreams/ktx/src/main/java/androidx/lifecycle/LiveDataReactiveSteams.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.lifecycle
+
+import org.reactivestreams.Publisher
+
+/**
+ * Adapts the given [LiveData] stream to a ReactiveStreams [Publisher].
+ *
+ * @see LiveDataReactiveStreams.toPublisher
+ */
+inline fun <T> LiveData<T>.toPublisher(lifecycle: LifecycleOwner): Publisher<T> =
+ LiveDataReactiveStreams.toPublisher(lifecycle, this)
+
+/**
+ * Creates an observable [LiveData] stream from a ReactiveStreams [Publisher].
+ *
+ * @see LiveDataReactiveStreams.fromPublisher
+ */
+inline fun <T> Publisher<T>.toLiveData(): LiveData<T> =
+ LiveDataReactiveStreams.fromPublisher(this)
diff --git a/lifecycle/reactivestreams/ktx/src/test/java/androidx/lifecycle/LiveDataReactiveStreamsTest.kt b/lifecycle/reactivestreams/ktx/src/test/java/androidx/lifecycle/LiveDataReactiveStreamsTest.kt
new file mode 100644
index 0000000..ad3bdb2
--- /dev/null
+++ b/lifecycle/reactivestreams/ktx/src/test/java/androidx/lifecycle/LiveDataReactiveStreamsTest.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.lifecycle
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.google.common.truth.Truth.assertThat
+import io.reactivex.processors.PublishProcessor
+import io.reactivex.processors.ReplayProcessor
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class LiveDataReactiveStreamsTest {
+ @get:Rule val rule = InstantTaskExecutorRule()
+
+ private lateinit var lifecycleOwner: LifecycleOwner
+
+ @Before fun init() {
+ lifecycleOwner = object : LifecycleOwner {
+ internal var registry = LifecycleRegistry(this)
+
+ init {
+ registry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
+ }
+
+ override fun getLifecycle(): Lifecycle {
+ return registry
+ }
+ }
+ }
+
+ @Test fun convertsFromPublisher() {
+ val processor = PublishProcessor.create<String>()
+ val liveData = processor.toLiveData()
+
+ val output = mutableListOf<String?>()
+ liveData.observe(lifecycleOwner, Observer { output.add(it) })
+
+ processor.onNext("foo")
+ processor.onNext("bar")
+ processor.onNext("baz")
+
+ assertThat(output).containsExactly("foo", "bar", "baz")
+ }
+
+ @Test fun convertsToPublisherWithSyncData() {
+ val liveData = MutableLiveData<String>()
+ liveData.value = "foo"
+
+ val outputProcessor = ReplayProcessor.create<String>()
+ liveData.toPublisher(lifecycleOwner).subscribe(outputProcessor)
+
+ liveData.value = "bar"
+ liveData.value = "baz"
+
+ assertThat(outputProcessor.values).asList().containsExactly("foo", "bar", "baz")
+ }
+}
diff --git a/lifecycle/reactivestreams/src/androidTest/AndroidManifest.xml b/lifecycle/reactivestreams/src/androidTest/AndroidManifest.xml
deleted file mode 100644
index 304fd93..0000000
--- a/lifecycle/reactivestreams/src/androidTest/AndroidManifest.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2017 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="androidx.lifecycle.reactivestreams.test">
- <uses-sdk android:targetSdkVersion="${target-sdk-version}"/>
-
- <application>
- <activity android:name="androidx.lifecycle.viewmodeltest.ViewModelActivity"
- android:theme="@style/Base.Theme.AppCompat">
- </activity>
- </application>
-
-</manifest>
diff --git a/lifecycle/reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.java b/lifecycle/reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.java
index 4af5a09..711e71e 100644
--- a/lifecycle/reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.java
+++ b/lifecycle/reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.java
@@ -165,7 +165,7 @@
}
/**
- * Creates an Observable {@link LiveData} stream from a ReactiveStreams publisher.
+ * Creates an observable {@link LiveData} stream from a ReactiveStreams {@link Publisher}}.
*
* <p>
* When the LiveData becomes active, it subscribes to the emissions from the Publisher.
diff --git a/lifecycle/reactivestreams/src/test/java/androidx/lifecycle/LiveDataReactiveStreamsTest.java b/lifecycle/reactivestreams/src/test/java/androidx/lifecycle/LiveDataReactiveStreamsTest.java
index 6311d94..ed792d2 100644
--- a/lifecycle/reactivestreams/src/test/java/androidx/lifecycle/LiveDataReactiveStreamsTest.java
+++ b/lifecycle/reactivestreams/src/test/java/androidx/lifecycle/LiveDataReactiveStreamsTest.java
@@ -21,15 +21,13 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
-import android.support.test.filters.SmallTest;
-
import androidx.annotation.Nullable;
-import androidx.arch.core.executor.ArchTaskExecutor;
-import androidx.arch.core.executor.TaskExecutor;
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
-import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TestRule;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
@@ -47,8 +45,9 @@
import io.reactivex.schedulers.TestScheduler;
import io.reactivex.subjects.AsyncSubject;
-@SmallTest
public class LiveDataReactiveStreamsTest {
+ @Rule public final TestRule instantTaskExecutorRule = new InstantTaskExecutorRule();
+
private LifecycleOwner mLifecycleOwner;
private final List<String> mLiveDataOutput = new ArrayList<>();
@@ -62,7 +61,6 @@
private final ReplayProcessor<String> mOutputProcessor = ReplayProcessor.create();
private static final TestScheduler sBackgroundScheduler = new TestScheduler();
- private Thread mTestThread;
@Before
public void init() {
@@ -77,31 +75,6 @@
return mRegistry;
}
};
- mTestThread = Thread.currentThread();
- ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
-
- @Override
- public void executeOnDiskIO(Runnable runnable) {
- throw new IllegalStateException();
- }
-
- @Override
- public void postToMainThread(Runnable runnable) {
- // Wrong implementation, but it is fine for test
- runnable.run();
- }
-
- @Override
- public boolean isMainThread() {
- return Thread.currentThread() == mTestThread;
- }
-
- });
- }
-
- @After
- public void removeExecutorDelegate() {
- ArchTaskExecutor.getInstance().setDelegate(null);
}
@Test
diff --git a/lifecycle/viewmodel/ktx/build.gradle b/lifecycle/viewmodel/ktx/build.gradle
new file mode 100644
index 0000000..a963911
--- /dev/null
+++ b/lifecycle/viewmodel/ktx/build.gradle
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ buildTypes {
+ debug {
+ testCoverageEnabled = false // Breaks Kotlin compiler.
+ }
+ }
+}
+
+dependencies {
+ api(project(":lifecycle:lifecycle-viewmodel"))
+ api(KOTLIN_STDLIB)
+
+ testImplementation(JUNIT)
+ testImplementation(TEST_RUNNER)
+}
+
+supportLibrary {
+ name = "Android Lifecycle ViewModel Kotlin Extensions"
+ publish = true
+ mavenVersion = LibraryVersions.LIFECYCLES_VIEWMODEL
+ mavenGroup = LibraryGroups.LIFECYCLE
+ inceptionYear = "2018"
+ description = "Kotlin extensions for 'viewmodel' artifact"
+}
diff --git a/lifecycle/viewmodel/ktx/src/main/AndroidManifest.xml b/lifecycle/viewmodel/ktx/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..75b1d20
--- /dev/null
+++ b/lifecycle/viewmodel/ktx/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest package="androidx.lifecycle.viewmodel.ktx"/>
diff --git a/lifecycle/viewmodel/ktx/src/main/java/androidx/lifecycle/ViewModelProvider.kt b/lifecycle/viewmodel/ktx/src/main/java/androidx/lifecycle/ViewModelProvider.kt
new file mode 100644
index 0000000..bc42e1b
--- /dev/null
+++ b/lifecycle/viewmodel/ktx/src/main/java/androidx/lifecycle/ViewModelProvider.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.lifecycle
+
+import androidx.annotation.MainThread
+
+/**
+ * Returns an existing ViewModel or creates a new one in the scope (usually, a fragment or
+ * an activity), associated with this `ViewModelProvider`.
+ *
+ * @see ViewModelProvider.get(Class)
+ */
+@MainThread
+inline fun <reified VM : ViewModel> ViewModelProvider.get() = get(VM::class.java)
diff --git a/lifecycle/viewmodel/ktx/src/test/java/androidx/lifecycle/ViewModelProviderTest.kt b/lifecycle/viewmodel/ktx/src/test/java/androidx/lifecycle/ViewModelProviderTest.kt
new file mode 100644
index 0000000..e8e76ce
--- /dev/null
+++ b/lifecycle/viewmodel/ktx/src/test/java/androidx/lifecycle/ViewModelProviderTest.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.lifecycle
+
+import android.support.test.filters.SmallTest
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+@SmallTest
+class ViewModelProviderTest {
+ class TestViewModel : ViewModel()
+
+ @Test
+ fun providerReifiedGet() {
+ val factory = object : ViewModelProvider.Factory {
+ override fun <T : ViewModel> create(modelClass: Class<T>) = modelClass.newInstance()
+ }
+ val provider = ViewModelProvider(ViewModelStore(), factory)
+
+ val viewModel = provider.get<TestViewModel>()
+ assertNotNull(viewModel)
+ }
+}
diff --git a/media-widget/api/current.txt b/media-widget/api/current.txt
new file mode 100644
index 0000000..d81423b
--- /dev/null
+++ b/media-widget/api/current.txt
@@ -0,0 +1,39 @@
+package androidx.media.widget {
+
+ public class MediaControlView2 extends android.view.ViewGroup {
+ ctor public MediaControlView2(android.content.Context);
+ ctor public MediaControlView2(android.content.Context, android.util.AttributeSet);
+ ctor public MediaControlView2(android.content.Context, android.util.AttributeSet, int);
+ method public void onMeasure(int, int);
+ method public void requestPlayButtonFocus();
+ method public void setOnFullScreenListener(androidx.media.widget.MediaControlView2.OnFullScreenListener);
+ }
+
+ public static abstract interface MediaControlView2.OnFullScreenListener {
+ method public abstract void onFullScreen(android.view.View, boolean);
+ }
+
+ public class VideoView2 extends android.view.ViewGroup {
+ ctor public VideoView2(android.content.Context);
+ ctor public VideoView2(android.content.Context, android.util.AttributeSet);
+ ctor public VideoView2(android.content.Context, android.util.AttributeSet, int);
+ method public androidx.media.widget.MediaControlView2 getMediaControlView2();
+ method public float getSpeed();
+ method public int getViewType();
+ method public boolean isSubtitleEnabled();
+ method public void onAttachedToWindow();
+ method public void onDetachedFromWindow();
+ method public void onMeasure(int, int);
+ method public void setAudioAttributes(android.media.AudioAttributes);
+ method public void setAudioFocusRequest(int);
+ method public void setMediaControlView2(androidx.media.widget.MediaControlView2, long);
+ method public void setSpeed(float);
+ method public void setSubtitleEnabled(boolean);
+ method public void setVideoUri(android.net.Uri, java.util.Map<java.lang.String, java.lang.String>);
+ method public void setViewType(int);
+ field public static final int VIEW_TYPE_SURFACEVIEW = 0; // 0x0
+ field public static final int VIEW_TYPE_TEXTUREVIEW = 1; // 0x1
+ }
+
+}
+
diff --git a/media-widget/build.gradle b/media-widget/build.gradle
index de3f9d9..e6ff65d 100644
--- a/media-widget/build.gradle
+++ b/media-widget/build.gradle
@@ -37,10 +37,13 @@
}
supportLibrary {
- name = "Android Media Support Library"
+ name = "Android Support Media Widget"
publish = true
mavenVersion = LibraryVersions.SUPPORT_LIBRARY
mavenGroup = LibraryGroups.MEDIA
inceptionYear = "2011"
- description = "Android Media Support Library"
+ description = "Android Support Media Widget"
+ minSdkVersion = 19
+ failOnDeprecationWarnings = false
+ failOnDeprecationWarnings = false
}
diff --git a/media-widget/src/androidTest/AndroidManifest.xml b/media-widget/src/androidTest/AndroidManifest.xml
index dd6e9e0..5304a48 100644
--- a/media-widget/src/androidTest/AndroidManifest.xml
+++ b/media-widget/src/androidTest/AndroidManifest.xml
@@ -1,25 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright 2018 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
+ Copyright 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="androidx.media.widget.test">
<uses-sdk android:targetSdkVersion="${target-sdk-version}"/>
<application>
<activity android:name="androidx.media.widget.VideoView2TestActivity"
+ android:theme="@style/Theme.AppCompat"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="VideoView2TestActivity">
<intent-filter>
diff --git a/media-widget/src/androidTest/java/androidx/media/widget/VideoView2Test.java b/media-widget/src/androidTest/java/androidx/media/widget/VideoView2Test.java
index 2876e47..da38bd2 100644
--- a/media-widget/src/androidTest/java/androidx/media/widget/VideoView2Test.java
+++ b/media-widget/src/androidTest/java/androidx/media/widget/VideoView2Test.java
@@ -61,7 +61,7 @@
/**
* Test {@link VideoView2}.
*/
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) // TODO: KITKAT
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.P) // TODO: KITKAT
@LargeTest
@RunWith(AndroidJUnit4.class)
public class VideoView2Test {
@@ -107,10 +107,17 @@
@Override
public void run() {
// Keep screen on while testing.
- mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
- mActivity.setTurnScreenOn(true);
- mActivity.setShowWhenLocked(true);
- mKeyguardManager.requestDismissKeyguard(mActivity, null);
+ if (Build.VERSION.SDK_INT >= 27) {
+ mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ mActivity.setTurnScreenOn(true);
+ mActivity.setShowWhenLocked(true);
+ mKeyguardManager.requestDismissKeyguard(mActivity, null);
+ } else {
+ mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+ | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+ | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+ | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
+ }
}
});
mInstrumentation.waitForIdleSync();
diff --git a/media-widget/src/androidTest/java/androidx/media/widget/VideoView2TestActivity.java b/media-widget/src/androidTest/java/androidx/media/widget/VideoView2TestActivity.java
index d6a3ebc..912d336 100644
--- a/media-widget/src/androidTest/java/androidx/media/widget/VideoView2TestActivity.java
+++ b/media-widget/src/androidTest/java/androidx/media/widget/VideoView2TestActivity.java
@@ -16,15 +16,15 @@
package androidx.media.widget;
-import android.app.Activity;
import android.os.Bundle;
+import androidx.fragment.app.FragmentActivity;
import androidx.media.widget.test.R;
/**
* A minimal application for {@link VideoView2} test.
*/
-public class VideoView2TestActivity extends Activity {
+public class VideoView2TestActivity extends FragmentActivity {
/**
* Called with the activity is first created.
*/
diff --git a/media-widget/src/main/java/androidx/media/widget/BaseLayout.java b/media-widget/src/main/java/androidx/media/widget/BaseLayout.java
index 0b6988a..982513a 100644
--- a/media-widget/src/main/java/androidx/media/widget/BaseLayout.java
+++ b/media-widget/src/main/java/androidx/media/widget/BaseLayout.java
@@ -39,7 +39,7 @@
}
BaseLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
- this(context, attrs, 0);
+ super(context, attrs);
}
BaseLayout(@NonNull Context context, @Nullable AttributeSet attrs,
diff --git a/media-widget/src/main/java/androidx/media/widget/MediaControlView2.java b/media-widget/src/main/java/androidx/media/widget/MediaControlView2.java
index 89fa946..a8371f2 100644
--- a/media-widget/src/main/java/androidx/media/widget/MediaControlView2.java
+++ b/media-widget/src/main/java/androidx/media/widget/MediaControlView2.java
@@ -27,6 +27,7 @@
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.AttributeSet;
import android.view.Gravity;
+import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
@@ -51,6 +52,8 @@
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.media.SessionToken2;
+import androidx.mediarouter.app.MediaRouteButton;
+import androidx.mediarouter.media.MediaRouteSelector;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -60,41 +63,34 @@
import java.util.List;
import java.util.Locale;
-// import androidx.mediarouter.app.MediaRouteButton;
-// import androidx.mediarouter.media.MediaRouter;
-// import androidx.mediarouter.media.MediaRouteSelector;
-
/**
- * @hide
- * A View that contains the controls for MediaPlayer2.
- * It provides a wide range of UI including buttons such as "Play/Pause", "Rewind", "Fast Forward",
- * "Subtitle", "Full Screen", and it is also possible to add multiple custom buttons.
+ * A View that contains the controls for {@link android.media.MediaPlayer}.
+ * It provides a wide range of buttons that serve the following functions: play/pause,
+ * rewind/fast-forward, skip to next/previous, select subtitle track, enter/exit full screen mode,
+ * adjust video quality, select audio track, mute/unmute, and adjust playback speed.
*
* <p>
* <em> MediaControlView2 can be initialized in two different ways: </em>
- * 1) When VideoView2 is initialized, it automatically initializes a MediaControlView2 instance and
- * adds it to the view.
- * 2) Initialize MediaControlView2 programmatically and add it to a ViewGroup instance.
+ * 1) When initializing {@link VideoView2} a default MediaControlView2 is created.
+ * 2) Initialize MediaControlView2 programmatically and add it to a {@link ViewGroup} instance.
*
* In the first option, VideoView2 automatically connects MediaControlView2 to MediaController,
- * which is necessary to communicate with MediaSession2. In the second option, however, the
- * developer needs to manually retrieve a MediaController instance and set it to MediaControlView2
- * by calling setController(MediaController controller).
+ * which is necessary to communicate with MediaSession. In the second option, however, the
+ * developer needs to manually retrieve a MediaController instance from MediaSession and set it to
+ * MediaControlView2.
*
* <p>
* There is no separate method that handles the show/hide behavior for MediaControlView2. Instead,
- * one can directly change the visibility of this view by calling View.setVisibility(int). The
- * values supported are View.VISIBLE and View.GONE.
- * In addition, the following customization is supported:
- * Set focus to the play/pause button by calling requestPlayButtonFocus().
+ * one can directly change the visibility of this view by calling {@link View#setVisibility(int)}.
+ * The values supported are View.VISIBLE and View.GONE.
*
* <p>
- * It is also possible to add custom buttons with custom icons and actions inside MediaControlView2.
- * Those buttons will be shown when the overflow button is clicked.
- * See VideoView2#setCustomActions for more details on how to add.
+ * In addition, the following customizations are supported:
+ * 1) Set focus to the play/pause button by calling requestPlayButtonFocus().
+ * 2) Set full screen mode
+ *
*/
@RequiresApi(21) // TODO correct minSdk API use incompatibilities and remove before release.
-@RestrictTo(LIBRARY_GROUP)
public class MediaControlView2 extends BaseLayout {
/**
* @hide
@@ -185,10 +181,6 @@
private static final String TAG = "MediaControlView2";
- static final String ARGUMENT_KEY_FULLSCREEN = "fullScreen";
-
- // TODO: Make these constants public api to support custom video view.
- // TODO: Combine these constants into one regarding TrackInfo.
static final String KEY_VIDEO_TRACK_COUNT = "VideoTrackCount";
static final String KEY_AUDIO_TRACK_COUNT = "AudioTrackCount";
static final String KEY_SUBTITLE_TRACK_COUNT = "SubtitleTrackCount";
@@ -196,8 +188,6 @@
static final String KEY_SELECTED_AUDIO_INDEX = "SelectedAudioIndex";
static final String KEY_SELECTED_SUBTITLE_INDEX = "SelectedSubtitleIndex";
static final String EVENT_UPDATE_TRACK_STATUS = "UpdateTrackStatus";
-
- // TODO: Remove this once integrating with MediaSession2 & MediaMetadata2
static final String KEY_STATE_IS_ADVERTISEMENT = "MediaTypeAdvertisement";
static final String EVENT_UPDATE_MEDIA_TYPE_STATUS = "UpdateMediaTypeStatus";
@@ -205,8 +195,6 @@
static final String COMMAND_SHOW_SUBTITLE = "showSubtitle";
// String for sending command to hide subtitle to MediaSession.
static final String COMMAND_HIDE_SUBTITLE = "hideSubtitle";
- // TODO: remove once the implementation is revised
- public static final String COMMAND_SET_FULLSCREEN = "setFullscreen";
// String for sending command to select audio track to MediaSession.
static final String COMMAND_SELECT_AUDIO_TRACK = "SelectTrack";
// String for sending command to set playback speed to MediaSession.
@@ -230,7 +218,6 @@
private static final int SIZE_TYPE_EMBEDDED = 0;
private static final int SIZE_TYPE_FULL = 1;
- // TODO: add support for Minimal size type.
private static final int SIZE_TYPE_MINIMAL = 2;
private static final int MAX_PROGRESS = 1000;
@@ -246,6 +233,7 @@
private MediaControllerCompat.TransportControls mControls;
private PlaybackStateCompat mPlaybackState;
private MediaMetadataCompat mMetadata;
+ private OnFullScreenListener mOnFullScreenListener;
private int mDuration;
private int mPrevState;
private int mPrevWidth;
@@ -261,8 +249,7 @@
private int mSelectedSpeedIndex;
private int mEmbeddedSettingsItemWidth;
private int mFullSettingsItemWidth;
- private int mEmbeddedSettingsItemHeight;
- private int mFullSettingsItemHeight;
+ private int mSettingsItemHeight;
private int mSettingsWindowMargin;
private int mMediaType;
private int mSizeType;
@@ -284,9 +271,8 @@
private TextView mTitleView;
private View mAdExternalLink;
private ImageButton mBackButton;
- // TODO (b/77158231) revive
- // private MediaRouteButton mRouteButton;
- // private MediaRouteSelector mRouteSelector;
+ private MediaRouteButton mRouteButton;
+ private MediaRouteSelector mRouteSelector;
// Relating to Center View
private ViewGroup mCenterView;
@@ -354,20 +340,9 @@
public MediaControlView2(@NonNull Context context, @Nullable AttributeSet attrs,
int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
- }
+ super(context, attrs, defStyleAttr);
- public MediaControlView2(@NonNull Context context, @Nullable AttributeSet attrs,
- int defStyleAttr, int defStyleRes) {
-// super((instance, superProvider, privateProvider) ->
-// ApiLoader.getProvider().createMediaControlView2(
-// (MediaControlView2) instance, superProvider, privateProvider,
-// attrs, defStyleAttr, defStyleRes),
-// context, attrs, defStyleAttr, defStyleRes);
-// mProvider.initialize(attrs, defStyleAttr, defStyleRes);
- super(context, attrs, defStyleAttr, defStyleRes);
-
- mResources = getContext().getResources();
+ mResources = context.getResources();
// Inflate MediaControlView2 from XML
mRoot = makeControllerView();
addView(mRoot);
@@ -375,9 +350,10 @@
/**
* Sets MediaSession2 token to control corresponding MediaSession2.
+ * @hide
*/
+ @RestrictTo(LIBRARY_GROUP)
public void setMediaSessionToken(SessionToken2 token) {
- //mProvider.setMediaSessionToken_impl(token);
}
/**
@@ -385,17 +361,20 @@
* @param l The callback that will be run
*/
public void setOnFullScreenListener(OnFullScreenListener l) {
- //mProvider.setOnFullScreenListener_impl(l);
+ mOnFullScreenListener = l;
}
/**
+ * Sets MediaController instance to MediaControlView2, which makes it possible to send and
+ * receive data between MediaControlView2 and VideoView2. This method does not need to be called
+ * when MediaControlView2 is initialized with VideoView2.
* @hide TODO: remove once the implementation is revised
*/
@RestrictTo(LIBRARY_GROUP)
public void setController(MediaControllerCompat controller) {
mController = controller;
if (controller != null) {
- mControls = controller.getTransportControls();
+ mControls = mController.getTransportControls();
// Set mMetadata and mPlaybackState to existing MediaSession variables since they may
// be called before the callback is called
mPlaybackState = mController.getPlaybackState();
@@ -429,8 +408,6 @@
*/
@RestrictTo(LIBRARY_GROUP)
public void setButtonVisibility(@Button int button, /*@Visibility*/ int visibility) {
- // TODO: add member variables for Fast-Forward/Prvious/Rewind buttons to save visibility in
- // order to prevent being overriden inside updateLayout().
switch (button) {
case MediaControlView2.BUTTON_PLAY_PAUSE:
if (mPlayPauseButton != null && canPause()) {
@@ -517,7 +494,6 @@
return false;
}
- // TODO: Should this function be removed?
@Override
public boolean onTrackballEvent(MotionEvent ev) {
return false;
@@ -546,17 +522,13 @@
manager.getDefaultDisplay().getSize(screenSize);
int screenWidth = screenSize.x;
int screenHeight = screenSize.y;
- int fullIconSize = mResources.getDimensionPixelSize(R.dimen.mcv2_full_icon_size);
- int embeddedIconSize = mResources.getDimensionPixelSize(
- R.dimen.mcv2_embedded_icon_size);
- int marginSize = mResources.getDimensionPixelSize(R.dimen.mcv2_icon_margin);
+ int iconSize = mResources.getDimensionPixelSize(R.dimen.mcv2_icon_size);
- // TODO: add support for Advertisement Mode.
if (mMediaType == MEDIA_TYPE_DEFAULT) {
// Max number of icons inside BottomBarRightView for Music mode is 4.
int maxIconCount = 4;
- updateLayout(maxIconCount, fullIconSize, embeddedIconSize, marginSize, currWidth,
- currHeight, screenWidth, screenHeight);
+ updateLayout(maxIconCount, iconSize, currWidth, currHeight, screenWidth,
+ screenHeight);
} else if (mMediaType == MEDIA_TYPE_MUSIC) {
if (mNeedUXUpdate) {
@@ -575,13 +547,12 @@
// Max number of icons inside BottomBarRightView for Music mode is 3.
int maxIconCount = 3;
- updateLayout(maxIconCount, fullIconSize, embeddedIconSize, marginSize, currWidth,
- currHeight, screenWidth, screenHeight);
+ updateLayout(maxIconCount, iconSize, currWidth, currHeight, screenWidth,
+ screenHeight);
}
mPrevWidth = currWidth;
mPrevHeight = currHeight;
}
- // TODO: move this to a different location.
// Update title bar parameters in order to avoid overlap between title view and the right
// side of the title bar.
updateTitleBarLayout();
@@ -591,7 +562,6 @@
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
- // TODO: Merge the below code with disableUnsupportedButtons().
if (mPlayPauseButton != null) {
mPlayPauseButton.setEnabled(enabled);
}
@@ -626,19 +596,16 @@
}
}
- // TODO (b/77158231) revive once androidx.mediarouter.* packagaes are available.
- /*
void setRouteSelector(MediaRouteSelector selector) {
mRouteSelector = selector;
if (mRouteSelector != null && !mRouteSelector.isEmpty()) {
- mRouteButton.setRouteSelector(selector, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
+ mRouteButton.setRouteSelector(selector);
mRouteButton.setVisibility(View.VISIBLE);
} else {
mRouteButton.setRouteSelector(MediaRouteSelector.EMPTY);
mRouteButton.setVisibility(View.GONE);
}
}
- */
///////////////////////////////////////////////////
// Protected or private methods
@@ -698,14 +665,12 @@
*
* @return The controller view.
*/
- // TODO: This was "protected". Determine if it should be protected in MCV2.
private ViewGroup makeControllerView() {
ViewGroup root = (ViewGroup) inflateLayout(getContext(), R.layout.media_controller);
initControllerView(root);
return root;
}
- // TODO(b/76444971) make sure this is compatible with ApiHelper's one in updatable.
private View inflateLayout(Context context, int resId) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
@@ -723,8 +688,7 @@
mBackButton.setOnClickListener(mBackListener);
mBackButton.setVisibility(View.GONE);
}
- // TODO (b/77158231) revive
- // mRouteButton = v.findViewById(R.id.cast);
+ mRouteButton = v.findViewById(R.id.cast);
// Relating to Center View
mCenterView = v.findViewById(R.id.center_view);
@@ -735,7 +699,7 @@
mMinimalExtraView = (LinearLayout) v.findViewById(R.id.minimal_extra_view);
LinearLayout.LayoutParams params =
(LinearLayout.LayoutParams) mMinimalExtraView.getLayoutParams();
- int iconSize = mResources.getDimensionPixelSize(R.dimen.mcv2_embedded_icon_size);
+ int iconSize = mResources.getDimensionPixelSize(R.dimen.mcv2_icon_size);
int marginSize = mResources.getDimensionPixelSize(R.dimen.mcv2_icon_margin);
params.setMargins(0, (iconSize + marginSize * 2) * (-1), 0, 0);
mMinimalExtraView.setLayoutParams(params);
@@ -778,7 +742,6 @@
mFullScreenButton = v.findViewById(R.id.fullscreen);
if (mFullScreenButton != null) {
mFullScreenButton.setOnClickListener(mFullScreenListener);
- // TODO: Show Fullscreen button when only it is possible.
}
mOverflowButtonRight = v.findViewById(R.id.overflow_right);
if (mOverflowButtonRight != null) {
@@ -815,10 +778,8 @@
mEmbeddedSettingsItemWidth = mResources.getDimensionPixelSize(
R.dimen.mcv2_embedded_settings_width);
mFullSettingsItemWidth = mResources.getDimensionPixelSize(R.dimen.mcv2_full_settings_width);
- mEmbeddedSettingsItemHeight = mResources.getDimensionPixelSize(
- R.dimen.mcv2_embedded_settings_height);
- mFullSettingsItemHeight = mResources.getDimensionPixelSize(
- R.dimen.mcv2_full_settings_height);
+ mSettingsItemHeight = mResources.getDimensionPixelSize(
+ R.dimen.mcv2_settings_height);
mSettingsWindowMargin = (-1) * mResources.getDimensionPixelSize(
R.dimen.mcv2_settings_offset);
mSettingsWindow = new PopupWindow(mSettingsListView, mEmbeddedSettingsItemWidth,
@@ -840,13 +801,6 @@
if (mFfwdButton != null && !canSeekForward()) {
mFfwdButton.setEnabled(false);
}
- // TODO What we really should do is add a canSeek to the MediaPlayerControl interface;
- // this scheme can break the case when applications want to allow seek through the
- // progress bar but disable forward/backward buttons.
- //
- // However, currently the flags SEEK_BACKWARD_AVAILABLE, SEEK_FORWARD_AVAILABLE,
- // and SEEK_AVAILABLE are all (un)set together; as such the aforementioned issue
- // shouldn't arise in existing applications.
if (mProgress != null && !canSeekBackward() && !canSeekForward()) {
mProgress.setEnabled(false);
}
@@ -1078,7 +1032,11 @@
private final OnClickListener mBackListener = new OnClickListener() {
@Override
public void onClick(View v) {
- // TODO: implement
+ View parent = (View) getParent();
+ if (parent != null) {
+ parent.onKeyDown(KeyEvent.KEYCODE_BACK,
+ new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK));
+ }
}
};
@@ -1105,8 +1063,11 @@
private final OnClickListener mFullScreenListener = new OnClickListener() {
@Override
public void onClick(View v) {
+ if (mOnFullScreenListener == null) {
+ return;
+ }
+
final boolean isEnteringFullScreen = !mIsFullScreen;
- // TODO: Re-arrange the button layouts according to the UX.
if (isEnteringFullScreen) {
mFullScreenButton.setImageDrawable(
mResources.getDrawable(R.drawable.ic_fullscreen_exit, null));
@@ -1114,11 +1075,9 @@
mFullScreenButton.setImageDrawable(
mResources.getDrawable(R.drawable.ic_fullscreen, null));
}
- Bundle args = new Bundle();
- args.putBoolean(ARGUMENT_KEY_FULLSCREEN, isEnteringFullScreen);
- mController.sendCommand(COMMAND_SET_FULLSCREEN, args, null);
-
mIsFullScreen = isEnteringFullScreen;
+ mOnFullScreenListener.onFullScreen(MediaControlView2.this,
+ mIsFullScreen);
}
};
@@ -1183,7 +1142,6 @@
mSubSettingsAdapter.setCheckPosition(mSelectedSpeedIndex);
mSettingsMode = SETTINGS_MODE_PLAYBACK_SPEED;
} else if (position == SETTINGS_MODE_HELP) {
- // TODO: implement this.
mSettingsWindow.dismiss();
return;
}
@@ -1214,7 +1172,6 @@
mSettingsWindow.dismiss();
break;
case SETTINGS_MODE_HELP:
- // TODO: implement this.
break;
case SETTINGS_MODE_SUBTITLE_TRACK:
if (position != mSelectedSubtitleTrackIndex) {
@@ -1240,7 +1197,6 @@
mSettingsWindow.dismiss();
break;
case SETTINGS_MODE_VIDEO_QUALITY:
- // TODO: add support for video quality
mSelectedVideoQualityIndex = position;
mSettingsWindow.dismiss();
break;
@@ -1350,21 +1306,14 @@
}
}
- private void updateLayout(int maxIconCount, int fullIconSize, int embeddedIconSize,
- int marginSize, int currWidth, int currHeight, int screenWidth, int screenHeight) {
- int fullBottomBarRightWidthMax = fullIconSize * maxIconCount
- + marginSize * (maxIconCount * 2);
- int embeddedBottomBarRightWidthMax = embeddedIconSize * maxIconCount
- + marginSize * (maxIconCount * 2);
+ private void updateLayout(int maxIconCount, int iconSize, int currWidth,
+ int currHeight, int screenWidth, int screenHeight) {
+ int bottomBarRightWidthMax = iconSize * maxIconCount;
int fullWidth = mTransportControls.getWidth() + mTimeView.getWidth()
- + fullBottomBarRightWidthMax;
- int embeddedWidth = mTimeView.getWidth() + embeddedBottomBarRightWidthMax;
+ + bottomBarRightWidthMax;
+ int embeddedWidth = mTimeView.getWidth() + bottomBarRightWidthMax;
int screenMaxLength = Math.max(screenWidth, screenHeight);
- if (fullWidth > screenMaxLength) {
- // TODO: screen may be smaller than the length needed for Full size.
- }
-
boolean isFullSize = (mMediaType == MEDIA_TYPE_DEFAULT) ? (currWidth == screenMaxLength) :
(currWidth == screenWidth && currHeight == screenHeight);
@@ -1403,6 +1352,11 @@
// Relating to Title Bar
mTitleBar.setVisibility(View.VISIBLE);
mBackButton.setVisibility(View.GONE);
+ mTitleView.setPadding(
+ mResources.getDimensionPixelSize(R.dimen.mcv2_embedded_icon_padding),
+ mTitleView.getPaddingTop(),
+ mTitleView.getPaddingRight(),
+ mTitleView.getPaddingBottom());
// Relating to Full Screen Button
mMinimalExtraView.setVisibility(View.GONE);
@@ -1431,6 +1385,11 @@
// Relating to Title Bar
mTitleBar.setVisibility(View.VISIBLE);
mBackButton.setVisibility(View.VISIBLE);
+ mTitleView.setPadding(
+ 0,
+ mTitleView.getPaddingTop(),
+ mTitleView.getPaddingRight(),
+ mTitleView.getPaddingBottom());
// Relating to Full Screen Button
mMinimalExtraView.setVisibility(View.GONE);
@@ -1523,7 +1482,6 @@
mRewButton.setVisibility(View.GONE);
}
}
- // TODO: Add support for Next and Previous buttons
mNextButton = v.findViewById(R.id.next);
if (mNextButton != null) {
mNextButton.setOnClickListener(mNextListener);
@@ -1590,15 +1548,12 @@
mSettingsWindow.setWidth(itemWidth);
// Calculate height of window and show
- int itemHeight = (mSizeType == SIZE_TYPE_EMBEDDED)
- ? mEmbeddedSettingsItemHeight : mFullSettingsItemHeight;
- int totalHeight = adapter.getCount() * itemHeight;
+ int totalHeight = adapter.getCount() * mSettingsItemHeight;
mSettingsWindow.dismiss();
mSettingsWindow.showAsDropDown(this, mSettingsWindowMargin,
mSettingsWindowMargin - totalHeight, Gravity.BOTTOM | Gravity.RIGHT);
}
- @RequiresApi(26) // TODO correct minSdk API use incompatibilities and remove before release.
private class MediaControllerCallback extends MediaControllerCompat.Callback {
@Override
public void onPlaybackStateChanged(PlaybackStateCompat state) {
@@ -1670,16 +1625,12 @@
for (final PlaybackStateCompat.CustomAction action : customActions) {
ImageButton button = new ImageButton(getContext(),
null /* AttributeSet */, 0 /* Style */);
- // TODO: Apply R.style.BottomBarButton to this button using library context.
// Refer Constructor with argument (int defStyleRes) of View.java
button.setImageResource(action.getIcon());
- button.setTooltipText(action.getName());
final String actionString = action.getAction().toString();
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
- // TODO: Currently, we are just sending extras that came from session.
- // Is it the right behavior?
mControls.sendCustomAction(actionString, action.getExtras());
setVisibility(View.VISIBLE);
}
@@ -1708,7 +1659,6 @@
mAudioTrackCount = extras.getInt(KEY_AUDIO_TRACK_COUNT);
mAudioTrackList = new ArrayList<String>();
if (mAudioTrackCount > 0) {
- // TODO: add more text about track info.
for (int i = 0; i < mAudioTrackCount; i++) {
String track = mResources.getString(
R.string.MediaControlView2_audio_track_number_text, i + 1);
@@ -1787,14 +1737,12 @@
@Override
public long getItemId(int position) {
// Auto-generated method stub--does not have any purpose here
- // TODO: implement this.
return 0;
}
@Override
public Object getItem(int position) {
// Auto-generated method stub--does not have any purpose here
- // TODO: implement this.
return null;
}
@@ -1838,7 +1786,6 @@
}
}
- // TODO: extend this class from SettingsAdapter
private class SubSettingsAdapter extends BaseAdapter {
private List<String> mTexts;
private int mCheckPosition;
@@ -1865,14 +1812,12 @@
@Override
public long getItemId(int position) {
// Auto-generated method stub--does not have any purpose here
- // TODO: implement this.
return 0;
}
@Override
public Object getItem(int position) {
// Auto-generated method stub--does not have any purpose here
- // TODO: implement this.
return null;
}
diff --git a/media-widget/src/main/java/androidx/media/widget/RoutePlayer.java b/media-widget/src/main/java/androidx/media/widget/RoutePlayer.java
new file mode 100644
index 0000000..00ac36a
--- /dev/null
+++ b/media-widget/src/main/java/androidx/media/widget/RoutePlayer.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.widget;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.media.session.MediaSession;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v4.media.session.PlaybackStateCompat;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.mediarouter.media.MediaItemStatus;
+import androidx.mediarouter.media.MediaRouter;
+import androidx.mediarouter.media.MediaSessionStatus;
+import androidx.mediarouter.media.RemotePlaybackClient;
+import androidx.mediarouter.media.RemotePlaybackClient.ItemActionCallback;
+import androidx.mediarouter.media.RemotePlaybackClient.SessionActionCallback;
+import androidx.mediarouter.media.RemotePlaybackClient.StatusCallback;
+
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+public class RoutePlayer extends MediaSession.Callback {
+ public static final long PLAYBACK_ACTIONS = PlaybackStateCompat.ACTION_PAUSE
+ | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_SEEK_TO
+ | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND;
+
+ private RemotePlaybackClient mClient;
+ private String mSessionId;
+ private String mItemId;
+ private PlayerEventCallback mCallback;
+
+ private StatusCallback mStatusCallback = new StatusCallback() {
+ @Override
+ public void onItemStatusChanged(Bundle data,
+ String sessionId, MediaSessionStatus sessionStatus,
+ String itemId, MediaItemStatus itemStatus) {
+ updateSessionStatus(sessionId, sessionStatus);
+ updateItemStatus(itemId, itemStatus);
+ }
+ };
+
+ public RoutePlayer(Context context, MediaRouter.RouteInfo route) {
+ mClient = new RemotePlaybackClient(context, route);
+ mClient.setStatusCallback(mStatusCallback);
+ if (mClient.isSessionManagementSupported()) {
+ mClient.startSession(null, new SessionActionCallback() {
+ @Override
+ public void onResult(Bundle data,
+ String sessionId, MediaSessionStatus sessionStatus) {
+ updateSessionStatus(sessionId, sessionStatus);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onPlay() {
+ if (mClient.isSessionManagementSupported()) {
+ mClient.resume(null, new SessionActionCallback() {
+ @Override
+ public void onResult(Bundle data,
+ String sessionId, MediaSessionStatus sessionStatus) {
+ updateSessionStatus(sessionId, sessionStatus);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onPause() {
+ if (mClient.isSessionManagementSupported()) {
+ mClient.pause(null, new SessionActionCallback() {
+ @Override
+ public void onResult(Bundle data,
+ String sessionId, MediaSessionStatus sessionStatus) {
+ updateSessionStatus(sessionId, sessionStatus);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onSeekTo(long pos) {
+ if (mClient.isSessionManagementSupported()) {
+ mClient.seek(mItemId, pos, null, new ItemActionCallback() {
+ @Override
+ public void onResult(Bundle data,
+ String sessionId, MediaSessionStatus sessionStatus,
+ String itemId, MediaItemStatus itemStatus) {
+ updateSessionStatus(sessionId, sessionStatus);
+ updateItemStatus(itemId, itemStatus);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onStop() {
+ if (mClient.isSessionManagementSupported()) {
+ mClient.stop(null, new SessionActionCallback() {
+ @Override
+ public void onResult(Bundle data,
+ String sessionId, MediaSessionStatus sessionStatus) {
+ updateSessionStatus(sessionId, sessionStatus);
+ }
+ });
+ }
+ }
+
+ /**
+ * Sets a callback to be notified of events for this player.
+ * @param callback the callback to receive the events.
+ */
+ public void setPlayerEventCallback(PlayerEventCallback callback) {
+ mCallback = callback;
+ }
+
+ // b/77556429
+// public void openVideo(DataSourceDesc dsd) {
+// mClient.play(dsd.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() {
+// @Override
+// public void onResult(Bundle data,
+// String sessionId, MediaSessionStatus sessionStatus,
+// String itemId, MediaItemStatus itemStatus) {
+// updateSessionStatus(sessionId, sessionStatus);
+// updateItemStatus(itemId, itemStatus);
+// playInternal(dsd.getUri());
+// }
+// });
+// }
+
+ /**
+ * Opens the video based on the given uri and updates the media session and item statuses.
+ * @param uri link to the video
+ */
+ public void openVideo(Uri uri) {
+ mClient.play(uri, "video/mp4", null, 0, null, new ItemActionCallback() {
+ @Override
+ public void onResult(Bundle data,
+ String sessionId, MediaSessionStatus sessionStatus,
+ String itemId, MediaItemStatus itemStatus) {
+ updateSessionStatus(sessionId, sessionStatus);
+ updateItemStatus(itemId, itemStatus);
+ }
+ });
+ }
+
+ /**
+ * Releases the {@link RemotePlaybackClient} and {@link PlayerEventCallback} instances.
+ */
+ public void release() {
+ if (mClient != null) {
+ mClient.release();
+ mClient = null;
+ }
+ if (mCallback != null) {
+ mCallback = null;
+ }
+ }
+
+ private void playInternal(Uri uri) {
+ mClient.play(uri, "video/mp4", null, 0, null, new ItemActionCallback() {
+ @Override
+ public void onResult(Bundle data,
+ String sessionId, MediaSessionStatus sessionStatus,
+ String itemId, MediaItemStatus itemStatus) {
+ updateSessionStatus(sessionId, sessionStatus);
+ updateItemStatus(itemId, itemStatus);
+ }
+ });
+ }
+
+ private void updateSessionStatus(String sessionId, MediaSessionStatus sessionStatus) {
+ mSessionId = sessionId;
+ }
+
+ private void updateItemStatus(String itemId, MediaItemStatus itemStatus) {
+ mItemId = itemId;
+ if (itemStatus == null || mCallback == null) return;
+ mCallback.onPlayerStateChanged(itemStatus);
+ }
+
+ /**
+ * A callback class to receive notifications for events on the route player.
+ */
+ public abstract static class PlayerEventCallback {
+ /**
+ * Override to handle changes in playback state.
+ *
+ * @param itemStatus The new MediaItemStatus of the RoutePlayer
+ */
+ public void onPlayerStateChanged(MediaItemStatus itemStatus) { }
+ }
+}
diff --git a/media-widget/src/main/java/androidx/media/widget/SubtitleView.java b/media-widget/src/main/java/androidx/media/widget/SubtitleView.java
new file mode 100644
index 0000000..1faff2f
--- /dev/null
+++ b/media-widget/src/main/java/androidx/media/widget/SubtitleView.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.os.Looper;
+import android.util.AttributeSet;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.media.subtitle.SubtitleController.Anchor;
+import androidx.media.subtitle.SubtitleTrack.RenderingWidget;
+
+@RequiresApi(21)
+class SubtitleView extends BaseLayout implements Anchor {
+ private static final String TAG = "SubtitleView";
+
+ private RenderingWidget mSubtitleWidget;
+ private RenderingWidget.OnChangedListener mSubtitlesChangedListener;
+
+ SubtitleView(Context context) {
+ this(context, null);
+ }
+
+ SubtitleView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ SubtitleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ SubtitleView(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public void setSubtitleWidget(RenderingWidget subtitleWidget) {
+ if (mSubtitleWidget == subtitleWidget) {
+ return;
+ }
+
+ final boolean attachedToWindow = isAttachedToWindow();
+ if (mSubtitleWidget != null) {
+ if (attachedToWindow) {
+ mSubtitleWidget.onDetachedFromWindow();
+ }
+
+ mSubtitleWidget.setOnChangedListener(null);
+ }
+ mSubtitleWidget = subtitleWidget;
+
+ if (subtitleWidget != null) {
+ if (mSubtitlesChangedListener == null) {
+ mSubtitlesChangedListener = new RenderingWidget.OnChangedListener() {
+ @Override
+ public void onChanged(RenderingWidget renderingWidget) {
+ invalidate();
+ }
+ };
+ }
+
+ setWillNotDraw(false);
+ subtitleWidget.setOnChangedListener(mSubtitlesChangedListener);
+
+ if (attachedToWindow) {
+ subtitleWidget.onAttachedToWindow();
+ requestLayout();
+ }
+ } else {
+ setWillNotDraw(true);
+ }
+
+ invalidate();
+ }
+
+ @Override
+ public Looper getSubtitleLooper() {
+ return Looper.getMainLooper();
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ if (mSubtitleWidget != null) {
+ mSubtitleWidget.onAttachedToWindow();
+ }
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ if (mSubtitleWidget != null) {
+ mSubtitleWidget.onDetachedFromWindow();
+ }
+ }
+
+ @Override
+ public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ if (mSubtitleWidget != null) {
+ final int width = getWidth() - getPaddingLeft() - getPaddingRight();
+ final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+
+ mSubtitleWidget.setSize(width, height);
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+
+ if (mSubtitleWidget != null) {
+ final int saveCount = canvas.save();
+ canvas.translate(getPaddingLeft(), getPaddingTop());
+ mSubtitleWidget.draw(canvas);
+ canvas.restoreToCount(saveCount);
+ }
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return SubtitleView.class.getName();
+ }
+}
diff --git a/media-widget/src/main/java/androidx/media/widget/VideoView2.java b/media-widget/src/main/java/androidx/media/widget/VideoView2.java
index a8ea450..a851370 100644
--- a/media-widget/src/main/java/androidx/media/widget/VideoView2.java
+++ b/media-widget/src/main/java/androidx/media/widget/VideoView2.java
@@ -19,38 +19,17 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Point;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
import android.media.AudioAttributes;
-import android.media.AudioFocusRequest;
import android.media.AudioManager;
-import android.media.MediaMetadataRetriever;
import android.media.MediaPlayer;
-import android.media.PlaybackParams;
import android.net.Uri;
import android.os.Bundle;
-import android.os.ResultReceiver;
-import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaControllerCompat;
-import android.support.v4.media.session.MediaControllerCompat.PlaybackInfo;
-import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.AttributeSet;
-import android.util.DisplayMetrics;
import android.util.Log;
-import android.util.Pair;
-import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
-import android.view.WindowManager;
-import android.view.accessibility.AccessibilityManager;
-import android.widget.ImageView;
-import android.widget.TextView;
import android.widget.VideoView;
import androidx.annotation.IntDef;
@@ -59,25 +38,21 @@
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
+import androidx.media.AudioAttributesCompat;
import androidx.media.DataSourceDesc;
import androidx.media.MediaItem2;
import androidx.media.MediaMetadata2;
import androidx.media.SessionToken2;
-import androidx.palette.graphics.Palette;
-import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
-// TODO: Replace MediaSession wtih MediaSession2 once MediaSession2 is submitted.
/**
- * @hide
- * Displays a video file. VideoView2 class is a View class which is wrapping {@link MediaPlayer}
- * so that developers can easily implement a video rendering application.
+ * Displays a video file. VideoView2 class is a ViewGroup class which is wrapping
+ * {@link MediaPlayer} so that developers can easily implement a video rendering application.
*
* <p>
* <em> Data sources that VideoView2 supports : </em>
@@ -98,20 +73,12 @@
* VideoView2 covers and inherits the most of
* VideoView's functionalities. The main differences are
* <ul>
- * <li> VideoView2 inherits FrameLayout and renders videos using SurfaceView and TextureView
+ * <li> VideoView2 inherits ViewGroup and renders videos using SurfaceView and TextureView
* selectively while VideoView inherits SurfaceView class.
* <li> VideoView2 is integrated with MediaControlView2 and a default MediaControlView2 instance is
- * attached to VideoView2 by default. If a developer does not want to use the default
- * MediaControlView2, needs to set enableControlView attribute to false. For instance,
- * <pre>
- * <VideoView2
- * android:id="@+id/video_view"
- * xmlns:widget="http://schemas.android.com/apk/com.android.media.update"
- * widget:enableControlView="false" />
- * </pre>
- * If a developer wants to attach a customed MediaControlView2, then set enableControlView attribute
- * to false and assign the customed media control widget using {@link #setMediaControlView2}.
- * <li> VideoView2 is integrated with MediaPlayer while VideoView is integrated with MediaPlayer.
+ * attached to VideoView2 by default.
+ * <li> If a developer wants to attach a customed MediaControlView2,
+ * assign the customed media control widget using {@link #setMediaControlView2}.
* <li> VideoView2 is integrated with MediaSession and so it responses with media key events.
* A VideoView2 keeps a MediaSession instance internally and connects it to a corresponding
* MediaControlView2 instance.
@@ -134,8 +101,7 @@
* {@link android.app.Activity#onRestoreInstanceState}.
*/
@RequiresApi(21) // TODO correct minSdk API use incompatibilities and remove before release.
-@RestrictTo(LIBRARY_GROUP)
-public class VideoView2 extends BaseLayout implements VideoViewInterface.SurfaceListener {
+public class VideoView2 extends BaseLayout {
/** @hide */
@RestrictTo(LIBRARY_GROUP)
@IntDef({
@@ -160,171 +126,10 @@
public static final int VIEW_TYPE_TEXTUREVIEW = 1;
private static final String TAG = "VideoView2";
- private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
- private static final long DEFAULT_SHOW_CONTROLLER_INTERVAL_MS = 2000;
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+ private static final boolean USE_MP2 = Log.isLoggable("VV2MP2", Log.DEBUG);
- private static final int STATE_ERROR = -1;
- private static final int STATE_IDLE = 0;
- private static final int STATE_PREPARING = 1;
- private static final int STATE_PREPARED = 2;
- private static final int STATE_PLAYING = 3;
- private static final int STATE_PAUSED = 4;
- private static final int STATE_PLAYBACK_COMPLETED = 5;
-
- private static final int INVALID_TRACK_INDEX = -1;
- private static final float INVALID_SPEED = 0f;
-
- private static final int SIZE_TYPE_EMBEDDED = 0;
- private static final int SIZE_TYPE_FULL = 1;
- // TODO: add support for Minimal size type.
- private static final int SIZE_TYPE_MINIMAL = 2;
-
- private AccessibilityManager mAccessibilityManager;
- private AudioManager mAudioManager;
- private AudioAttributes mAudioAttributes;
- private int mAudioFocusType = AudioManager.AUDIOFOCUS_GAIN; // legacy focus gain
- private boolean mAudioFocused = false;
-
- private Pair<Executor, OnCustomActionListener> mCustomActionListenerRecord;
- private OnViewTypeChangedListener mViewTypeChangedListener;
- private OnFullScreenRequestListener mFullScreenRequestListener;
-
- private VideoViewInterface mCurrentView;
- private VideoTextureView mTextureView;
- private VideoSurfaceView mSurfaceView;
-
- private MediaPlayer mMediaPlayer;
- private DataSourceDesc mDsd;
- private MediaControlView2 mMediaControlView;
- private MediaSessionCompat mMediaSession;
- private MediaControllerCompat mMediaController;
- private MediaMetadata2 mMediaMetadata;
- private MediaMetadataRetriever mRetriever;
- private boolean mNeedUpdateMediaType;
- private Bundle mMediaTypeData;
- private String mTitle;
-
- // TODO: move music view inside SurfaceView/TextureView or implement VideoViewInterface.
- private WindowManager mManager;
- private Resources mResources;
- private View mMusicView;
- private Drawable mMusicAlbumDrawable;
- private String mMusicTitleText;
- private String mMusicArtistText;
- private boolean mIsMusicMediaType;
- private int mPrevWidth;
- private int mPrevHeight;
- private int mDominantColor;
- private int mSizeType;
-
- private PlaybackStateCompat.Builder mStateBuilder;
- private List<PlaybackStateCompat.CustomAction> mCustomActionList;
-
- private int mTargetState = STATE_IDLE;
- private int mCurrentState = STATE_IDLE;
- private int mCurrentBufferPercentage;
- private long mSeekWhenPrepared; // recording the seek position while preparing
-
- private int mVideoWidth;
- private int mVideoHeight;
-
- private ArrayList<Integer> mVideoTrackIndices;
- private ArrayList<Integer> mAudioTrackIndices;
- // private ArrayList<Pair<Integer, SubtitleTrack>> mSubtitleTrackIndices;
- // private SubtitleController mSubtitleController;
-
- // selected video/audio/subtitle track index as MediaPlayer returns
- private int mSelectedVideoTrackIndex;
- private int mSelectedAudioTrackIndex;
- private int mSelectedSubtitleTrackIndex;
-
- // private SubtitleView mSubtitleView;
- private boolean mSubtitleEnabled;
-
- private float mSpeed;
- // TODO: Remove mFallbackSpeed when integration with MediaPlayer's new setPlaybackParams().
- // Refer: https://docs.google.com/document/d/1nzAfns6i2hJ3RkaUre3QMT6wsDedJ5ONLiA_OOBFFX8/edit
- private float mFallbackSpeed; // keep the original speed before 'pause' is called.
- private float mVolumeLevelFloat;
- private int mVolumeLevel;
-
- private long mShowControllerIntervalMs;
-
- // private MediaRouter mMediaRouter;
- // private MediaRouteSelector mRouteSelector;
- // private MediaRouter.RouteInfo mRoute;
- // private RoutePlayer mRoutePlayer;
-
- // TODO (b/77158231)
- /*
- private final MediaRouter.Callback mRouterCallback = new MediaRouter.Callback() {
- @Override
- public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) {
- if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
- // Stop local playback (if necessary)
- resetPlayer();
- mRoute = route;
- mRoutePlayer = new RoutePlayer(getContext(), route);
- mRoutePlayer.setPlayerEventCallback(new RoutePlayer.PlayerEventCallback() {
- @Override
- public void onPlayerStateChanged(MediaItemStatus itemStatus) {
- PlaybackStateCompat.Builder psBuilder = new PlaybackStateCompat.Builder();
- psBuilder.setActions(RoutePlayer.PLAYBACK_ACTIONS);
- long position = itemStatus.getContentPosition();
- switch (itemStatus.getPlaybackState()) {
- case MediaItemStatus.PLAYBACK_STATE_PENDING:
- psBuilder.setState(PlaybackStateCompat.STATE_NONE, position, 0);
- mCurrentState = STATE_IDLE;
- break;
- case MediaItemStatus.PLAYBACK_STATE_PLAYING:
- psBuilder.setState(PlaybackStateCompat.STATE_PLAYING, position, 1);
- mCurrentState = STATE_PLAYING;
- break;
- case MediaItemStatus.PLAYBACK_STATE_PAUSED:
- psBuilder.setState(PlaybackStateCompat.STATE_PAUSED, position, 0);
- mCurrentState = STATE_PAUSED;
- break;
- case MediaItemStatus.PLAYBACK_STATE_BUFFERING:
- psBuilder.setState(
- PlaybackStateCompat.STATE_BUFFERING, position, 0);
- mCurrentState = STATE_PAUSED;
- break;
- case MediaItemStatus.PLAYBACK_STATE_FINISHED:
- psBuilder.setState(PlaybackStateCompat.STATE_STOPPED, position, 0);
- mCurrentState = STATE_PLAYBACK_COMPLETED;
- break;
- }
-
- PlaybackStateCompat pbState = psBuilder.build();
- mMediaSession.setPlaybackState(pbState);
-
- MediaMetadataCompat.Builder mmBuilder = new MediaMetadataCompat.Builder();
- mmBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION,
- itemStatus.getContentDuration());
- mMediaSession.setMetadata(mmBuilder.build());
- }
- });
- // Start remote playback (if necessary)
- mRoutePlayer.openVideo(mDsd);
- }
- }
-
- @Override
- public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route, int reason) {
- if (mRoute != null && mRoutePlayer != null) {
- mRoutePlayer.release();
- mRoutePlayer = null;
- }
- if (mRoute == route) {
- mRoute = null;
- }
- if (reason != MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) {
- // TODO: Resume local playback (if necessary)
- openVideo(mDsd);
- }
- }
- };
- */
+ private VideoView2Impl mImpl;
public VideoView2(@NonNull Context context) {
this(context, null);
@@ -335,86 +140,20 @@
}
public VideoView2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
- }
-
- public VideoView2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
- int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
-
- mVideoWidth = 0;
- mVideoHeight = 0;
- mSpeed = 1.0f;
- mFallbackSpeed = mSpeed;
- mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX;
- // TODO: add attributes to get this value.
- mShowControllerIntervalMs = DEFAULT_SHOW_CONTROLLER_INTERVAL_MS;
-
- mAccessibilityManager = (AccessibilityManager) context.getSystemService(
- Context.ACCESSIBILITY_SERVICE);
-
- mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
- mAudioAttributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA)
- .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE).build();
- setFocusable(true);
- setFocusableInTouchMode(true);
- requestFocus();
-
- // TODO: try to keep a single child at a time rather than always having both.
- mTextureView = new VideoTextureView(getContext());
- mSurfaceView = new VideoSurfaceView(getContext());
- LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT,
- LayoutParams.MATCH_PARENT);
- mTextureView.setLayoutParams(params);
- mSurfaceView.setLayoutParams(params);
- mTextureView.setSurfaceListener(this);
- mSurfaceView.setSurfaceListener(this);
-
- addView(mTextureView);
- addView(mSurfaceView);
-
- // mSubtitleView = new SubtitleView(getContext());
- // mSubtitleView.setLayoutParams(params);
- // mSubtitleView.setBackgroundColor(0);
- // addView(mSubtitleView);
-
- boolean enableControlView = (attrs == null) || attrs.getAttributeBooleanValue(
- "http://schemas.android.com/apk/res/android",
- "enableControlView", true);
- if (enableControlView) {
- mMediaControlView = new MediaControlView2(getContext());
+ super(context, attrs, defStyleAttr);
+ if (android.os.Build.VERSION.SDK_INT >= 28) {
+ if (USE_MP2) {
+ Log.d(TAG, "Create VideoView2ImplBase");
+ mImpl = new VideoView2ImplBase();
+ } else {
+ Log.d(TAG, "Create VideoView2ImplApi28WithMp1");
+ mImpl = new VideoView2ImplApi28WithMp1();
+ }
+ } else {
+ Log.d(TAG, "Create VideoView2ImplBaseWithMp1");
+ mImpl = new VideoView2ImplBaseWithMp1();
}
-
- mSubtitleEnabled = (attrs == null) || attrs.getAttributeBooleanValue(
- "http://schemas.android.com/apk/res/android",
- "enableSubtitle", false);
-
- // TODO: Choose TextureView when SurfaceView cannot be created.
- // Choose surface view by default
- int viewType = (attrs == null) ? VideoView2.VIEW_TYPE_SURFACEVIEW
- : attrs.getAttributeIntValue(
- "http://schemas.android.com/apk/res/android",
- "viewType", VideoView2.VIEW_TYPE_SURFACEVIEW);
- if (viewType == VideoView2.VIEW_TYPE_SURFACEVIEW) {
- Log.d(TAG, "viewType attribute is surfaceView.");
- mTextureView.setVisibility(View.GONE);
- mSurfaceView.setVisibility(View.VISIBLE);
- mCurrentView = mSurfaceView;
- } else if (viewType == VideoView2.VIEW_TYPE_TEXTUREVIEW) {
- Log.d(TAG, "viewType attribute is textureView.");
- mTextureView.setVisibility(View.VISIBLE);
- mSurfaceView.setVisibility(View.GONE);
- mCurrentView = mTextureView;
- }
-
- // TODO (b/77158231)
- /*
- MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
- builder.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
- builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
- builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
- mRouteSelector = builder.build();
- */
+ mImpl.initialize(this, context, attrs, defStyleAttr);
}
/**
@@ -425,15 +164,7 @@
* @param intervalMs a time interval in milliseconds until VideoView2 hides MediaControlView2.
*/
public void setMediaControlView2(MediaControlView2 mediaControlView, long intervalMs) {
- mMediaControlView = mediaControlView;
- mShowControllerIntervalMs = intervalMs;
- // TODO: Call MediaControlView2.setRouteSelector only when cast availalbe.
- // TODO (b/77158231)
- // mMediaControlView.setRouteSelector(mRouteSelector);
-
- if (isAttachedToWindow()) {
- attachMediaControlView();
- }
+ mImpl.setMediaControlView2(mediaControlView, intervalMs);
}
/**
@@ -441,7 +172,7 @@
* {@link #setMediaControlView2} method.
*/
public MediaControlView2 getMediaControlView2() {
- return mMediaControlView;
+ return mImpl.getMediaControlView2();
}
/**
@@ -463,7 +194,7 @@
*/
@RestrictTo(LIBRARY_GROUP)
public MediaMetadata2 getMediaMetadata() {
- return mMediaMetadata;
+ return mImpl.getMediaMetadata();
}
/**
@@ -478,10 +209,7 @@
*/
@RestrictTo(LIBRARY_GROUP)
public MediaControllerCompat getMediaController() {
- if (mMediaSession == null) {
- throw new IllegalStateException("MediaSession instance is not available.");
- }
- return mMediaController;
+ return mImpl.getMediaController();
}
/**
@@ -494,7 +222,6 @@
*/
@RestrictTo(LIBRARY_GROUP)
public SessionToken2 getMediaSessionToken() {
- //return mProvider.getMediaSessionToken_impl();
return null;
}
@@ -505,10 +232,7 @@
* @param enable shows closed caption or subtitles if this value is true, or hides.
*/
public void setSubtitleEnabled(boolean enable) {
- if (enable != mSubtitleEnabled) {
- selectOrDeselectSubtitle(enable);
- }
- mSubtitleEnabled = enable;
+ mImpl.setSubtitleEnabled(enable);
}
/**
@@ -517,7 +241,7 @@
* has been enabled by {@link #setSubtitleEnabled}.
*/
public boolean isSubtitleEnabled() {
- return mSubtitleEnabled;
+ return mImpl.isSubtitleEnabled();
}
/**
@@ -529,17 +253,18 @@
* be reset to the normal speed 1.0f.
* @param speed the playback speed. It should be positive.
*/
- // TODO: Support this via MediaController2.
public void setSpeed(float speed) {
- if (speed <= 0.0f) {
- Log.e(TAG, "Unsupported speed (" + speed + ") is ignored.");
- return;
- }
- mSpeed = speed;
- if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
- applySpeed();
- }
- updatePlaybackState();
+ mImpl.setSpeed(speed);
+ }
+
+ /**
+ * Returns playback speed.
+ *
+ * It returns the same value that has been set by {@link #setSpeed}, if it was available value.
+ * If {@link #setSpeed} has not been called before, then the normal speed 1.0f will be returned.
+ */
+ public float getSpeed() {
+ return mImpl.getSpeed();
}
/**
@@ -558,14 +283,7 @@
* playback.
*/
public void setAudioFocusRequest(int focusGain) {
- if (focusGain != AudioManager.AUDIOFOCUS_NONE
- && focusGain != AudioManager.AUDIOFOCUS_GAIN
- && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
- && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
- && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) {
- throw new IllegalArgumentException("Illegal audio focus type " + focusGain);
- }
- mAudioFocusType = focusGain;
+ mImpl.setAudioFocusRequest(focusGain);
}
/**
@@ -574,10 +292,19 @@
* @param attributes non-null <code>AudioAttributes</code>.
*/
public void setAudioAttributes(@NonNull AudioAttributes attributes) {
- if (attributes == null) {
- throw new IllegalArgumentException("Illegal null AudioAttributes");
- }
- mAudioAttributes = attributes;
+ mImpl.setAudioAttributes(AudioAttributesCompat.wrap(attributes));
+ }
+
+ /**
+ * Sets the {@link AudioAttributesCompat} to be used during the playback of the video.
+ *
+ * @param attributes non-null <code>AudioAttributesCompat</code>.
+ *
+ * @hide TODO unhide and remove setAudioAttributes with framework attributes
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void setAudioAttributes(@NonNull AudioAttributesCompat attributes) {
+ mImpl.setAudioAttributes(attributes);
}
/**
@@ -585,11 +312,11 @@
*
* @param path the path of the video.
*
- * @hide TODO remove
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
public void setVideoPath(String path) {
- setVideoUri(Uri.parse(path));
+ mImpl.setVideoUri(Uri.parse(path));
}
/**
@@ -597,11 +324,11 @@
*
* @param uri the URI of the video.
*
- * @hide TODO remove
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
public void setVideoUri(Uri uri) {
- setVideoUri(uri, null);
+ mImpl.setVideoUri(uri, null);
}
/**
@@ -613,13 +340,9 @@
* changed with key/value pairs through the headers parameter with
* "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
* to disallow or allow cross domain redirection.
- *
- * @hide TODO remove
*/
- @RestrictTo(LIBRARY_GROUP)
- public void setVideoUri(Uri uri, Map<String, String> headers) {
- mSeekWhenPrepared = 0;
- openVideo(uri, headers);
+ public void setVideoUri(Uri uri, @Nullable Map<String, String> headers) {
+ mImpl.setVideoUri(uri, headers);
}
/**
@@ -627,9 +350,11 @@
* object to VideoView2 is {@link #setDataSource}.
* @param mediaItem the MediaItem2 to play
* @see #setDataSource
+ *
+ * @hide
*/
+ @RestrictTo(LIBRARY_GROUP)
public void setMediaItem(@NonNull MediaItem2 mediaItem) {
- //mProvider.setMediaItem_impl(mediaItem);
}
/**
@@ -640,7 +365,6 @@
*/
@RestrictTo(LIBRARY_GROUP)
public void setDataSource(@NonNull DataSourceDesc dataSource) {
- //mProvider.setDataSource_impl(dataSource);
}
/**
@@ -653,22 +377,7 @@
* </ul>
*/
public void setViewType(@ViewType int viewType) {
- if (viewType == mCurrentView.getViewType()) {
- return;
- }
- VideoViewInterface targetView;
- if (viewType == VideoView2.VIEW_TYPE_TEXTUREVIEW) {
- Log.d(TAG, "switching to TextureView");
- targetView = mTextureView;
- } else if (viewType == VideoView2.VIEW_TYPE_SURFACEVIEW) {
- Log.d(TAG, "switching to SurfaceView");
- targetView = mSurfaceView;
- } else {
- throw new IllegalArgumentException("Unknown view type: " + viewType);
- }
- ((View) targetView).setVisibility(View.VISIBLE);
- targetView.takeOver(mCurrentView);
- requestLayout();
+ mImpl.setViewType(viewType);
}
/**
@@ -678,7 +387,7 @@
*/
@ViewType
public int getViewType() {
- return mCurrentView.getViewType();
+ return mImpl.getViewType();
}
/**
@@ -689,17 +398,12 @@
* buttons in {@link MediaControlView2}.
* @param executor executor to run callbacks on.
* @param listener A listener to be called when a custom button is clicked.
- * @hide TODO remove
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
public void setCustomActions(List<PlaybackStateCompat.CustomAction> actionList,
Executor executor, OnCustomActionListener listener) {
- mCustomActionList = actionList;
- mCustomActionListenerRecord = new Pair<>(executor, listener);
-
- // Create a new playback builder in order to clear existing the custom actions.
- mStateBuilder = null;
- updatePlaybackState();
+ mImpl.setCustomActions(actionList, executor, listener);
}
/**
@@ -711,54 +415,19 @@
@VisibleForTesting
@RestrictTo(LIBRARY_GROUP)
public void setOnViewTypeChangedListener(OnViewTypeChangedListener l) {
- mViewTypeChangedListener = l;
- }
-
- /**
- * Registers a callback to be invoked when the fullscreen mode should be changed.
- * @param l The callback that will be run
- * @hide TODO remove
- */
- @RestrictTo(LIBRARY_GROUP)
- public void setFullScreenRequestListener(OnFullScreenRequestListener l) {
- mFullScreenRequestListener = l;
+ mImpl.setOnViewTypeChangedListener(l);
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
-
- // Create MediaSession
- mMediaSession = new MediaSessionCompat(getContext(), "VideoView2MediaSession");
- mMediaSession.setCallback(new MediaSessionCallback());
- mMediaSession.setActive(true);
- mMediaController = mMediaSession.getController();
- // TODO (b/77158231)
- // mMediaRouter = MediaRouter.getInstance(getContext());
- // mMediaRouter.setMediaSession(mMediaSession);
- // mMediaRouter.addCallback(mRouteSelector, mRouterCallback);
- attachMediaControlView();
- // TODO: remove this after moving MediaSession creating code inside initializing VideoView2
- if (mCurrentState == STATE_PREPARED) {
- extractTracks();
- extractMetadata();
- extractAudioMetadata();
- if (mNeedUpdateMediaType) {
- mMediaSession.sendSessionEvent(
- MediaControlView2.EVENT_UPDATE_MEDIA_TYPE_STATUS,
- mMediaTypeData);
- mNeedUpdateMediaType = false;
- }
- }
+ mImpl.onAttachedToWindowImpl();
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
-
- mMediaSession.release();
- mMediaSession = null;
- mMediaController = null;
+ mImpl.onDetachedFromWindowImpl();
}
@Override
@@ -768,76 +437,25 @@
@Override
public boolean onTouchEvent(MotionEvent ev) {
- if (DEBUG) {
- Log.d(TAG, "onTouchEvent(). mCurrentState=" + mCurrentState
- + ", mTargetState=" + mTargetState);
- }
- if (ev.getAction() == MotionEvent.ACTION_UP && mMediaControlView != null) {
- if (!mIsMusicMediaType || mSizeType != SIZE_TYPE_FULL) {
- toggleMediaControlViewVisibility();
- }
- }
-
+ mImpl.onTouchEventImpl(ev);
return super.onTouchEvent(ev);
}
@Override
public boolean onTrackballEvent(MotionEvent ev) {
- if (ev.getAction() == MotionEvent.ACTION_UP && mMediaControlView != null) {
- if (!mIsMusicMediaType || mSizeType != SIZE_TYPE_FULL) {
- toggleMediaControlViewVisibility();
- }
- }
-
+ mImpl.onTrackballEventImpl(ev);
return super.onTrackballEvent(ev);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
- // TODO: Test touch event handling logic thoroughly and simplify the logic.
return super.dispatchTouchEvent(ev);
}
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- if (mIsMusicMediaType) {
- if (mPrevWidth != getMeasuredWidth()
- || mPrevHeight != getMeasuredHeight()) {
- int currWidth = getMeasuredWidth();
- int currHeight = getMeasuredHeight();
- Point screenSize = new Point();
- mManager.getDefaultDisplay().getSize(screenSize);
- int screenWidth = screenSize.x;
- int screenHeight = screenSize.y;
-
- if (currWidth == screenWidth && currHeight == screenHeight) {
- int orientation = retrieveOrientation();
- if (orientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
- inflateMusicView(R.layout.full_landscape_music);
- } else {
- inflateMusicView(R.layout.full_portrait_music);
- }
-
- if (mSizeType != SIZE_TYPE_FULL) {
- mSizeType = SIZE_TYPE_FULL;
- // Remove existing mFadeOut callback
- mMediaControlView.removeCallbacks(mFadeOut);
- mMediaControlView.setVisibility(View.VISIBLE);
- }
- } else {
- if (mSizeType != SIZE_TYPE_EMBEDDED) {
- mSizeType = SIZE_TYPE_EMBEDDED;
- inflateMusicView(R.layout.embedded_music);
- // Add new mFadeOut callback
- mMediaControlView.postDelayed(mFadeOut, mShowControllerIntervalMs);
- }
- }
- mPrevWidth = currWidth;
- mPrevHeight = currHeight;
- }
- }
+ mImpl.onMeasureImpl(widthMeasureSpec, heightMeasureSpec);
}
/**
@@ -845,7 +463,6 @@
*
* @hide
*/
- @VisibleForTesting
@RestrictTo(LIBRARY_GROUP)
public interface OnViewTypeChangedListener {
/**
@@ -862,19 +479,6 @@
}
/**
- * Interface definition of a callback to be invoked to inform the fullscreen mode is changed.
- * Application should handle the fullscreen mode accordingly.
- * @hide TODO remove
- */
- @RestrictTo(LIBRARY_GROUP)
- public interface OnFullScreenRequestListener {
- /**
- * Called to indicate a fullscreen mode change.
- */
- void onFullScreenRequest(View view, boolean fullScreen);
- }
-
- /**
* Interface definition of a callback to be invoked to inform that a custom action is performed.
* @hide TODO remove
*/
@@ -889,901 +493,4 @@
*/
void onCustomAction(String action, Bundle extras);
}
-
- ///////////////////////////////////////////////////
- // Implements VideoViewInterface.SurfaceListener
- ///////////////////////////////////////////////////
-
- @Override
- public void onSurfaceCreated(View view, int width, int height) {
- if (DEBUG) {
- Log.d(TAG, "onSurfaceCreated(). mCurrentState=" + mCurrentState
- + ", mTargetState=" + mTargetState + ", width/height: " + width + "/" + height
- + ", " + view.toString());
- }
- if (needToStart()) {
- mMediaController.getTransportControls().play();
- }
- }
-
- @Override
- public void onSurfaceDestroyed(View view) {
- if (DEBUG) {
- Log.d(TAG, "onSurfaceDestroyed(). mCurrentState=" + mCurrentState
- + ", mTargetState=" + mTargetState + ", " + view.toString());
- }
- }
-
- @Override
- public void onSurfaceChanged(View view, int width, int height) {
- // TODO: Do we need to call requestLayout here?
- if (DEBUG) {
- Log.d(TAG, "onSurfaceChanged(). width/height: " + width + "/" + height
- + ", " + view.toString());
- }
- }
-
- @Override
- public void onSurfaceTakeOverDone(VideoViewInterface view) {
- if (DEBUG) {
- Log.d(TAG, "onSurfaceTakeOverDone(). Now current view is: " + view);
- }
- mCurrentView = view;
- if (mViewTypeChangedListener != null) {
- mViewTypeChangedListener.onViewTypeChanged(this, view.getViewType());
- }
- if (needToStart()) {
- mMediaController.getTransportControls().play();
- }
- }
-
- ///////////////////////////////////////////////////
- // Protected or private methods
- ///////////////////////////////////////////////////
-
- private void attachMediaControlView() {
- // Get MediaController from MediaSession and set it inside MediaControlView
- mMediaControlView.setController(mMediaSession.getController());
-
- LayoutParams params =
- new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
- addView(mMediaControlView, params);
- }
-
- private boolean isInPlaybackState() {
- // TODO (b/77158231)
- // return (mMediaPlayer != null || mRoutePlayer != null)
- return (mMediaPlayer != null)
- && mCurrentState != STATE_ERROR
- && mCurrentState != STATE_IDLE
- && mCurrentState != STATE_PREPARING;
- }
-
- private boolean needToStart() {
- // TODO (b/77158231)
- // return (mMediaPlayer != null || mRoutePlayer != null)
- return (mMediaPlayer != null)
- && isAudioGranted()
- && isWaitingPlayback();
- }
-
- private boolean isWaitingPlayback() {
- return mCurrentState != STATE_PLAYING && mTargetState == STATE_PLAYING;
- }
-
- private boolean isAudioGranted() {
- return mAudioFocused || mAudioFocusType == AudioManager.AUDIOFOCUS_NONE;
- }
-
- AudioManager.OnAudioFocusChangeListener mAudioFocusListener =
- new AudioManager.OnAudioFocusChangeListener() {
- @Override
- public void onAudioFocusChange(int focusChange) {
- switch (focusChange) {
- case AudioManager.AUDIOFOCUS_GAIN:
- mAudioFocused = true;
- if (needToStart()) {
- mMediaController.getTransportControls().play();
- }
- break;
- case AudioManager.AUDIOFOCUS_LOSS:
- case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
- case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
- // There is no way to distinguish pause() by transient
- // audio focus loss and by other explicit actions.
- // TODO: If we can distinguish those cases, change the code to resume when it
- // gains audio focus again for AUDIOFOCUS_LOSS_TRANSIENT and
- // AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
- mAudioFocused = false;
- if (isInPlaybackState() && mMediaPlayer.isPlaying()) {
- mMediaController.getTransportControls().pause();
- } else {
- mTargetState = STATE_PAUSED;
- }
- }
- }
- };
-
- @SuppressWarnings("deprecation")
- private void requestAudioFocus(int focusType) {
- int result;
- if (android.os.Build.VERSION.SDK_INT >= 26) {
- AudioFocusRequest focusRequest;
- focusRequest = new AudioFocusRequest.Builder(focusType)
- .setAudioAttributes(mAudioAttributes)
- .setOnAudioFocusChangeListener(mAudioFocusListener)
- .build();
- result = mAudioManager.requestAudioFocus(focusRequest);
- } else {
- result = mAudioManager.requestAudioFocus(mAudioFocusListener,
- AudioManager.STREAM_MUSIC,
- focusType);
- }
- if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
- mAudioFocused = false;
- } else if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
- mAudioFocused = true;
- } else if (result == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
- mAudioFocused = false;
- }
- }
-
- // Creates a MediaPlayer instance and prepare playback.
- private void openVideo(Uri uri, Map<String, String> headers) {
- resetPlayer();
- if (isRemotePlayback()) {
- // TODO (b/77158231)
- // mRoutePlayer.openVideo(dsd);
- return;
- }
-
- try {
- Log.d(TAG, "openVideo(): creating new MediaPlayer instance.");
- mMediaPlayer = new MediaPlayer();
- mSurfaceView.setMediaPlayer(mMediaPlayer);
- mTextureView.setMediaPlayer(mMediaPlayer);
- mCurrentView.assignSurfaceToMediaPlayer(mMediaPlayer);
-
- final Context context = getContext();
- // TODO: Add timely firing logic for more accurate sync between CC and video frame
- // mSubtitleController = new SubtitleController(context);
- // mSubtitleController.registerRenderer(new ClosedCaptionRenderer(context));
- // mSubtitleController.setAnchor((SubtitleController.Anchor) mSubtitleView);
-
- mMediaPlayer.setOnPreparedListener(mPreparedListener);
- mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
- mMediaPlayer.setOnCompletionListener(mCompletionListener);
- mMediaPlayer.setOnSeekCompleteListener(mSeekCompleteListener);
- mMediaPlayer.setOnErrorListener(mErrorListener);
- mMediaPlayer.setOnInfoListener(mInfoListener);
- mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);
-
- mCurrentBufferPercentage = -1;
- mMediaPlayer.setDataSource(getContext(), uri, headers);
- mMediaPlayer.setAudioAttributes(mAudioAttributes);
- // mMediaPlayer.setOnSubtitleDataListener(mSubtitleListener);
- // we don't set the target state here either, but preserve the
- // target state that was there before.
- mCurrentState = STATE_PREPARING;
- mMediaPlayer.prepareAsync();
-
- // Save file name as title since the file may not have a title Metadata.
- mTitle = uri.getPath();
- String scheme = uri.getScheme();
- if (scheme != null && scheme.equals("file")) {
- mTitle = uri.getLastPathSegment();
- }
- mRetriever = new MediaMetadataRetriever();
- mRetriever.setDataSource(getContext(), uri);
-
- if (DEBUG) {
- Log.d(TAG, "openVideo(). mCurrentState=" + mCurrentState
- + ", mTargetState=" + mTargetState);
- }
- } catch (IOException | IllegalArgumentException ex) {
- Log.w(TAG, "Unable to open content: " + uri, ex);
- mCurrentState = STATE_ERROR;
- mTargetState = STATE_ERROR;
- mErrorListener.onError(mMediaPlayer,
- MediaPlayer.MEDIA_ERROR_UNKNOWN, MediaPlayer.MEDIA_ERROR_IO);
- }
- }
-
- /*
- * Reset the media player in any state
- */
- @SuppressWarnings("deprecation")
- private void resetPlayer() {
- if (mMediaPlayer != null) {
- mMediaPlayer.reset();
- mMediaPlayer.release();
- mMediaPlayer = null;
- mTextureView.setMediaPlayer(null);
- mSurfaceView.setMediaPlayer(null);
- mCurrentState = STATE_IDLE;
- mTargetState = STATE_IDLE;
- if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
- mAudioManager.abandonAudioFocus(null);
- }
- }
- mVideoWidth = 0;
- mVideoHeight = 0;
- }
-
- private void updatePlaybackState() {
- if (mStateBuilder == null) {
- /*
- // Get the capabilities of the player for this stream
- mMetadata = mMediaPlayer.getMetadata(MediaPlayer.METADATA_ALL,
- MediaPlayer.BYPASS_METADATA_FILTER);
-
- // Add Play action as default
- long playbackActions = PlaybackStateCompat.ACTION_PLAY;
- if (mMetadata != null) {
- if (!mMetadata.has(Metadata.PAUSE_AVAILABLE)
- || mMetadata.getBoolean(Metadata.PAUSE_AVAILABLE)) {
- playbackActions |= PlaybackStateCompat.ACTION_PAUSE;
- }
- if (!mMetadata.has(Metadata.SEEK_BACKWARD_AVAILABLE)
- || mMetadata.getBoolean(Metadata.SEEK_BACKWARD_AVAILABLE)) {
- playbackActions |= PlaybackStateCompat.ACTION_REWIND;
- }
- if (!mMetadata.has(Metadata.SEEK_FORWARD_AVAILABLE)
- || mMetadata.getBoolean(Metadata.SEEK_FORWARD_AVAILABLE)) {
- playbackActions |= PlaybackStateCompat.ACTION_FAST_FORWARD;
- }
- if (!mMetadata.has(Metadata.SEEK_AVAILABLE)
- || mMetadata.getBoolean(Metadata.SEEK_AVAILABLE)) {
- playbackActions |= PlaybackStateCompat.ACTION_SEEK_TO;
- }
- } else {
- playbackActions |= (PlaybackStateCompat.ACTION_PAUSE
- | PlaybackStateCompat.ACTION_REWIND
- | PlaybackStateCompat.ACTION_FAST_FORWARD
- | PlaybackStateCompat.ACTION_SEEK_TO);
- }
- */
- // TODO determine the actionable list based the metadata info.
- long playbackActions = PlaybackStateCompat.ACTION_PLAY
- | PlaybackStateCompat.ACTION_PAUSE
- | PlaybackStateCompat.ACTION_REWIND | PlaybackStateCompat.ACTION_FAST_FORWARD
- | PlaybackStateCompat.ACTION_SEEK_TO;
- mStateBuilder = new PlaybackStateCompat.Builder();
- mStateBuilder.setActions(playbackActions);
-
- if (mCustomActionList != null) {
- for (PlaybackStateCompat.CustomAction action : mCustomActionList) {
- mStateBuilder.addCustomAction(action);
- }
- }
- }
- mStateBuilder.setState(getCorrespondingPlaybackState(),
- mMediaPlayer.getCurrentPosition(), mSpeed);
- if (mCurrentState != STATE_ERROR
- && mCurrentState != STATE_IDLE
- && mCurrentState != STATE_PREPARING) {
- // TODO: this should be replaced with MediaPlayer2.getBufferedPosition() once it is
- // implemented.
- if (mCurrentBufferPercentage == -1) {
- mStateBuilder.setBufferedPosition(-1);
- } else {
- mStateBuilder.setBufferedPosition(
- (long) (mCurrentBufferPercentage / 100.0 * mMediaPlayer.getDuration()));
- }
- }
-
- // Set PlaybackState for MediaSession
- if (mMediaSession != null) {
- PlaybackStateCompat state = mStateBuilder.build();
- mMediaSession.setPlaybackState(state);
- }
- }
-
- private int getCorrespondingPlaybackState() {
- switch (mCurrentState) {
- case STATE_ERROR:
- return PlaybackStateCompat.STATE_ERROR;
- case STATE_IDLE:
- return PlaybackStateCompat.STATE_NONE;
- case STATE_PREPARING:
- return PlaybackStateCompat.STATE_CONNECTING;
- case STATE_PREPARED:
- return PlaybackStateCompat.STATE_PAUSED;
- case STATE_PLAYING:
- return PlaybackStateCompat.STATE_PLAYING;
- case STATE_PAUSED:
- return PlaybackStateCompat.STATE_PAUSED;
- case STATE_PLAYBACK_COMPLETED:
- return PlaybackStateCompat.STATE_STOPPED;
- default:
- return -1;
- }
- }
-
- private final Runnable mFadeOut = new Runnable() {
- @Override
- public void run() {
- if (mCurrentState == STATE_PLAYING) {
- mMediaControlView.setVisibility(View.GONE);
- }
- }
- };
-
- private void showController() {
- // TODO: Decide what to show when the state is not in playback state
- if (mMediaControlView == null || !isInPlaybackState()
- || (mIsMusicMediaType && mSizeType == SIZE_TYPE_FULL)) {
- return;
- }
- mMediaControlView.removeCallbacks(mFadeOut);
- mMediaControlView.setVisibility(View.VISIBLE);
- if (mShowControllerIntervalMs != 0
- && !mAccessibilityManager.isTouchExplorationEnabled()) {
- mMediaControlView.postDelayed(mFadeOut, mShowControllerIntervalMs);
- }
- }
-
- private void toggleMediaControlViewVisibility() {
- if (mMediaControlView.getVisibility() == View.VISIBLE) {
- mMediaControlView.removeCallbacks(mFadeOut);
- mMediaControlView.setVisibility(View.GONE);
- } else {
- showController();
- }
- }
-
- private void applySpeed() {
- if (android.os.Build.VERSION.SDK_INT < 23) {
- // TODO: MediaPlayer2 will cover this, or implement with SoundPool.
- return;
- }
- PlaybackParams params = mMediaPlayer.getPlaybackParams().allowDefaults();
- if (mSpeed != params.getSpeed()) {
- try {
- params.setSpeed(mSpeed);
- mMediaPlayer.setPlaybackParams(params);
- mFallbackSpeed = mSpeed;
- } catch (IllegalArgumentException e) {
- Log.e(TAG, "PlaybackParams has unsupported value: " + e);
- // TODO: should revise this part after integrating with MP2.
- // If mSpeed had an illegal value for speed rate, system will determine best
- // handling (see PlaybackParams.AUDIO_FALLBACK_MODE_DEFAULT).
- // Note: The pre-MP2 returns 0.0f when it is paused. In this case, VideoView2 will
- // use mFallbackSpeed instead.
- float fallbackSpeed = mMediaPlayer.getPlaybackParams().allowDefaults().getSpeed();
- if (fallbackSpeed > 0.0f) {
- mFallbackSpeed = fallbackSpeed;
- }
- mSpeed = mFallbackSpeed;
- }
- }
- }
-
- private boolean isRemotePlayback() {
- if (mMediaController == null) {
- return false;
- }
- PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo();
- return playbackInfo != null
- && playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE;
- }
-
- private void selectOrDeselectSubtitle(boolean select) {
- if (!isInPlaybackState()) {
- return;
- }
- /*
- if (select) {
- if (mSubtitleTrackIndices.size() > 0) {
- // TODO: make this selection dynamic
- mSelectedSubtitleTrackIndex = mSubtitleTrackIndices.get(0).first;
- mSubtitleController.selectTrack(mSubtitleTrackIndices.get(0).second);
- mMediaPlayer.selectTrack(mSelectedSubtitleTrackIndex);
- mSubtitleView.setVisibility(View.VISIBLE);
- }
- } else {
- if (mSelectedSubtitleTrackIndex != INVALID_TRACK_INDEX) {
- mMediaPlayer.deselectTrack(mSelectedSubtitleTrackIndex);
- mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX;
- mSubtitleView.setVisibility(View.GONE);
- }
- }
- */
- }
-
- private void extractTracks() {
- MediaPlayer.TrackInfo[] trackInfos = mMediaPlayer.getTrackInfo();
- mVideoTrackIndices = new ArrayList<>();
- mAudioTrackIndices = new ArrayList<>();
- /*
- mSubtitleTrackIndices = new ArrayList<>();
- mSubtitleController.reset();
- */
- for (int i = 0; i < trackInfos.length; ++i) {
- int trackType = trackInfos[i].getTrackType();
- if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO) {
- mVideoTrackIndices.add(i);
- } else if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO) {
- mAudioTrackIndices.add(i);
- /*
- } else if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE
- || trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT) {
- SubtitleTrack track = mSubtitleController.addTrack(trackInfos[i].getFormat());
- if (track != null) {
- mSubtitleTrackIndices.add(new Pair<>(i, track));
- }
- */
- }
- }
- // Select first tracks as default
- if (mVideoTrackIndices.size() > 0) {
- mSelectedVideoTrackIndex = 0;
- }
- if (mAudioTrackIndices.size() > 0) {
- mSelectedAudioTrackIndex = 0;
- }
- if (mVideoTrackIndices.size() == 0 && mAudioTrackIndices.size() > 0) {
- mIsMusicMediaType = true;
- }
-
- Bundle data = new Bundle();
- data.putInt(MediaControlView2.KEY_VIDEO_TRACK_COUNT, mVideoTrackIndices.size());
- data.putInt(MediaControlView2.KEY_AUDIO_TRACK_COUNT, mAudioTrackIndices.size());
- /*
- data.putInt(MediaControlView2.KEY_SUBTITLE_TRACK_COUNT, mSubtitleTrackIndices.size());
- if (mSubtitleTrackIndices.size() > 0) {
- selectOrDeselectSubtitle(mSubtitleEnabled);
- }
- */
- mMediaSession.sendSessionEvent(MediaControlView2.EVENT_UPDATE_TRACK_STATUS, data);
- }
-
- private void extractMetadata() {
- // Get and set duration and title values as MediaMetadata for MediaControlView2
- MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
- String title = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
- if (title != null) {
- mTitle = title;
- }
- builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mTitle);
- builder.putLong(
- MediaMetadataCompat.METADATA_KEY_DURATION, mMediaPlayer.getDuration());
-
- if (mMediaSession != null) {
- mMediaSession.setMetadata(builder.build());
- }
- }
-
- @SuppressWarnings("deprecation")
- private void extractAudioMetadata() {
- if (!mIsMusicMediaType) {
- return;
- }
-
- mResources = getResources();
- mManager = (WindowManager) getContext().getApplicationContext()
- .getSystemService(Context.WINDOW_SERVICE);
-
- byte[] album = mRetriever.getEmbeddedPicture();
- if (album != null) {
- Bitmap bitmap = BitmapFactory.decodeByteArray(album, 0, album.length);
- mMusicAlbumDrawable = new BitmapDrawable(bitmap);
-
- // TODO: replace with visualizer
- Palette.Builder builder = Palette.from(bitmap);
- builder.generate(new Palette.PaletteAsyncListener() {
- @Override
- public void onGenerated(Palette palette) {
- // TODO: add dominant color for default album image.
- mDominantColor = palette.getDominantColor(0);
- if (mMusicView != null) {
- mMusicView.setBackgroundColor(mDominantColor);
- }
- }
- });
- } else {
- mMusicAlbumDrawable = mResources.getDrawable(R.drawable.ic_default_album_image);
- }
-
- String title = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
- if (title != null) {
- mMusicTitleText = title;
- } else {
- mMusicTitleText = mResources.getString(R.string.mcv2_music_title_unknown_text);
- }
-
- String artist = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);
- if (artist != null) {
- mMusicArtistText = artist;
- } else {
- mMusicArtistText = mResources.getString(R.string.mcv2_music_artist_unknown_text);
- }
-
- // Send title and artist string to MediaControlView2
- MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
- builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mMusicTitleText);
- builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, mMusicArtistText);
- mMediaSession.setMetadata(builder.build());
-
- // Display Embedded mode as default
- removeView(mSurfaceView);
- removeView(mTextureView);
- inflateMusicView(R.layout.embedded_music);
- }
-
- private int retrieveOrientation() {
- DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
- int width = dm.widthPixels;
- int height = dm.heightPixels;
-
- return (height > width)
- ? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
- : ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
- }
-
- private void inflateMusicView(int layoutId) {
- removeView(mMusicView);
-
- LayoutInflater inflater = (LayoutInflater) getContext()
- .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- View v = inflater.inflate(layoutId, null);
- v.setBackgroundColor(mDominantColor);
-
- ImageView albumView = v.findViewById(R.id.album);
- if (albumView != null) {
- albumView.setImageDrawable(mMusicAlbumDrawable);
- }
-
- TextView titleView = v.findViewById(R.id.title);
- if (titleView != null) {
- titleView.setText(mMusicTitleText);
- }
-
- TextView artistView = v.findViewById(R.id.artist);
- if (artistView != null) {
- artistView.setText(mMusicArtistText);
- }
-
- mMusicView = v;
- addView(mMusicView, 0);
- }
-
- /*
- OnSubtitleDataListener mSubtitleListener =
- new OnSubtitleDataListener() {
- @Override
- public void onSubtitleData(MediaPlayer mp, SubtitleData data) {
- if (DEBUG) {
- Log.d(TAG, "onSubtitleData(): getTrackIndex: " + data.getTrackIndex()
- + ", getCurrentPosition: " + mp.getCurrentPosition()
- + ", getStartTimeUs(): " + data.getStartTimeUs()
- + ", diff: "
- + (data.getStartTimeUs() / 1000 - mp.getCurrentPosition())
- + "ms, getDurationUs(): " + data.getDurationUs());
-
- }
- final int index = data.getTrackIndex();
- if (index != mSelectedSubtitleTrackIndex) {
- Log.d(TAG, "onSubtitleData(): getTrackIndex: " + data.getTrackIndex()
- + ", selected track index: " + mSelectedSubtitleTrackIndex);
- return;
- }
- for (Pair<Integer, SubtitleTrack> p : mSubtitleTrackIndices) {
- if (p.first == index) {
- SubtitleTrack track = p.second;
- track.onData(data);
- }
- }
- }
- };
- */
-
- MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener =
- new MediaPlayer.OnVideoSizeChangedListener() {
- @Override
- public void onVideoSizeChanged(
- MediaPlayer mp, int width, int height) {
- if (DEBUG) {
- Log.d(TAG, "onVideoSizeChanged(): size: " + width + "/" + height);
- }
- mVideoWidth = mp.getVideoWidth();
- mVideoHeight = mp.getVideoHeight();
- if (DEBUG) {
- Log.d(TAG, "onVideoSizeChanged(): mVideoSize:" + mVideoWidth + "/"
- + mVideoHeight);
- }
- if (mVideoWidth != 0 && mVideoHeight != 0) {
- requestLayout();
- }
- }
- };
- MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() {
- @Override
- public void onPrepared(MediaPlayer mp) {
- if (DEBUG) {
- Log.d(TAG, "OnPreparedListener(). mCurrentState=" + mCurrentState
- + ", mTargetState=" + mTargetState);
- }
- mCurrentState = STATE_PREPARED;
- // Create and set playback state for MediaControlView2
- updatePlaybackState();
-
- // TODO: change this to send TrackInfos to MediaControlView2
- // TODO: create MediaSession when initializing VideoView2
- if (mMediaSession != null) {
- extractTracks();
- }
-
- if (mMediaControlView != null) {
- mMediaControlView.setEnabled(true);
- }
- int videoWidth = mp.getVideoWidth();
- int videoHeight = mp.getVideoHeight();
-
- // mSeekWhenPrepared may be changed after seekTo() call
- long seekToPosition = mSeekWhenPrepared;
- if (seekToPosition != 0) {
- mMediaController.getTransportControls().seekTo(seekToPosition);
- }
-
- if (videoWidth != 0 && videoHeight != 0) {
- if (videoWidth != mVideoWidth || videoHeight != mVideoHeight) {
- if (DEBUG) {
- Log.i(TAG, "OnPreparedListener() : ");
- Log.i(TAG, " video size: " + videoWidth + "/" + videoHeight);
- Log.i(TAG, " measuredSize: " + getMeasuredWidth() + "/"
- + getMeasuredHeight());
- Log.i(TAG, " viewSize: " + getWidth() + "/" + getHeight());
- }
- mVideoWidth = videoWidth;
- mVideoHeight = videoHeight;
- requestLayout();
- }
-
- if (needToStart()) {
- mMediaController.getTransportControls().play();
- }
- } else {
- // We don't know the video size yet, but should start anyway.
- // The video size might be reported to us later.
- if (needToStart()) {
- mMediaController.getTransportControls().play();
- }
- }
- // Get and set duration and title values as MediaMetadata for MediaControlView2
- MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
-
- // TODO: Get title via other public APIs.
- /*
- if (mMetadata != null && mMetadata.has(Metadata.TITLE)) {
- mTitle = mMetadata.getString(Metadata.TITLE);
- }
- */
- builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mTitle);
- builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, mMediaPlayer.getDuration());
-
- if (mMediaSession != null) {
- mMediaSession.setMetadata(builder.build());
-
- // TODO: merge this code with the above code when integrating with
- // MediaSession2.
- if (mNeedUpdateMediaType) {
- mMediaSession.sendSessionEvent(
- MediaControlView2.EVENT_UPDATE_MEDIA_TYPE_STATUS, mMediaTypeData);
- mNeedUpdateMediaType = false;
- }
- }
- }
- };
-
- MediaPlayer.OnSeekCompleteListener mSeekCompleteListener =
- new MediaPlayer.OnSeekCompleteListener() {
- @Override
- public void onSeekComplete(MediaPlayer mp) {
- updatePlaybackState();
- }
- };
-
- MediaPlayer.OnCompletionListener mCompletionListener = new MediaPlayer.OnCompletionListener() {
- @Override
- @SuppressWarnings("deprecation")
- public void onCompletion(MediaPlayer mp) {
- mCurrentState = STATE_PLAYBACK_COMPLETED;
- mTargetState = STATE_PLAYBACK_COMPLETED;
- updatePlaybackState();
- if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
- mAudioManager.abandonAudioFocus(null);
- }
- }
- };
-
- MediaPlayer.OnInfoListener mInfoListener = new MediaPlayer.OnInfoListener() {
- @Override
- public boolean onInfo(MediaPlayer mp, int what, int extra) {
- if (what == MediaPlayer.MEDIA_INFO_METADATA_UPDATE) {
- extractTracks();
- }
- return true;
- }
- };
-
- MediaPlayer.OnErrorListener mErrorListener = new MediaPlayer.OnErrorListener() {
- @Override
- public boolean onError(MediaPlayer mp, int frameworkErr, int implErr) {
- if (DEBUG) {
- Log.d(TAG, "Error: " + frameworkErr + "," + implErr);
- }
- mCurrentState = STATE_ERROR;
- mTargetState = STATE_ERROR;
- updatePlaybackState();
-
- if (mMediaControlView != null) {
- mMediaControlView.setVisibility(View.GONE);
- }
- return true;
- }
- };
-
- MediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener =
- new MediaPlayer.OnBufferingUpdateListener() {
- @Override
- public void onBufferingUpdate(MediaPlayer mp, int percent) {
- mCurrentBufferPercentage = percent;
- updatePlaybackState();
- }
- };
-
- private class MediaSessionCallback extends MediaSessionCompat.Callback {
- @Override
- public void onCommand(String command, Bundle args, ResultReceiver receiver) {
- if (isRemotePlayback()) {
- // TODO (b/77158231)
- // mRoutePlayer.onCommand(command, args, receiver);
- } else {
- switch (command) {
- case MediaControlView2.COMMAND_SHOW_SUBTITLE:
- /*
- int subtitleIndex = args.getInt(
- MediaControlView2.KEY_SELECTED_SUBTITLE_INDEX,
- INVALID_TRACK_INDEX);
- if (subtitleIndex != INVALID_TRACK_INDEX) {
- int subtitleTrackIndex = mSubtitleTrackIndices.get(subtitleIndex).first;
- if (subtitleTrackIndex != mSelectedSubtitleTrackIndex) {
- mSelectedSubtitleTrackIndex = subtitleTrackIndex;
- setSubtitleEnabled(true);
- }
- }
- */
- break;
- case MediaControlView2.COMMAND_HIDE_SUBTITLE:
- setSubtitleEnabled(false);
- break;
- case MediaControlView2.COMMAND_SET_FULLSCREEN:
- if (mFullScreenRequestListener != null) {
- mFullScreenRequestListener.onFullScreenRequest(
- VideoView2.this,
- args.getBoolean(MediaControlView2.ARGUMENT_KEY_FULLSCREEN));
- }
- break;
- case MediaControlView2.COMMAND_SELECT_AUDIO_TRACK:
- int audioIndex = args.getInt(MediaControlView2.KEY_SELECTED_AUDIO_INDEX,
- INVALID_TRACK_INDEX);
- if (audioIndex != INVALID_TRACK_INDEX) {
- int audioTrackIndex = mAudioTrackIndices.get(audioIndex);
- if (audioTrackIndex != mSelectedAudioTrackIndex) {
- mSelectedAudioTrackIndex = audioTrackIndex;
- mMediaPlayer.selectTrack(mSelectedAudioTrackIndex);
- }
- }
- break;
- case MediaControlView2.COMMAND_SET_PLAYBACK_SPEED:
- float speed = args.getFloat(
- MediaControlView2.KEY_PLAYBACK_SPEED, INVALID_SPEED);
- if (speed != INVALID_SPEED && speed != mSpeed) {
- setSpeed(speed);
- mSpeed = speed;
- }
- break;
- case MediaControlView2.COMMAND_MUTE:
- mVolumeLevel = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
- mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 0, 0);
- break;
- case MediaControlView2.COMMAND_UNMUTE:
- mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mVolumeLevel, 0);
- break;
- }
- }
- showController();
- }
-
- @Override
- public void onCustomAction(final String action, final Bundle extras) {
- mCustomActionListenerRecord.first.execute(new Runnable() {
- @Override
- public void run() {
- mCustomActionListenerRecord.second.onCustomAction(action, extras);
- }
- });
- showController();
- }
-
- @Override
- public void onPlay() {
- if (!isAudioGranted()) {
- requestAudioFocus(mAudioFocusType);
- }
-
- if ((isInPlaybackState() && mCurrentView.hasAvailableSurface()) || mIsMusicMediaType) {
- if (isRemotePlayback()) {
- // TODO (b/77158231)
- // mRoutePlayer.onPlay();
- } else {
- applySpeed();
- mMediaPlayer.start();
- mCurrentState = STATE_PLAYING;
- updatePlaybackState();
- }
- mCurrentState = STATE_PLAYING;
- }
- mTargetState = STATE_PLAYING;
- if (DEBUG) {
- Log.d(TAG, "onPlay(). mCurrentState=" + mCurrentState
- + ", mTargetState=" + mTargetState);
- }
- showController();
- }
-
- @Override
- public void onPause() {
- if (isInPlaybackState()) {
- if (isRemotePlayback()) {
- // TODO (b/77158231)
- // mRoutePlayer.onPause();
- mCurrentState = STATE_PAUSED;
- } else if (mMediaPlayer.isPlaying()) {
- mMediaPlayer.pause();
- mCurrentState = STATE_PAUSED;
- updatePlaybackState();
- }
- }
- mTargetState = STATE_PAUSED;
- if (DEBUG) {
- Log.d(TAG, "onPause(). mCurrentState=" + mCurrentState
- + ", mTargetState=" + mTargetState);
- }
- showController();
- }
-
- @Override
- public void onSeekTo(long pos) {
- if (isInPlaybackState()) {
- if (isRemotePlayback()) {
- // TODO (b/77158231)
- // mRoutePlayer.onSeekTo(pos);
- } else {
- // TODO Refactor VideoView2 with FooImplBase and FooImplApiXX.
- if (android.os.Build.VERSION.SDK_INT < 26) {
- mMediaPlayer.seekTo((int) pos);
- } else {
- mMediaPlayer.seekTo(pos, MediaPlayer.SEEK_PREVIOUS_SYNC);
- }
- mSeekWhenPrepared = 0;
- }
- } else {
- mSeekWhenPrepared = pos;
- }
- showController();
- }
-
- @Override
- public void onStop() {
- if (isRemotePlayback()) {
- // TODO (b/77158231)
- // mRoutePlayer.onStop();
- } else {
- resetPlayer();
- }
- showController();
- }
- }
}
diff --git a/media-widget/src/main/java/androidx/media/widget/VideoView2Impl.java b/media-widget/src/main/java/androidx/media/widget/VideoView2Impl.java
new file mode 100644
index 0000000..7632490
--- /dev/null
+++ b/media-widget/src/main/java/androidx/media/widget/VideoView2Impl.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.widget;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.media.AudioAttributesCompat;
+import androidx.media.DataSourceDesc;
+import androidx.media.MediaItem2;
+import androidx.media.MediaMetadata2;
+import androidx.media.SessionToken2;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * Interface for impl classes.
+ */
+interface VideoView2Impl {
+ void initialize(
+ VideoView2 instance, Context context,
+ @Nullable AttributeSet attrs, int defStyleAttr);
+
+ /**
+ * Sets MediaControlView2 instance. It will replace the previously assigned MediaControlView2
+ * instance if any.
+ *
+ * @param mediaControlView a media control view2 instance.
+ * @param intervalMs a time interval in milliseconds until VideoView2 hides MediaControlView2.
+ */
+ void setMediaControlView2(MediaControlView2 mediaControlView, long intervalMs);
+
+ /**
+ * Returns MediaControlView2 instance which is currently attached to VideoView2 by default or by
+ * {@link #setMediaControlView2} method.
+ */
+ MediaControlView2 getMediaControlView2();
+
+ /**
+ * Sets MediaMetadata2 instance. It will replace the previously assigned MediaMetadata2 instance
+ * if any.
+ *
+ * @param metadata a MediaMetadata2 instance.
+ */
+ void setMediaMetadata(MediaMetadata2 metadata);
+
+ /**
+ * Returns MediaMetadata2 instance which is retrieved from MediaPlayer inside VideoView2 by
+ * default or by {@link #setMediaMetadata} method.
+ */
+ MediaMetadata2 getMediaMetadata();
+
+ /**
+ * Returns MediaController instance which is connected with MediaSession that VideoView2 is
+ * using. This method should be called when VideoView2 is attached to window, or it throws
+ * IllegalStateException, since internal MediaSession instance is not available until
+ * this view is attached to window. Please check {@link View#isAttachedToWindow}
+ * before calling this method.
+ *
+ * @throws IllegalStateException if interal MediaSession is not created yet.
+ */
+ MediaControllerCompat getMediaController();
+
+ /**
+ * Returns {@link SessionToken2} so that developers create their own
+ * {@link androidx.media.MediaController2} instance. This method should be called when
+ * VideoView2 is attached to window, or it throws IllegalStateException.
+ *
+ * @throws IllegalStateException if interal MediaSession is not created yet.
+ */
+ SessionToken2 getMediaSessionToken();
+
+ /**
+ * Shows or hides closed caption or subtitles if there is any.
+ * The first subtitle track will be chosen if there multiple subtitle tracks exist.
+ * Default behavior of VideoView2 is not showing subtitle.
+ * @param enable shows closed caption or subtitles if this value is true, or hides.
+ */
+ void setSubtitleEnabled(boolean enable);
+
+ /**
+ * Returns true if showing subtitle feature is enabled or returns false.
+ * Although there is no subtitle track or closed caption, it can return true, if the feature
+ * has been enabled by {@link #setSubtitleEnabled}.
+ */
+ boolean isSubtitleEnabled();
+
+ /**
+ * Sets playback speed.
+ *
+ * It is expressed as a multiplicative factor, where normal speed is 1.0f. If it is less than
+ * or equal to zero, it will be just ignored and nothing will be changed. If it exceeds the
+ * maximum speed that internal engine supports, system will determine best handling or it will
+ * be reset to the normal speed 1.0f.
+ * @param speed the playback speed. It should be positive.
+ */
+ void setSpeed(float speed);
+
+ /**
+ * Returns playback speed.
+ *
+ * It returns the same value that has been set by {@link #setSpeed}, if it was available value.
+ * If {@link #setSpeed} has not been called before, then the normal speed 1.0f will be returned.
+ */
+ float getSpeed();
+
+ /**
+ * Sets which type of audio focus will be requested during the playback, or configures playback
+ * to not request audio focus. Valid values for focus requests are
+ * {@link AudioManager#AUDIOFOCUS_GAIN}, {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT},
+ * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK}, and
+ * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}. Or use
+ * {@link AudioManager#AUDIOFOCUS_NONE} to express that audio focus should not be
+ * requested when playback starts. You can for instance use this when playing a silent animation
+ * through this class, and you don't want to affect other audio applications playing in the
+ * background.
+ *
+ * @param focusGain the type of audio focus gain that will be requested, or
+ * {@link AudioManager#AUDIOFOCUS_NONE} to disable the use audio focus during
+ * playback.
+ */
+ void setAudioFocusRequest(int focusGain);
+
+ /**
+ * Sets the {@link AudioAttributesCompat} to be used during the playback of the video.
+ *
+ * @param attributes non-null <code>AudioAttributesCompat</code>.
+ */
+ void setAudioAttributes(@NonNull AudioAttributesCompat attributes);
+
+ /**
+ * Sets video path.
+ *
+ * @param path the path of the video.
+ */
+ void setVideoPath(String path);
+
+ /**
+ * Sets video URI.
+ *
+ * @param uri the URI of the video.
+ */
+ void setVideoUri(Uri uri);
+
+ /**
+ * Sets video URI using specific headers.
+ *
+ * @param uri the URI of the video.
+ * @param headers the headers for the URI request.
+ * Note that the cross domain redirection is allowed by default, but that can be
+ * changed with key/value pairs through the headers parameter with
+ * "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
+ * to disallow or allow cross domain redirection.
+ */
+ void setVideoUri(Uri uri, @Nullable Map<String, String> headers);
+
+ /**
+ * Sets {@link MediaItem2} object to render using VideoView2. Alternative way to set media
+ * object to VideoView2 is {@link #setDataSource}.
+ * @param mediaItem the MediaItem2 to play
+ * @see #setDataSource
+ */
+ void setMediaItem(@NonNull MediaItem2 mediaItem);
+
+ /**
+ * Sets {@link DataSourceDesc} object to render using VideoView2.
+ * @param dataSource the {@link DataSourceDesc} object to play.
+ * @see #setMediaItem
+ */
+ void setDataSource(@NonNull DataSourceDesc dataSource);
+
+ /**
+ * Selects which view will be used to render video between SurfacView and TextureView.
+ *
+ * @param viewType the view type to render video
+ * <ul>
+ * <li>{@link #VideoView2.VIEW_TYPE_SURFACEVIEW}
+ * <li>{@link #VideoView2.VIEW_TYPE_TEXTUREVIEW}
+ * </ul>
+ */
+ void setViewType(@VideoView2.ViewType int viewType);
+
+ /**
+ * Returns view type.
+ *
+ * @return view type. See {@see setViewType}.
+ */
+ @VideoView2.ViewType
+ int getViewType();
+
+ /**
+ * Sets custom actions which will be shown as custom buttons in {@link MediaControlView2}.
+ *
+ * @param actionList A list of {@link PlaybackStateCompat.CustomAction}. The return value of
+ * {@link PlaybackStateCompat.CustomAction#getIcon()} will be used to draw
+ * buttons in {@link MediaControlView2}.
+ * @param executor executor to run callbacks on.
+ * @param listener A listener to be called when a custom button is clicked.
+ */
+ void setCustomActions(List<PlaybackStateCompat.CustomAction> actionList,
+ Executor executor, VideoView2.OnCustomActionListener listener);
+
+ /**
+ * Registers a callback to be invoked when a view type change is done.
+ * {@see #setViewType(int)}
+ * @param l The callback that will be run
+ */
+ void setOnViewTypeChangedListener(VideoView2.OnViewTypeChangedListener l);
+
+ void onAttachedToWindowImpl();
+
+ void onDetachedFromWindowImpl();
+
+ void onTouchEventImpl(MotionEvent ev);
+
+ void onTrackballEventImpl(MotionEvent ev);
+
+ void onMeasureImpl(int widthMeasureSpec, int heightMeasureSpec);
+}
diff --git a/media-widget/src/main/java/androidx/media/widget/VideoSurfaceView.java b/media-widget/src/main/java/androidx/media/widget/impl/VideoSurfaceView.java
similarity index 76%
copy from media-widget/src/main/java/androidx/media/widget/VideoSurfaceView.java
copy to media-widget/src/main/java/androidx/media/widget/impl/VideoSurfaceView.java
index d417bd2..5c140c8 100644
--- a/media-widget/src/main/java/androidx/media/widget/VideoSurfaceView.java
+++ b/media-widget/src/main/java/androidx/media/widget/impl/VideoSurfaceView.java
@@ -20,59 +20,44 @@
import android.content.Context;
import android.graphics.Rect;
-import android.media.MediaPlayer;
-import android.util.AttributeSet;
import android.util.Log;
+import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import androidx.media.MediaPlayer2;
@RequiresApi(21)
-class VideoSurfaceView extends SurfaceView implements VideoViewInterface, SurfaceHolder.Callback {
+class VideoSurfaceView extends SurfaceView
+ implements VideoViewInterface, SurfaceHolder.Callback {
private static final String TAG = "VideoSurfaceView";
- private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
- private SurfaceHolder mSurfaceHolder = null;
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+ private Surface mSurface = null;
private SurfaceListener mSurfaceListener = null;
- private MediaPlayer mMediaPlayer;
+ private MediaPlayer2 mMediaPlayer;
// A flag to indicate taking over other view should be proceed.
private boolean mIsTakingOverOldView;
private VideoViewInterface mOldView;
-
VideoSurfaceView(Context context) {
- this(context, null);
- }
-
- VideoSurfaceView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- getHolder().addCallback(this);
- }
-
- @RequiresApi(21)
- VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr,
- int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
+ super(context, null);
getHolder().addCallback(this);
}
////////////////////////////////////////////////////
- // implements VideoViewInterface
+ // implements VideoViewInterfaceWithMp1
////////////////////////////////////////////////////
@Override
- public boolean assignSurfaceToMediaPlayer(MediaPlayer mp) {
- Log.d(TAG, "assignSurfaceToMediaPlayer(): mSurfaceHolder: " + mSurfaceHolder);
+ public boolean assignSurfaceToMediaPlayer(MediaPlayer2 mp) {
+ Log.d(TAG, "assignSurfaceToMediaPlayer(): mSurface: " + mSurface);
if (mp == null || !hasAvailableSurface()) {
return false;
}
- mp.setDisplay(mSurfaceHolder);
+ mp.setSurface(mSurface);
return true;
}
@@ -87,7 +72,7 @@
}
@Override
- public void setMediaPlayer(MediaPlayer mp) {
+ public void setMediaPlayer(MediaPlayer2 mp) {
mMediaPlayer = mp;
if (mIsTakingOverOldView) {
takeOver(mOldView);
@@ -111,7 +96,7 @@
@Override
public boolean hasAvailableSurface() {
- return (mSurfaceHolder != null && mSurfaceHolder.getSurface() != null);
+ return mSurface != null && mSurface.isValid();
}
////////////////////////////////////////////////////
@@ -120,8 +105,8 @@
@Override
public void surfaceCreated(SurfaceHolder holder) {
- Log.d(TAG, "surfaceCreated: mSurfaceHolder: " + mSurfaceHolder + ", new holder: " + holder);
- mSurfaceHolder = holder;
+ Log.d(TAG, "surfaceCreated: mSurface: " + mSurface + ", new : " + holder.getSurface());
+ mSurface = holder.getSurface();
if (mIsTakingOverOldView) {
takeOver(mOldView);
} else {
@@ -129,7 +114,7 @@
}
if (mSurfaceListener != null) {
- Rect rect = mSurfaceHolder.getSurfaceFrame();
+ Rect rect = holder.getSurfaceFrame();
mSurfaceListener.onSurfaceCreated(this, rect.width(), rect.height());
}
}
@@ -144,13 +129,12 @@
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
// After we return from this we can't use the surface any more
- mSurfaceHolder = null;
+ mSurface = null;
if (mSurfaceListener != null) {
mSurfaceListener.onSurfaceDestroyed(this);
}
}
- // TODO: Investigate the way to move onMeasure() code into FrameLayout.
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int videoWidth = (mMediaPlayer == null) ? 0 : mMediaPlayer.getVideoWidth();
@@ -194,10 +178,4 @@
Log.i(TAG, " measuredSize: " + getMeasuredWidth() + "/" + getMeasuredHeight());
}
}
-
- @Override
- public String toString() {
- return "ViewType: SurfaceView / Visibility: " + getVisibility()
- + " / surfaceHolder: " + mSurfaceHolder;
- }
}
diff --git a/media-widget/src/main/java/androidx/media/widget/VideoTextureView.java b/media-widget/src/main/java/androidx/media/widget/impl/VideoTextureView.java
similarity index 87%
rename from media-widget/src/main/java/androidx/media/widget/VideoTextureView.java
rename to media-widget/src/main/java/androidx/media/widget/impl/VideoTextureView.java
index cdc833b..3726047 100644
--- a/media-widget/src/main/java/androidx/media/widget/VideoTextureView.java
+++ b/media-widget/src/main/java/androidx/media/widget/impl/VideoTextureView.java
@@ -20,8 +20,6 @@
import android.content.Context;
import android.graphics.SurfaceTexture;
-import android.media.MediaPlayer;
-import android.util.AttributeSet;
import android.util.Log;
import android.view.Surface;
import android.view.TextureView;
@@ -29,44 +27,32 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
+import androidx.media.MediaPlayer2;
@RequiresApi(21)
class VideoTextureView extends TextureView
implements VideoViewInterface, TextureView.SurfaceTextureListener {
- private static final String TAG = "VideoTextureView";
- private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
+ private static final String TAG = "VideoTextureViewWithMp1";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private Surface mSurface;
private SurfaceListener mSurfaceListener;
- private MediaPlayer mMediaPlayer;
+ private MediaPlayer2 mMediaPlayer;
// A flag to indicate taking over other view should be proceed.
private boolean mIsTakingOverOldView;
private VideoViewInterface mOldView;
VideoTextureView(Context context) {
- this(context, null);
- }
-
- VideoTextureView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- VideoTextureView(Context context, AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
- }
-
- VideoTextureView(
- Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
+ super(context, null);
setSurfaceTextureListener(this);
}
////////////////////////////////////////////////////
- // implements VideoViewInterface
+ // implements VideoViewInterfaceWithMp1
////////////////////////////////////////////////////
@Override
- public boolean assignSurfaceToMediaPlayer(MediaPlayer mp) {
+ public boolean assignSurfaceToMediaPlayer(MediaPlayer2 mp) {
if (mp == null || !hasAvailableSurface()) {
// Surface is not ready.
return false;
@@ -86,7 +72,7 @@
}
@Override
- public void setMediaPlayer(MediaPlayer mp) {
+ public void setMediaPlayer(MediaPlayer2 mp) {
mMediaPlayer = mp;
if (mIsTakingOverOldView) {
takeOver(mOldView);
diff --git a/media-widget/src/main/java/androidx/media/widget/impl/VideoView2ImplBase.java b/media-widget/src/main/java/androidx/media/widget/impl/VideoView2ImplBase.java
new file mode 100644
index 0000000..68477f8
--- /dev/null
+++ b/media-widget/src/main/java/androidx/media/widget/impl/VideoView2ImplBase.java
@@ -0,0 +1,1570 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.widget;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Point;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.AudioAttributes;
+import android.media.AudioFocusRequest;
+import android.media.AudioManager;
+import android.media.MediaMetadataRetriever;
+import android.media.PlaybackParams;
+import android.media.SubtitleData;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaControllerCompat.PlaybackInfo;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.media.AudioAttributesCompat;
+import androidx.media.DataSourceDesc;
+import androidx.media.MediaItem2;
+import androidx.media.MediaMetadata2;
+import androidx.media.MediaPlayer2;
+import androidx.media.MediaPlayer2.MediaPlayer2EventCallback;
+import androidx.media.SessionToken2;
+import androidx.media.subtitle.ClosedCaptionRenderer;
+import androidx.media.subtitle.SubtitleController;
+import androidx.media.subtitle.SubtitleTrack;
+import androidx.mediarouter.media.MediaControlIntent;
+import androidx.mediarouter.media.MediaItemStatus;
+import androidx.mediarouter.media.MediaRouteSelector;
+import androidx.mediarouter.media.MediaRouter;
+import androidx.palette.graphics.Palette;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * Base implementation of VideoView2.
+ */
+@RequiresApi(28) // TODO correct minSdk API use incompatibilities and remove before release.
+class VideoView2ImplBase implements VideoView2Impl, VideoViewInterface.SurfaceListener {
+ private static final String TAG = "VideoView2ImplBase";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+ private static final long DEFAULT_SHOW_CONTROLLER_INTERVAL_MS = 2000;
+
+ private static final int STATE_ERROR = -1;
+ private static final int STATE_IDLE = 0;
+ private static final int STATE_PREPARING = 1;
+ private static final int STATE_PREPARED = 2;
+ private static final int STATE_PLAYING = 3;
+ private static final int STATE_PAUSED = 4;
+ private static final int STATE_PLAYBACK_COMPLETED = 5;
+
+ private static final int INVALID_TRACK_INDEX = -1;
+ private static final float INVALID_SPEED = 0f;
+
+ private static final int SIZE_TYPE_EMBEDDED = 0;
+ private static final int SIZE_TYPE_FULL = 1;
+ private static final int SIZE_TYPE_MINIMAL = 2;
+
+ private AccessibilityManager mAccessibilityManager;
+ private AudioManager mAudioManager;
+ private AudioAttributesCompat mAudioAttributes;
+ private int mAudioFocusType = AudioManager.AUDIOFOCUS_GAIN; // legacy focus gain
+ private boolean mAudioFocused = false;
+
+ private Pair<Executor, VideoView2.OnCustomActionListener> mCustomActionListenerRecord;
+ private VideoView2.OnViewTypeChangedListener mViewTypeChangedListener;
+
+ private VideoViewInterface mCurrentView;
+ private VideoTextureView mTextureView;
+ private VideoSurfaceView mSurfaceView;
+
+ private MediaPlayer2 mMediaPlayer;
+ private DataSourceDesc mDsd;
+ private Uri mUri;
+ private Map<String, String> mHeaders;
+ private MediaControlView2 mMediaControlView;
+ private MediaSessionCompat mMediaSession;
+ private MediaControllerCompat mMediaController;
+ private MediaMetadata2 mMediaMetadata;
+ private MediaMetadataRetriever mRetriever;
+ private boolean mNeedUpdateMediaType;
+ private Bundle mMediaTypeData;
+ private String mTitle;
+
+ private WindowManager mManager;
+ private Resources mResources;
+ private View mMusicView;
+ private Drawable mMusicAlbumDrawable;
+ private String mMusicTitleText;
+ private String mMusicArtistText;
+ private boolean mIsMusicMediaType;
+ private int mPrevWidth;
+ private int mPrevHeight;
+ private int mDominantColor;
+ private int mSizeType;
+
+ private PlaybackStateCompat.Builder mStateBuilder;
+ private List<PlaybackStateCompat.CustomAction> mCustomActionList;
+
+ private int mTargetState = STATE_IDLE;
+ private int mCurrentState = STATE_IDLE;
+ private int mCurrentBufferPercentage;
+ private long mSeekWhenPrepared; // recording the seek position while preparing
+
+ private int mVideoWidth;
+ private int mVideoHeight;
+
+ private ArrayList<Integer> mVideoTrackIndices;
+ private ArrayList<Integer> mAudioTrackIndices;
+ private ArrayList<Pair<Integer, SubtitleTrack>> mSubtitleTrackIndices;
+ private SubtitleController mSubtitleController;
+
+ // selected video/audio/subtitle track index as MediaPlayer returns
+ private int mSelectedVideoTrackIndex;
+ private int mSelectedAudioTrackIndex;
+ private int mSelectedSubtitleTrackIndex;
+
+ private SubtitleView mSubtitleView;
+ private boolean mSubtitleEnabled;
+
+ private float mSpeed;
+ private float mFallbackSpeed; // keep the original speed before 'pause' is called.
+ private float mVolumeLevelFloat;
+ private int mVolumeLevel;
+ private VideoView2 mInstance;
+
+ private long mShowControllerIntervalMs;
+
+ private MediaRouter mMediaRouter;
+ private MediaRouteSelector mRouteSelector;
+ private MediaRouter.RouteInfo mRoute;
+ private RoutePlayer mRoutePlayer;
+
+ private final MediaRouter.Callback mRouterCallback = new MediaRouter.Callback() {
+ @Override
+ public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) {
+ if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
+ // Stop local playback (if necessary)
+ resetPlayer();
+ mRoute = route;
+ mRoutePlayer = new RoutePlayer(mInstance.getContext(), route);
+ mRoutePlayer.setPlayerEventCallback(new RoutePlayer.PlayerEventCallback() {
+ @Override
+ public void onPlayerStateChanged(MediaItemStatus itemStatus) {
+ PlaybackStateCompat.Builder psBuilder = new PlaybackStateCompat.Builder();
+ psBuilder.setActions(RoutePlayer.PLAYBACK_ACTIONS);
+ long position = itemStatus.getContentPosition();
+ switch (itemStatus.getPlaybackState()) {
+ case MediaItemStatus.PLAYBACK_STATE_PENDING:
+ psBuilder.setState(PlaybackStateCompat.STATE_NONE, position, 0);
+ mCurrentState = STATE_IDLE;
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_PLAYING:
+ psBuilder.setState(PlaybackStateCompat.STATE_PLAYING, position, 1);
+ mCurrentState = STATE_PLAYING;
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_PAUSED:
+ psBuilder.setState(PlaybackStateCompat.STATE_PAUSED, position, 0);
+ mCurrentState = STATE_PAUSED;
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_BUFFERING:
+ psBuilder.setState(
+ PlaybackStateCompat.STATE_BUFFERING, position, 0);
+ mCurrentState = STATE_PAUSED;
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_FINISHED:
+ psBuilder.setState(PlaybackStateCompat.STATE_STOPPED, position, 0);
+ mCurrentState = STATE_PLAYBACK_COMPLETED;
+ break;
+ }
+
+ PlaybackStateCompat pbState = psBuilder.build();
+ mMediaSession.setPlaybackState(pbState);
+
+ MediaMetadataCompat.Builder mmBuilder = new MediaMetadataCompat.Builder();
+ mmBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION,
+ itemStatus.getContentDuration());
+ mMediaSession.setMetadata(mmBuilder.build());
+ }
+ });
+ // Start remote playback (if necessary)
+ // TODO: b/77556429
+ mRoutePlayer.openVideo(mUri);
+ }
+ }
+
+ @Override
+ public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route, int reason) {
+ if (mRoute != null && mRoutePlayer != null) {
+ mRoutePlayer.release();
+ mRoutePlayer = null;
+ }
+ if (mRoute == route) {
+ mRoute = null;
+ }
+ if (reason != MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) {
+ // TODO: Resume local playback (if necessary)
+ // TODO: b/77556429
+ openVideo(mDsd);
+ }
+ }
+ };
+
+ @Override
+ public void initialize(
+ VideoView2 instance, Context context,
+ @Nullable AttributeSet attrs, int defStyleAttr) {
+ mInstance = instance;
+
+ mVideoWidth = 0;
+ mVideoHeight = 0;
+ mSpeed = 1.0f;
+ mFallbackSpeed = mSpeed;
+ mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX;
+ mShowControllerIntervalMs = DEFAULT_SHOW_CONTROLLER_INTERVAL_MS;
+
+ mAccessibilityManager = (AccessibilityManager) context.getSystemService(
+ Context.ACCESSIBILITY_SERVICE);
+
+ mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ mAudioAttributes = new AudioAttributesCompat.Builder()
+ .setUsage(AudioAttributesCompat.USAGE_MEDIA)
+ .setContentType(AudioAttributesCompat.CONTENT_TYPE_MOVIE).build();
+
+ mInstance.setFocusable(true);
+ mInstance.setFocusableInTouchMode(true);
+ mInstance.requestFocus();
+
+ mTextureView = new VideoTextureView(context);
+ mSurfaceView = new VideoSurfaceView(context);
+ LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT);
+ mTextureView.setLayoutParams(params);
+ mSurfaceView.setLayoutParams(params);
+ mTextureView.setSurfaceListener(this);
+ mSurfaceView.setSurfaceListener(this);
+
+ mInstance.addView(mTextureView);
+ mInstance.addView(mSurfaceView);
+
+ mSubtitleView = new SubtitleView(context);
+ mSubtitleView.setLayoutParams(params);
+ mSubtitleView.setBackgroundColor(0);
+ mInstance.addView(mSubtitleView);
+
+ boolean enableControlView = (attrs == null) || attrs.getAttributeBooleanValue(
+ "http://schemas.android.com/apk/res/android",
+ "enableControlView", true);
+ if (enableControlView) {
+ mMediaControlView = new MediaControlView2(context);
+ }
+
+ mSubtitleEnabled = (attrs == null) || attrs.getAttributeBooleanValue(
+ "http://schemas.android.com/apk/res/android",
+ "enableSubtitle", false);
+
+ // Choose surface view by default
+ int viewType = (attrs == null) ? VideoView2.VIEW_TYPE_SURFACEVIEW
+ : attrs.getAttributeIntValue(
+ "http://schemas.android.com/apk/res/android",
+ "viewType", VideoView2.VIEW_TYPE_SURFACEVIEW);
+ if (viewType == VideoView2.VIEW_TYPE_SURFACEVIEW) {
+ Log.d(TAG, "viewType attribute is surfaceView.");
+ mTextureView.setVisibility(View.GONE);
+ mSurfaceView.setVisibility(View.VISIBLE);
+ mCurrentView = mSurfaceView;
+ } else if (viewType == VideoView2.VIEW_TYPE_TEXTUREVIEW) {
+ Log.d(TAG, "viewType attribute is textureView.");
+ mTextureView.setVisibility(View.VISIBLE);
+ mSurfaceView.setVisibility(View.GONE);
+ mCurrentView = mTextureView;
+ }
+
+ MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
+ builder.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
+ builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
+ mRouteSelector = builder.build();
+ }
+
+ /**
+ * Sets MediaControlView2 instance. It will replace the previously assigned MediaControlView2
+ * instance if any.
+ *
+ * @param mediaControlView a media control view2 instance.
+ * @param intervalMs a time interval in milliseconds until VideoView2 hides MediaControlView2.
+ */
+ @Override
+ public void setMediaControlView2(MediaControlView2 mediaControlView, long intervalMs) {
+ mMediaControlView = mediaControlView;
+ mShowControllerIntervalMs = intervalMs;
+ mMediaControlView.setRouteSelector(mRouteSelector);
+
+ if (mInstance.isAttachedToWindow()) {
+ attachMediaControlView();
+ }
+ }
+
+ /**
+ * Returns MediaControlView2 instance which is currently attached to VideoView2 by default or by
+ * {@link #setMediaControlView2} method.
+ */
+ @Override
+ public MediaControlView2 getMediaControlView2() {
+ return mMediaControlView;
+ }
+
+ /**
+ * Sets MediaMetadata2 instance. It will replace the previously assigned MediaMetadata2 instance
+ * if any.
+ *
+ * @param metadata a MediaMetadata2 instance.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void setMediaMetadata(MediaMetadata2 metadata) {
+ //mProvider.setMediaMetadata_impl(metadata);
+ }
+
+ /**
+ * Returns MediaMetadata2 instance which is retrieved from MediaPlayer inside VideoView2 by
+ * default or by {@link #setMediaMetadata} method.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public MediaMetadata2 getMediaMetadata() {
+ return mMediaMetadata;
+ }
+
+ /**
+ * Returns MediaController instance which is connected with MediaSession that VideoView2 is
+ * using. This method should be called when VideoView2 is attached to window, or it throws
+ * IllegalStateException, since internal MediaSession instance is not available until
+ * this view is attached to window. Please check {@link View#isAttachedToWindow}
+ * before calling this method.
+ *
+ * @throws IllegalStateException if interal MediaSession is not created yet.
+ * @hide TODO: remove
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public MediaControllerCompat getMediaController() {
+ if (mMediaSession == null) {
+ throw new IllegalStateException("MediaSession instance is not available.");
+ }
+ return mMediaController;
+ }
+
+ /**
+ * Returns {@link SessionToken2} so that developers create their own
+ * {@link androidx.media.MediaController2} instance. This method should be called when
+ * VideoView2 is attached to window, or it throws IllegalStateException.
+ *
+ * @throws IllegalStateException if interal MediaSession is not created yet.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public SessionToken2 getMediaSessionToken() {
+ //return mProvider.getMediaSessionToken_impl();
+ return null;
+ }
+
+ /**
+ * Shows or hides closed caption or subtitles if there is any.
+ * The first subtitle track will be chosen if there multiple subtitle tracks exist.
+ * Default behavior of VideoView2 is not showing subtitle.
+ * @param enable shows closed caption or subtitles if this value is true, or hides.
+ */
+ @Override
+ public void setSubtitleEnabled(boolean enable) {
+ // No-op on API < 28
+ }
+
+ /**
+ * Returns true if showing subtitle feature is enabled or returns false.
+ * Although there is no subtitle track or closed caption, it can return true, if the feature
+ * has been enabled by {@link #setSubtitleEnabled}.
+ */
+ @Override
+ public boolean isSubtitleEnabled() {
+ // Not supported on API < 28
+ return false;
+ }
+
+ /**
+ * Sets playback speed.
+ *
+ * It is expressed as a multiplicative factor, where normal speed is 1.0f. If it is less than
+ * or equal to zero, it will be just ignored and nothing will be changed. If it exceeds the
+ * maximum speed that internal engine supports, system will determine best handling or it will
+ * be reset to the normal speed 1.0f.
+ * @param speed the playback speed. It should be positive.
+ */
+ @Override
+ public void setSpeed(float speed) {
+ if (speed <= 0.0f) {
+ Log.e(TAG, "Unsupported speed (" + speed + ") is ignored.");
+ return;
+ }
+ mSpeed = speed;
+ if (isPlaying()) {
+ applySpeed();
+ }
+ updatePlaybackState();
+ }
+
+ /**
+ * Returns playback speed.
+ *
+ * It returns the same value that has been set by {@link #setSpeed}, if it was available value.
+ * If {@link #setSpeed} has not been called before, then the normal speed 1.0f will be returned.
+ */
+ @Override
+ public float getSpeed() {
+ return mSpeed;
+ }
+
+ /**
+ * Sets which type of audio focus will be requested during the playback, or configures playback
+ * to not request audio focus. Valid values for focus requests are
+ * {@link AudioManager#AUDIOFOCUS_GAIN}, {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT},
+ * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK}, and
+ * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}. Or use
+ * {@link AudioManager#AUDIOFOCUS_NONE} to express that audio focus should not be
+ * requested when playback starts. You can for instance use this when playing a silent animation
+ * through this class, and you don't want to affect other audio applications playing in the
+ * background.
+ *
+ * @param focusGain the type of audio focus gain that will be requested, or
+ * {@link AudioManager#AUDIOFOCUS_NONE} to disable the use audio focus during
+ * playback.
+ */
+ @Override
+ public void setAudioFocusRequest(int focusGain) {
+ if (focusGain != AudioManager.AUDIOFOCUS_NONE
+ && focusGain != AudioManager.AUDIOFOCUS_GAIN
+ && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
+ && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
+ && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) {
+ throw new IllegalArgumentException("Illegal audio focus type " + focusGain);
+ }
+ mAudioFocusType = focusGain;
+ }
+
+ /**
+ * Sets the {@link AudioAttributesCompat} to be used during the playback of the video.
+ *
+ * @param attributes non-null <code>AudioAttributesCompat</code>.
+ */
+ @Override
+ public void setAudioAttributes(@NonNull AudioAttributesCompat attributes) {
+ if (attributes == null) {
+ throw new IllegalArgumentException("Illegal null AudioAttributes");
+ }
+ mAudioAttributes = attributes;
+ }
+
+ /**
+ * Sets video path.
+ *
+ * @param path the path of the video.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void setVideoPath(String path) {
+ setVideoUri(Uri.parse(path));
+ }
+
+ /**
+ * Sets video URI.
+ *
+ * @param uri the URI of the video.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void setVideoUri(Uri uri) {
+ setVideoUri(uri, null);
+ }
+
+ /**
+ * Sets video URI using specific headers.
+ *
+ * @param uri the URI of the video.
+ * @param headers the headers for the URI request.
+ * Note that the cross domain redirection is allowed by default, but that can be
+ * changed with key/value pairs through the headers parameter with
+ * "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
+ * to disallow or allow cross domain redirection.
+ */
+ @Override
+ public void setVideoUri(Uri uri, @Nullable Map<String, String> headers) {
+ DataSourceDesc.Builder builder = new DataSourceDesc.Builder();
+ builder.setDataSource(mInstance.getContext(), uri, headers, null);
+ setDataSource(builder.build());
+ }
+
+ /**
+ * Sets {@link MediaItem2} object to render using VideoView2. Alternative way to set media
+ * object to VideoView2 is {@link #setDataSource}.
+ * @param mediaItem the MediaItem2 to play
+ * @see #setDataSource
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void setMediaItem(@NonNull MediaItem2 mediaItem) {
+ }
+
+ /**
+ * Sets {@link DataSourceDesc} object to render using VideoView2.
+ * @param dataSource the {@link DataSourceDesc} object to play.
+ * @see #setMediaItem
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void setDataSource(@NonNull DataSourceDesc dataSource) {
+ mDsd = dataSource;
+ mSeekWhenPrepared = 0;
+ openVideo(dataSource);
+ }
+
+ /**
+ * Selects which view will be used to render video between SurfacView and TextureView.
+ *
+ * @param viewType the view type to render video
+ * <ul>
+ * <li>{@link #VideoView2.VIEW_TYPE_SURFACEVIEW}
+ * <li>{@link #VideoView2.VIEW_TYPE_TEXTUREVIEW}
+ * </ul>
+ */
+ @Override
+ public void setViewType(@VideoView2.ViewType int viewType) {
+ if (viewType == mCurrentView.getViewType()) {
+ return;
+ }
+ VideoViewInterface targetView;
+ if (viewType == VideoView2.VIEW_TYPE_TEXTUREVIEW) {
+ Log.d(TAG, "switching to TextureView");
+ targetView = mTextureView;
+ } else if (viewType == VideoView2.VIEW_TYPE_SURFACEVIEW) {
+ Log.d(TAG, "switching to SurfaceView");
+ targetView = mSurfaceView;
+ } else {
+ throw new IllegalArgumentException("Unknown view type: " + viewType);
+ }
+ ((View) targetView).setVisibility(View.VISIBLE);
+ targetView.takeOver(mCurrentView);
+ mInstance.requestLayout();
+ }
+
+ /**
+ * Returns view type.
+ *
+ * @return view type. See {@see setViewType}.
+ */
+ @VideoView2.ViewType
+ @Override
+ public int getViewType() {
+ return mCurrentView.getViewType();
+ }
+
+ /**
+ * Sets custom actions which will be shown as custom buttons in {@link MediaControlView2}.
+ *
+ * @param actionList A list of {@link PlaybackStateCompat.CustomAction}. The return value of
+ * {@link PlaybackStateCompat.CustomAction#getIcon()} will be used to draw
+ * buttons in {@link MediaControlView2}.
+ * @param executor executor to run callbacks on.
+ * @param listener A listener to be called when a custom button is clicked.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void setCustomActions(List<PlaybackStateCompat.CustomAction> actionList,
+ Executor executor, VideoView2.OnCustomActionListener listener) {
+ mCustomActionList = actionList;
+ mCustomActionListenerRecord = new Pair<>(executor, listener);
+
+ // Create a new playback builder in order to clear existing the custom actions.
+ mStateBuilder = null;
+ updatePlaybackState();
+ }
+
+ /**
+ * Registers a callback to be invoked when a view type change is done.
+ * {@see #setViewType(int)}
+ * @param l The callback that will be run
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void setOnViewTypeChangedListener(VideoView2.OnViewTypeChangedListener l) {
+ mViewTypeChangedListener = l;
+ }
+
+ @Override
+ public void onAttachedToWindowImpl() {
+ // Create MediaSession
+ mMediaSession = new MediaSessionCompat(mInstance.getContext(), "VideoView2MediaSession");
+ mMediaSession.setCallback(new MediaSessionCallback());
+ mMediaSession.setActive(true);
+ mMediaController = mMediaSession.getController();
+ attachMediaControlView();
+ if (mCurrentState == STATE_PREPARED) {
+ extractTracks();
+ extractMetadata();
+ extractAudioMetadata();
+ if (mNeedUpdateMediaType) {
+ mMediaSession.sendSessionEvent(
+ MediaControlView2.EVENT_UPDATE_MEDIA_TYPE_STATUS,
+ mMediaTypeData);
+ mNeedUpdateMediaType = false;
+ }
+ }
+
+ mMediaRouter = MediaRouter.getInstance(mInstance.getContext());
+ mMediaRouter.setMediaSessionCompat(mMediaSession);
+ mMediaRouter.addCallback(mRouteSelector, mRouterCallback,
+ MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
+ }
+
+ @Override
+ public void onDetachedFromWindowImpl() {
+ mMediaSession.release();
+ mMediaSession = null;
+ mMediaController = null;
+ }
+
+ @Override
+ public void onTouchEventImpl(MotionEvent ev) {
+ if (DEBUG) {
+ Log.d(TAG, "onTouchEvent(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
+ if (ev.getAction() == MotionEvent.ACTION_UP && mMediaControlView != null) {
+ if (!mIsMusicMediaType || mSizeType != SIZE_TYPE_FULL) {
+ toggleMediaControlViewVisibility();
+ }
+ }
+ }
+
+ @Override
+ public void onTrackballEventImpl(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_UP && mMediaControlView != null) {
+ if (!mIsMusicMediaType || mSizeType != SIZE_TYPE_FULL) {
+ toggleMediaControlViewVisibility();
+ }
+ }
+ }
+
+ @Override
+ public void onMeasureImpl(int widthMeasureSpec, int heightMeasureSpec) {
+ if (mIsMusicMediaType) {
+ int currWidth = mInstance.getMeasuredWidth();
+ int currHeight = mInstance.getMeasuredHeight();
+ if (mPrevWidth != currWidth || mPrevHeight != currHeight) {
+ Point screenSize = new Point();
+ mManager.getDefaultDisplay().getSize(screenSize);
+ int screenWidth = screenSize.x;
+ int screenHeight = screenSize.y;
+
+ if (currWidth == screenWidth && currHeight == screenHeight) {
+ int orientation = retrieveOrientation();
+ if (orientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
+ inflateMusicView(R.layout.full_landscape_music);
+ } else {
+ inflateMusicView(R.layout.full_portrait_music);
+ }
+
+ if (mSizeType != SIZE_TYPE_FULL) {
+ mSizeType = SIZE_TYPE_FULL;
+ // Remove existing mFadeOut callback
+ mMediaControlView.removeCallbacks(mFadeOut);
+ mMediaControlView.setVisibility(View.VISIBLE);
+ }
+ } else {
+ if (mSizeType != SIZE_TYPE_EMBEDDED) {
+ mSizeType = SIZE_TYPE_EMBEDDED;
+ inflateMusicView(R.layout.embedded_music);
+ // Add new mFadeOut callback
+ mMediaControlView.postDelayed(mFadeOut, mShowControllerIntervalMs);
+ }
+ }
+ mPrevWidth = currWidth;
+ mPrevHeight = currHeight;
+ }
+ }
+ }
+
+ ///////////////////////////////////////////////////
+ // Implements VideoViewInterface.SurfaceListener
+ ///////////////////////////////////////////////////
+
+ /**
+ * @hide
+ */
+ @Override
+ @RestrictTo(LIBRARY_GROUP)
+ public void onSurfaceCreated(View view, int width, int height) {
+ if (DEBUG) {
+ Log.d(TAG, "onSurfaceCreated(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState + ", width/height: " + width + "/" + height
+ + ", " + view.toString());
+ }
+ if (needToStart()) {
+ mMediaController.getTransportControls().play();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ @RestrictTo(LIBRARY_GROUP)
+ public void onSurfaceDestroyed(View view) {
+ if (DEBUG) {
+ Log.d(TAG, "onSurfaceDestroyed(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState + ", " + view.toString());
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ @RestrictTo(LIBRARY_GROUP)
+ public void onSurfaceChanged(View view, int width, int height) {
+ if (DEBUG) {
+ Log.d(TAG, "onSurfaceChanged(). width/height: " + width + "/" + height
+ + ", " + view.toString());
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ @RestrictTo(LIBRARY_GROUP)
+ public void onSurfaceTakeOverDone(VideoViewInterface view) {
+ if (DEBUG) {
+ Log.d(TAG, "onSurfaceTakeOverDone(). Now current view is: " + view);
+ }
+ mCurrentView = view;
+ if (mViewTypeChangedListener != null) {
+ mViewTypeChangedListener.onViewTypeChanged(mInstance, view.getViewType());
+ }
+ if (needToStart()) {
+ mMediaController.getTransportControls().play();
+ }
+ }
+
+ ///////////////////////////////////////////////////
+ // Protected or private methods
+ ///////////////////////////////////////////////////
+
+ private void attachMediaControlView() {
+ // Get MediaController from MediaSession and set it inside MediaControlView
+ mMediaControlView.setController(mMediaSession.getController());
+
+ LayoutParams params =
+ new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ mInstance.addView(mMediaControlView, params);
+ }
+
+ private boolean isInPlaybackState() {
+ return (mMediaPlayer != null || mRoutePlayer != null)
+ && mCurrentState != STATE_ERROR
+ && mCurrentState != STATE_IDLE
+ && mCurrentState != STATE_PREPARING;
+ }
+
+ private boolean needToStart() {
+ return (mMediaPlayer != null || mRoutePlayer != null)
+ && isAudioGranted()
+ && isWaitingPlayback();
+ }
+
+ private boolean isPlaying() {
+ return mMediaPlayer != null
+ && mMediaPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING;
+ }
+
+ private boolean isWaitingPlayback() {
+ return mCurrentState != STATE_PLAYING && mTargetState == STATE_PLAYING;
+ }
+
+ private boolean isAudioGranted() {
+ return mAudioFocused || mAudioFocusType == AudioManager.AUDIOFOCUS_NONE;
+ }
+
+ private AudioManager.OnAudioFocusChangeListener mAudioFocusListener =
+ new AudioManager.OnAudioFocusChangeListener() {
+ @Override
+ public void onAudioFocusChange(int focusChange) {
+ switch (focusChange) {
+ case AudioManager.AUDIOFOCUS_GAIN:
+ mAudioFocused = true;
+ if (needToStart()) {
+ mMediaController.getTransportControls().play();
+ }
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS:
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+ mAudioFocused = false;
+ if (isInPlaybackState() && isPlaying()) {
+ mMediaController.getTransportControls().pause();
+ } else {
+ mTargetState = STATE_PAUSED;
+ }
+ }
+ }
+ };
+
+ @SuppressWarnings("deprecation")
+ private void requestAudioFocus(int focusType) {
+ int result;
+ if (android.os.Build.VERSION.SDK_INT >= 26) {
+ AudioFocusRequest focusRequest;
+ focusRequest = new AudioFocusRequest.Builder(focusType)
+ .setAudioAttributes((AudioAttributes) mAudioAttributes.unwrap())
+ .setOnAudioFocusChangeListener(mAudioFocusListener)
+ .build();
+ result = mAudioManager.requestAudioFocus(focusRequest);
+ } else {
+ result = mAudioManager.requestAudioFocus(mAudioFocusListener,
+ AudioManager.STREAM_MUSIC,
+ focusType);
+ }
+ if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
+ mAudioFocused = false;
+ } else if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+ mAudioFocused = true;
+ } else if (result == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
+ mAudioFocused = false;
+ }
+ }
+
+ // Creates a MediaPlayer instance and prepare playback.
+ private void openVideo(DataSourceDesc dsd) {
+ Uri uri = dsd.getUri();
+ Map<String, String> headers = dsd.getUriHeaders();
+ resetPlayer();
+ if (isRemotePlayback()) {
+ // TODO: b/77556429
+ mRoutePlayer.openVideo(uri);
+ return;
+ }
+
+ try {
+ Log.d(TAG, "openVideo(): creating new MediaPlayer instance.");
+ mMediaPlayer = MediaPlayer2.create();
+ mSurfaceView.setMediaPlayer(mMediaPlayer);
+ mTextureView.setMediaPlayer(mMediaPlayer);
+ mCurrentView.assignSurfaceToMediaPlayer(mMediaPlayer);
+
+ final Context context = mInstance.getContext();
+ mSubtitleController = new SubtitleController(context);
+ mSubtitleController.registerRenderer(new ClosedCaptionRenderer(context));
+ mSubtitleController.setAnchor((SubtitleController.Anchor) mSubtitleView);
+ Executor executor = new Executor() {
+ @Override
+ public void execute(Runnable runnable) {
+ runnable.run();
+ }
+ };
+ mMediaPlayer.setMediaPlayer2EventCallback(executor, mMediaPlayer2Callback);
+
+ mCurrentBufferPercentage = -1;
+ mMediaPlayer.setDataSource(dsd);
+ mMediaPlayer.setAudioAttributes(mAudioAttributes);
+ // we don't set the target state here either, but preserve the
+ // target state that was there before.
+ mCurrentState = STATE_PREPARING;
+ mMediaPlayer.prepare();
+
+ // Save file name as title since the file may not have a title Metadata.
+ mTitle = uri.getPath();
+ String scheme = uri.getScheme();
+ if (scheme != null && scheme.equals("file")) {
+ mTitle = uri.getLastPathSegment();
+ mRetriever = new MediaMetadataRetriever();
+ mRetriever.setDataSource(context, uri);
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "openVideo(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
+ } catch (IllegalArgumentException ex) {
+ Log.w(TAG, "Unable to open content: " + uri, ex);
+ mCurrentState = STATE_ERROR;
+ mTargetState = STATE_ERROR;
+ mMediaPlayer2Callback.onError(mMediaPlayer, dsd,
+ MediaPlayer2.MEDIA_ERROR_UNKNOWN, MediaPlayer2.MEDIA_ERROR_IO);
+ }
+ }
+
+ /*
+ * Reset the media player in any state
+ */
+ @SuppressWarnings("deprecation")
+ private void resetPlayer() {
+ if (mMediaPlayer != null) {
+ final MediaPlayer2 player = mMediaPlayer;
+ new AsyncTask<MediaPlayer2, Void, Void>() {
+ @Override
+ protected Void doInBackground(MediaPlayer2... players) {
+ // TODO: Fix NPE while MediaPlayer2.close()
+ //players[0].close();
+ return null;
+ }
+ }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, player);
+ mMediaPlayer = null;
+ mTextureView.setMediaPlayer(null);
+ mSurfaceView.setMediaPlayer(null);
+ mCurrentState = STATE_IDLE;
+ mTargetState = STATE_IDLE;
+ if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
+ mAudioManager.abandonAudioFocus(null);
+ }
+ }
+ mVideoWidth = 0;
+ mVideoHeight = 0;
+ }
+
+ private void updatePlaybackState() {
+ if (mStateBuilder == null) {
+ long playbackActions = PlaybackStateCompat.ACTION_PLAY
+ | PlaybackStateCompat.ACTION_PAUSE
+ | PlaybackStateCompat.ACTION_REWIND | PlaybackStateCompat.ACTION_FAST_FORWARD
+ | PlaybackStateCompat.ACTION_SEEK_TO;
+ mStateBuilder = new PlaybackStateCompat.Builder();
+ mStateBuilder.setActions(playbackActions);
+
+ if (mCustomActionList != null) {
+ for (PlaybackStateCompat.CustomAction action : mCustomActionList) {
+ mStateBuilder.addCustomAction(action);
+ }
+ }
+ }
+ mStateBuilder.setState(getCorrespondingPlaybackState(),
+ mMediaPlayer.getCurrentPosition(), mSpeed);
+ if (mCurrentState != STATE_ERROR
+ && mCurrentState != STATE_IDLE
+ && mCurrentState != STATE_PREPARING) {
+ if (mCurrentBufferPercentage == -1) {
+ mStateBuilder.setBufferedPosition(-1);
+ } else {
+ mStateBuilder.setBufferedPosition(
+ (long) (mCurrentBufferPercentage / 100.0 * mMediaPlayer.getDuration()));
+ }
+ }
+
+ // Set PlaybackState for MediaSession
+ if (mMediaSession != null) {
+ PlaybackStateCompat state = mStateBuilder.build();
+ mMediaSession.setPlaybackState(state);
+ }
+ }
+
+ private int getCorrespondingPlaybackState() {
+ switch (mCurrentState) {
+ case STATE_ERROR:
+ return PlaybackStateCompat.STATE_ERROR;
+ case STATE_IDLE:
+ return PlaybackStateCompat.STATE_NONE;
+ case STATE_PREPARING:
+ return PlaybackStateCompat.STATE_CONNECTING;
+ case STATE_PREPARED:
+ return PlaybackStateCompat.STATE_PAUSED;
+ case STATE_PLAYING:
+ return PlaybackStateCompat.STATE_PLAYING;
+ case STATE_PAUSED:
+ return PlaybackStateCompat.STATE_PAUSED;
+ case STATE_PLAYBACK_COMPLETED:
+ return PlaybackStateCompat.STATE_STOPPED;
+ default:
+ return -1;
+ }
+ }
+
+ private final Runnable mFadeOut = new Runnable() {
+ @Override
+ public void run() {
+ if (mCurrentState == STATE_PLAYING) {
+ mMediaControlView.setVisibility(View.GONE);
+ }
+ }
+ };
+
+ private void showController() {
+ if (mMediaControlView == null || !isInPlaybackState()
+ || (mIsMusicMediaType && mSizeType == SIZE_TYPE_FULL)) {
+ return;
+ }
+ mMediaControlView.removeCallbacks(mFadeOut);
+ mMediaControlView.setVisibility(View.VISIBLE);
+ if (mShowControllerIntervalMs != 0
+ && !mAccessibilityManager.isTouchExplorationEnabled()) {
+ mMediaControlView.postDelayed(mFadeOut, mShowControllerIntervalMs);
+ }
+ }
+
+ private void toggleMediaControlViewVisibility() {
+ if (mMediaControlView.getVisibility() == View.VISIBLE) {
+ mMediaControlView.removeCallbacks(mFadeOut);
+ mMediaControlView.setVisibility(View.GONE);
+ } else {
+ showController();
+ }
+ }
+
+ private void applySpeed() {
+ if (android.os.Build.VERSION.SDK_INT < 23) {
+ return;
+ }
+ PlaybackParams params = mMediaPlayer.getPlaybackParams().allowDefaults();
+ if (mSpeed != params.getSpeed()) {
+ try {
+ params.setSpeed(mSpeed);
+ mMediaPlayer.setPlaybackParams(params);
+ mFallbackSpeed = mSpeed;
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "PlaybackParams has unsupported value: " + e);
+ float fallbackSpeed = mMediaPlayer.getPlaybackParams().allowDefaults().getSpeed();
+ if (fallbackSpeed > 0.0f) {
+ mFallbackSpeed = fallbackSpeed;
+ }
+ mSpeed = mFallbackSpeed;
+ }
+ }
+ }
+
+ private boolean isRemotePlayback() {
+ if (mMediaController == null) {
+ return false;
+ }
+ PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo();
+ return playbackInfo != null
+ && playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE;
+ }
+
+ private void selectOrDeselectSubtitle(boolean select) {
+ if (!isInPlaybackState()) {
+ return;
+ }
+ if (select) {
+ if (mSubtitleTrackIndices.size() > 0) {
+ mSelectedSubtitleTrackIndex = mSubtitleTrackIndices.get(0).first;
+ mSubtitleController.selectTrack(mSubtitleTrackIndices.get(0).second);
+ mMediaPlayer.selectTrack(mSelectedSubtitleTrackIndex);
+ mSubtitleView.setVisibility(View.VISIBLE);
+ }
+ } else {
+ if (mSelectedSubtitleTrackIndex != INVALID_TRACK_INDEX) {
+ mMediaPlayer.deselectTrack(mSelectedSubtitleTrackIndex);
+ mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX;
+ mSubtitleView.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ private void extractTracks() {
+ List<MediaPlayer2.TrackInfo> trackInfos = mMediaPlayer.getTrackInfo();
+ mVideoTrackIndices = new ArrayList<>();
+ mAudioTrackIndices = new ArrayList<>();
+ mSubtitleTrackIndices = new ArrayList<>();
+ mSubtitleController.reset();
+ for (int i = 0; i < trackInfos.size(); ++i) {
+ int trackType = trackInfos.get(i).getTrackType();
+ if (trackType == MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_VIDEO) {
+ mVideoTrackIndices.add(i);
+ } else if (trackType == MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_AUDIO) {
+ mAudioTrackIndices.add(i);
+ } else if (trackType == MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) {
+ SubtitleTrack track = mSubtitleController.addTrack(trackInfos.get(i).getFormat());
+ if (track != null) {
+ mSubtitleTrackIndices.add(new Pair<>(i, track));
+ }
+ }
+ }
+ // Select first tracks as default
+ if (mVideoTrackIndices.size() > 0) {
+ mSelectedVideoTrackIndex = 0;
+ }
+ if (mAudioTrackIndices.size() > 0) {
+ mSelectedAudioTrackIndex = 0;
+ }
+ if (mVideoTrackIndices.size() == 0 && mAudioTrackIndices.size() > 0) {
+ mIsMusicMediaType = true;
+ }
+
+ Bundle data = new Bundle();
+ data.putInt(MediaControlView2.KEY_VIDEO_TRACK_COUNT, mVideoTrackIndices.size());
+ data.putInt(MediaControlView2.KEY_AUDIO_TRACK_COUNT, mAudioTrackIndices.size());
+ data.putInt(MediaControlView2.KEY_SUBTITLE_TRACK_COUNT, mSubtitleTrackIndices.size());
+ if (mSubtitleTrackIndices.size() > 0) {
+ selectOrDeselectSubtitle(mSubtitleEnabled);
+ }
+ mMediaSession.sendSessionEvent(MediaControlView2.EVENT_UPDATE_TRACK_STATUS, data);
+ }
+
+ private void extractMetadata() {
+ if (mRetriever == null) {
+ return;
+ }
+ // Get and set duration and title values as MediaMetadata for MediaControlView2
+ MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+ String title = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
+ if (title != null) {
+ mTitle = title;
+ }
+ builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mTitle);
+ builder.putLong(
+ MediaMetadataCompat.METADATA_KEY_DURATION, mMediaPlayer.getDuration());
+
+ if (mMediaSession != null) {
+ mMediaSession.setMetadata(builder.build());
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private void extractAudioMetadata() {
+ if (mRetriever == null || !mIsMusicMediaType) {
+ return;
+ }
+
+ mResources = mInstance.getResources();
+ mManager = (WindowManager) mInstance.getContext().getApplicationContext()
+ .getSystemService(Context.WINDOW_SERVICE);
+
+ byte[] album = mRetriever.getEmbeddedPicture();
+ if (album != null) {
+ Bitmap bitmap = BitmapFactory.decodeByteArray(album, 0, album.length);
+ mMusicAlbumDrawable = new BitmapDrawable(bitmap);
+
+ Palette.Builder builder = Palette.from(bitmap);
+ builder.generate(new Palette.PaletteAsyncListener() {
+ @Override
+ public void onGenerated(Palette palette) {
+ mDominantColor = palette.getDominantColor(0);
+ if (mMusicView != null) {
+ mMusicView.setBackgroundColor(mDominantColor);
+ }
+ }
+ });
+ } else {
+ mMusicAlbumDrawable = mResources.getDrawable(R.drawable.ic_default_album_image);
+ }
+
+ String title = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
+ if (title != null) {
+ mMusicTitleText = title;
+ } else {
+ mMusicTitleText = mResources.getString(R.string.mcv2_music_title_unknown_text);
+ }
+
+ String artist = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);
+ if (artist != null) {
+ mMusicArtistText = artist;
+ } else {
+ mMusicArtistText = mResources.getString(R.string.mcv2_music_artist_unknown_text);
+ }
+
+ // Send title and artist string to MediaControlView2
+ MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+ builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mMusicTitleText);
+ builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, mMusicArtistText);
+ mMediaSession.setMetadata(builder.build());
+
+ // Display Embedded mode as default
+ mInstance.removeView(mSurfaceView);
+ mInstance.removeView(mTextureView);
+ inflateMusicView(R.layout.embedded_music);
+ }
+
+ private int retrieveOrientation() {
+ DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
+ int width = dm.widthPixels;
+ int height = dm.heightPixels;
+
+ return (height > width)
+ ? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+ : ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+ }
+
+ private void inflateMusicView(int layoutId) {
+ mInstance.removeView(mMusicView);
+
+ LayoutInflater inflater = (LayoutInflater) mInstance.getContext()
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View v = inflater.inflate(layoutId, null);
+ v.setBackgroundColor(mDominantColor);
+
+ ImageView albumView = v.findViewById(R.id.album);
+ if (albumView != null) {
+ albumView.setImageDrawable(mMusicAlbumDrawable);
+ }
+
+ TextView titleView = v.findViewById(R.id.title);
+ if (titleView != null) {
+ titleView.setText(mMusicTitleText);
+ }
+
+ TextView artistView = v.findViewById(R.id.artist);
+ if (artistView != null) {
+ artistView.setText(mMusicArtistText);
+ }
+
+ mMusicView = v;
+ mInstance.addView(mMusicView, 0);
+ }
+
+ MediaPlayer2EventCallback mMediaPlayer2Callback =
+ new MediaPlayer2EventCallback() {
+ @Override
+ public void onVideoSizeChanged(
+ MediaPlayer2 mp, DataSourceDesc dsd, int width, int height) {
+ if (DEBUG) {
+ Log.d(TAG, "onVideoSizeChanged(): size: " + width + "/" + height);
+ }
+ mVideoWidth = mp.getVideoWidth();
+ mVideoHeight = mp.getVideoHeight();
+ if (DEBUG) {
+ Log.d(TAG, "onVideoSizeChanged(): mVideoSize:" + mVideoWidth + "/"
+ + mVideoHeight);
+ }
+ if (mVideoWidth != 0 && mVideoHeight != 0) {
+ mInstance.requestLayout();
+ }
+ }
+
+ @Override
+ public void onInfo(
+ MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ if (what == MediaPlayer2.MEDIA_INFO_METADATA_UPDATE) {
+ extractTracks();
+ } else if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+ this.onPrepared(mp, dsd);
+ } else if (what == MediaPlayer2.MEDIA_INFO_PLAYBACK_COMPLETE) {
+ this.onCompletion(mp, dsd);
+ } else if (what == MediaPlayer2.MEDIA_INFO_BUFFERING_UPDATE) {
+ this.onBufferingUpdate(mp, dsd, extra);
+ }
+ }
+
+ @Override
+ public void onError(
+ MediaPlayer2 mp, DataSourceDesc dsd, int frameworkErr, int implErr) {
+ if (DEBUG) {
+ Log.d(TAG, "Error: " + frameworkErr + "," + implErr);
+ }
+ mCurrentState = STATE_ERROR;
+ mTargetState = STATE_ERROR;
+ updatePlaybackState();
+
+ if (mMediaControlView != null) {
+ mMediaControlView.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public void onCallCompleted(
+ MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+ if (what == MediaPlayer2.CALL_COMPLETED_SEEK_TO) {
+ updatePlaybackState();
+ }
+ }
+
+ @Override
+ public void onSubtitleData(MediaPlayer2 mp, DataSourceDesc dsd, SubtitleData data) {
+ if (DEBUG) {
+ Log.d(TAG, "onSubtitleData(): getTrackIndex: " + data.getTrackIndex()
+ + ", getCurrentPosition: " + mp.getCurrentPosition()
+ + ", getStartTimeUs(): " + data.getStartTimeUs()
+ + ", diff: "
+ + (data.getStartTimeUs() / 1000 - mp.getCurrentPosition())
+ + "ms, getDurationUs(): " + data.getDurationUs());
+
+ }
+ final int index = data.getTrackIndex();
+ if (index != mSelectedSubtitleTrackIndex) {
+ Log.d(TAG, "onSubtitleData(): getTrackIndex: " + data.getTrackIndex()
+ + ", selected track index: " + mSelectedSubtitleTrackIndex);
+ return;
+ }
+ for (Pair<Integer, SubtitleTrack> p : mSubtitleTrackIndices) {
+ if (p.first == index) {
+ SubtitleTrack track = p.second;
+ track.onData(data);
+ }
+ }
+ }
+
+ private void onPrepared(MediaPlayer2 mp, DataSourceDesc dsd) {
+ if (DEBUG) {
+ Log.d(TAG, "OnPreparedListener(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
+ mCurrentState = STATE_PREPARED;
+ // Create and set playback state for MediaControlView2
+ updatePlaybackState();
+
+ if (mMediaSession != null) {
+ extractTracks();
+ extractMetadata();
+ extractAudioMetadata();
+ }
+
+ if (mMediaControlView != null) {
+ mMediaControlView.setEnabled(true);
+ }
+ int videoWidth = mp.getVideoWidth();
+ int videoHeight = mp.getVideoHeight();
+
+ // mSeekWhenPrepared may be changed after seekTo() call
+ long seekToPosition = mSeekWhenPrepared;
+ if (seekToPosition != 0) {
+ mMediaController.getTransportControls().seekTo(seekToPosition);
+ }
+
+ if (videoWidth != 0 && videoHeight != 0) {
+ if (videoWidth != mVideoWidth || videoHeight != mVideoHeight) {
+ mVideoWidth = videoWidth;
+ mVideoHeight = videoHeight;
+ mInstance.requestLayout();
+ }
+
+ if (needToStart()) {
+ mMediaController.getTransportControls().play();
+ }
+
+ } else {
+ // We don't know the video size yet, but should start anyway.
+ // The video size might be reported to us later.
+ if (needToStart()) {
+ mMediaController.getTransportControls().play();
+ }
+ }
+ // Get and set duration and title values as MediaMetadata for MediaControlView2
+ MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+
+ builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mTitle);
+ builder.putLong(
+ MediaMetadataCompat.METADATA_KEY_DURATION, mMediaPlayer.getDuration());
+
+ if (mMediaSession != null) {
+ mMediaSession.setMetadata(builder.build());
+
+ if (mNeedUpdateMediaType) {
+ mMediaSession.sendSessionEvent(
+ MediaControlView2.EVENT_UPDATE_MEDIA_TYPE_STATUS,
+ mMediaTypeData);
+ mNeedUpdateMediaType = false;
+ }
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private void onCompletion(MediaPlayer2 mp, DataSourceDesc dsd) {
+ mCurrentState = STATE_PLAYBACK_COMPLETED;
+ mTargetState = STATE_PLAYBACK_COMPLETED;
+ updatePlaybackState();
+ if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
+ mAudioManager.abandonAudioFocus(null);
+ }
+ }
+
+ private void onBufferingUpdate(MediaPlayer2 mp, DataSourceDesc dsd, int percent) {
+ mCurrentBufferPercentage = percent;
+ updatePlaybackState();
+ }
+ };
+
+ private class MediaSessionCallback extends MediaSessionCompat.Callback {
+ @Override
+ public void onCommand(String command, Bundle args, ResultReceiver receiver) {
+ if (isRemotePlayback()) {
+ mRoutePlayer.onCommand(command, args, receiver);
+ } else {
+ switch (command) {
+ case MediaControlView2.COMMAND_SHOW_SUBTITLE:
+ int subtitleIndex = args.getInt(
+ MediaControlView2.KEY_SELECTED_SUBTITLE_INDEX,
+ INVALID_TRACK_INDEX);
+ if (subtitleIndex != INVALID_TRACK_INDEX) {
+ int subtitleTrackIndex = mSubtitleTrackIndices.get(subtitleIndex).first;
+ if (subtitleTrackIndex != mSelectedSubtitleTrackIndex) {
+ mSelectedSubtitleTrackIndex = subtitleTrackIndex;
+ mInstance.setSubtitleEnabled(true);
+ }
+ }
+ break;
+ case MediaControlView2.COMMAND_HIDE_SUBTITLE:
+ mInstance.setSubtitleEnabled(false);
+ break;
+ case MediaControlView2.COMMAND_SELECT_AUDIO_TRACK:
+ int audioIndex = args.getInt(MediaControlView2.KEY_SELECTED_AUDIO_INDEX,
+ INVALID_TRACK_INDEX);
+ if (audioIndex != INVALID_TRACK_INDEX) {
+ int audioTrackIndex = mAudioTrackIndices.get(audioIndex);
+ if (audioTrackIndex != mSelectedAudioTrackIndex) {
+ mSelectedAudioTrackIndex = audioTrackIndex;
+ mMediaPlayer.selectTrack(mSelectedAudioTrackIndex);
+ }
+ }
+ break;
+ case MediaControlView2.COMMAND_SET_PLAYBACK_SPEED:
+ float speed = args.getFloat(
+ MediaControlView2.KEY_PLAYBACK_SPEED, INVALID_SPEED);
+ if (speed != INVALID_SPEED && speed != mSpeed) {
+ setSpeed(speed);
+ mSpeed = speed;
+ }
+ break;
+ case MediaControlView2.COMMAND_MUTE:
+ mVolumeLevel = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
+ mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 0, 0);
+ break;
+ case MediaControlView2.COMMAND_UNMUTE:
+ mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mVolumeLevel, 0);
+ break;
+ }
+ }
+ showController();
+ }
+
+ @Override
+ public void onCustomAction(final String action, final Bundle extras) {
+ mCustomActionListenerRecord.first.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCustomActionListenerRecord.second.onCustomAction(action, extras);
+ }
+ });
+ showController();
+ }
+
+ @Override
+ public void onPlay() {
+ if (!isAudioGranted()) {
+ requestAudioFocus(mAudioFocusType);
+ }
+
+ if ((isInPlaybackState() && mCurrentView.hasAvailableSurface()) || mIsMusicMediaType) {
+ if (isRemotePlayback()) {
+ mRoutePlayer.onPlay();
+ } else {
+ applySpeed();
+ mMediaPlayer.play();
+ mCurrentState = STATE_PLAYING;
+ updatePlaybackState();
+ }
+ mCurrentState = STATE_PLAYING;
+ }
+ mTargetState = STATE_PLAYING;
+ if (DEBUG) {
+ Log.d(TAG, "onPlay(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
+ showController();
+ }
+
+ @Override
+ public void onPause() {
+ if (isInPlaybackState()) {
+ if (isRemotePlayback()) {
+ mRoutePlayer.onPlay();
+ mCurrentState = STATE_PAUSED;
+ } else if (isPlaying()) {
+ mMediaPlayer.pause();
+ mCurrentState = STATE_PAUSED;
+ updatePlaybackState();
+ }
+ }
+ mTargetState = STATE_PAUSED;
+ if (DEBUG) {
+ Log.d(TAG, "onPause(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
+ showController();
+ }
+
+ @Override
+ public void onSeekTo(long pos) {
+ if (isInPlaybackState()) {
+ if (isRemotePlayback()) {
+ mRoutePlayer.onPlay();
+ } else {
+ if (android.os.Build.VERSION.SDK_INT < 26) {
+ mMediaPlayer.seekTo((int) pos);
+ } else {
+ mMediaPlayer.seekTo(pos, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+ }
+ mSeekWhenPrepared = 0;
+ }
+ } else {
+ mSeekWhenPrepared = pos;
+ }
+ showController();
+ }
+
+ @Override
+ public void onStop() {
+ if (isRemotePlayback()) {
+ mRoutePlayer.onPlay();
+ } else {
+ resetPlayer();
+ }
+ showController();
+ }
+ }
+}
diff --git a/media-widget/src/main/java/androidx/media/widget/VideoViewInterface.java b/media-widget/src/main/java/androidx/media/widget/impl/VideoViewInterface.java
similarity index 94%
rename from media-widget/src/main/java/androidx/media/widget/VideoViewInterface.java
rename to media-widget/src/main/java/androidx/media/widget/impl/VideoViewInterface.java
index 81b40a9..a497e32 100644
--- a/media-widget/src/main/java/androidx/media/widget/VideoViewInterface.java
+++ b/media-widget/src/main/java/androidx/media/widget/impl/VideoViewInterface.java
@@ -16,10 +16,10 @@
package androidx.media.widget;
-import android.media.MediaPlayer;
import android.view.View;
import androidx.annotation.NonNull;
+import androidx.media.MediaPlayer2;
interface VideoViewInterface {
/**
@@ -29,10 +29,10 @@
* @return true if the surface is successfully assigned, false if not. It will fail to assign
* if any of MediaPlayer or surface is unavailable.
*/
- boolean assignSurfaceToMediaPlayer(MediaPlayer mp);
+ boolean assignSurfaceToMediaPlayer(MediaPlayer2 mp);
void setSurfaceListener(SurfaceListener l);
int getViewType();
- void setMediaPlayer(MediaPlayer mp);
+ void setMediaPlayer(MediaPlayer2 mp);
/**
* Takes over oldView. It means that the MediaPlayer will start rendering on this view.
diff --git a/media-widget/src/main/java/androidx/media/widget/VideoSurfaceView.java b/media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoSurfaceViewWithMp1.java
similarity index 82%
rename from media-widget/src/main/java/androidx/media/widget/VideoSurfaceView.java
rename to media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoSurfaceViewWithMp1.java
index d417bd2..740d908 100644
--- a/media-widget/src/main/java/androidx/media/widget/VideoSurfaceView.java
+++ b/media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoSurfaceViewWithMp1.java
@@ -21,7 +21,6 @@
import android.content.Context;
import android.graphics.Rect;
import android.media.MediaPlayer;
-import android.util.AttributeSet;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
@@ -31,39 +30,24 @@
import androidx.annotation.RequiresApi;
@RequiresApi(21)
-class VideoSurfaceView extends SurfaceView implements VideoViewInterface, SurfaceHolder.Callback {
- private static final String TAG = "VideoSurfaceView";
- private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
+class VideoSurfaceViewWithMp1 extends SurfaceView
+ implements VideoViewInterfaceWithMp1, SurfaceHolder.Callback {
+ private static final String TAG = "VideoSurfaceViewWithMp1";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private SurfaceHolder mSurfaceHolder = null;
private SurfaceListener mSurfaceListener = null;
private MediaPlayer mMediaPlayer;
// A flag to indicate taking over other view should be proceed.
private boolean mIsTakingOverOldView;
- private VideoViewInterface mOldView;
+ private VideoViewInterfaceWithMp1 mOldView;
-
- VideoSurfaceView(Context context) {
- this(context, null);
- }
-
- VideoSurfaceView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- getHolder().addCallback(this);
- }
-
- @RequiresApi(21)
- VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr,
- int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
+ VideoSurfaceViewWithMp1(Context context) {
+ super(context, null);
getHolder().addCallback(this);
}
////////////////////////////////////////////////////
- // implements VideoViewInterface
+ // implements VideoViewInterfaceWithMp1
////////////////////////////////////////////////////
@Override
@@ -95,7 +79,7 @@
}
@Override
- public void takeOver(@NonNull VideoViewInterface oldView) {
+ public void takeOver(@NonNull VideoViewInterfaceWithMp1 oldView) {
if (assignSurfaceToMediaPlayer(mMediaPlayer)) {
((View) oldView).setVisibility(GONE);
mIsTakingOverOldView = false;
@@ -150,7 +134,6 @@
}
}
- // TODO: Investigate the way to move onMeasure() code into FrameLayout.
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int videoWidth = (mMediaPlayer == null) ? 0 : mMediaPlayer.getVideoWidth();
@@ -194,10 +177,4 @@
Log.i(TAG, " measuredSize: " + getMeasuredWidth() + "/" + getMeasuredHeight());
}
}
-
- @Override
- public String toString() {
- return "ViewType: SurfaceView / Visibility: " + getVisibility()
- + " / surfaceHolder: " + mSurfaceHolder;
- }
}
diff --git a/media-widget/src/main/java/androidx/media/widget/VideoTextureView.java b/media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoTextureViewWithMp1.java
similarity index 85%
copy from media-widget/src/main/java/androidx/media/widget/VideoTextureView.java
copy to media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoTextureViewWithMp1.java
index cdc833b..e20d78f 100644
--- a/media-widget/src/main/java/androidx/media/widget/VideoTextureView.java
+++ b/media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoTextureViewWithMp1.java
@@ -21,7 +21,6 @@
import android.content.Context;
import android.graphics.SurfaceTexture;
import android.media.MediaPlayer;
-import android.util.AttributeSet;
import android.util.Log;
import android.view.Surface;
import android.view.TextureView;
@@ -31,38 +30,25 @@
import androidx.annotation.RequiresApi;
@RequiresApi(21)
-class VideoTextureView extends TextureView
- implements VideoViewInterface, TextureView.SurfaceTextureListener {
- private static final String TAG = "VideoTextureView";
- private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
+class VideoTextureViewWithMp1 extends TextureView
+ implements VideoViewInterfaceWithMp1, TextureView.SurfaceTextureListener {
+ private static final String TAG = "VideoTextureViewWithMp1";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private Surface mSurface;
private SurfaceListener mSurfaceListener;
private MediaPlayer mMediaPlayer;
// A flag to indicate taking over other view should be proceed.
private boolean mIsTakingOverOldView;
- private VideoViewInterface mOldView;
+ private VideoViewInterfaceWithMp1 mOldView;
- VideoTextureView(Context context) {
- this(context, null);
- }
-
- VideoTextureView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- VideoTextureView(Context context, AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
- }
-
- VideoTextureView(
- Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
+ VideoTextureViewWithMp1(Context context) {
+ super(context, null);
setSurfaceTextureListener(this);
}
////////////////////////////////////////////////////
- // implements VideoViewInterface
+ // implements VideoViewInterfaceWithMp1
////////////////////////////////////////////////////
@Override
@@ -94,7 +80,7 @@
}
@Override
- public void takeOver(@NonNull VideoViewInterface oldView) {
+ public void takeOver(@NonNull VideoViewInterfaceWithMp1 oldView) {
if (assignSurfaceToMediaPlayer(mMediaPlayer)) {
((View) oldView).setVisibility(GONE);
mIsTakingOverOldView = false;
diff --git a/media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoView2ImplApi28WithMp1.java b/media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoView2ImplApi28WithMp1.java
new file mode 100644
index 0000000..e0f6d36
--- /dev/null
+++ b/media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoView2ImplApi28WithMp1.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.widget;
+
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.media.MediaPlayer.OnSubtitleDataListener;
+import android.media.SubtitleData;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Pair;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.media.subtitle.ClosedCaptionRenderer;
+import androidx.media.subtitle.SubtitleController;
+import androidx.media.subtitle.SubtitleTrack;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Map;
+
+/**
+ * Base implementation of VideoView2.
+ */
+@RequiresApi(28)
+class VideoView2ImplApi28WithMp1 extends VideoView2ImplBaseWithMp1 {
+ private static final String TAG = "VideoView2ImplApi28_1";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private static final int INVALID_TRACK_INDEX = -1;
+
+ private ArrayList<Pair<Integer, SubtitleTrack>> mSubtitleTrackIndices;
+ private SubtitleController mSubtitleController;
+
+ // selected video/audio/subtitle track index as MediaPlayer returns
+ private int mSelectedSubtitleTrackIndex;
+
+ private SubtitleView mSubtitleView;
+ private boolean mSubtitleEnabled;
+
+ @Override
+ public void initialize(
+ VideoView2 instance, Context context,
+ @Nullable AttributeSet attrs, int defStyleAttr) {
+ super.initialize(instance, context, attrs, defStyleAttr);
+ mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX;
+
+ LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT);
+ mSubtitleView = new SubtitleView(context);
+ mSubtitleView.setLayoutParams(params);
+ mSubtitleView.setBackgroundColor(0);
+ mInstance.addView(mSubtitleView);
+
+ mSubtitleEnabled = (attrs == null) || attrs.getAttributeBooleanValue(
+ "http://schemas.android.com/apk/res/android",
+ "enableSubtitle", false);
+ }
+
+ /**
+ * Shows or hides closed caption or subtitles if there is any.
+ * The first subtitle track will be chosen if there multiple subtitle tracks exist.
+ * Default behavior of VideoView2 is not showing subtitle.
+ * @param enable shows closed caption or subtitles if this value is true, or hides.
+ */
+ @Override
+ public void setSubtitleEnabled(boolean enable) {
+ if (enable != mSubtitleEnabled) {
+ selectOrDeselectSubtitle(enable);
+ }
+ mSubtitleEnabled = enable;
+ }
+
+ /**
+ * Returns true if showing subtitle feature is enabled or returns false.
+ * Although there is no subtitle track or closed caption, it can return true, if the feature
+ * has been enabled by {@link #setSubtitleEnabled}.
+ */
+ @Override
+ public boolean isSubtitleEnabled() {
+ return mSubtitleEnabled;
+ }
+
+ ///////////////////////////////////////////////////
+ // Protected or private methods
+ ///////////////////////////////////////////////////
+
+ /**
+ * Used in openVideo(). Setup MediaPlayer and related objects before calling prepare.
+ */
+ @Override
+ protected void setupMediaPlayer(Context context, Uri uri, Map<String, String> headers)
+ throws IOException {
+ super.setupMediaPlayer(context, uri, headers);
+
+ mSubtitleController = new SubtitleController(context);
+ mSubtitleController.registerRenderer(new ClosedCaptionRenderer(context));
+ mSubtitleController.setAnchor((SubtitleController.Anchor) mSubtitleView);
+
+ mMediaPlayer.setOnSubtitleDataListener(mSubtitleListener);
+ }
+
+ private void selectOrDeselectSubtitle(boolean select) {
+ if (!isInPlaybackState()) {
+ return;
+ }
+ if (select) {
+ if (mSubtitleTrackIndices.size() > 0) {
+ mSelectedSubtitleTrackIndex = mSubtitleTrackIndices.get(0).first;
+ mSubtitleController.selectTrack(mSubtitleTrackIndices.get(0).second);
+ mMediaPlayer.selectTrack(mSelectedSubtitleTrackIndex);
+ mSubtitleView.setVisibility(View.VISIBLE);
+ }
+ } else {
+ if (mSelectedSubtitleTrackIndex != INVALID_TRACK_INDEX) {
+ mMediaPlayer.deselectTrack(mSelectedSubtitleTrackIndex);
+ mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX;
+ mSubtitleView.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ @Override
+ protected void extractTracks() {
+ MediaPlayer.TrackInfo[] trackInfos = mMediaPlayer.getTrackInfo();
+ mVideoTrackIndices = new ArrayList<>();
+ mAudioTrackIndices = new ArrayList<>();
+ mSubtitleTrackIndices = new ArrayList<>();
+ mSubtitleController.reset();
+ for (int i = 0; i < trackInfos.length; ++i) {
+ int trackType = trackInfos[i].getTrackType();
+ if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO) {
+ mVideoTrackIndices.add(i);
+ } else if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO) {
+ mAudioTrackIndices.add(i);
+ } else if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) {
+ SubtitleTrack track = mSubtitleController.addTrack(trackInfos[i].getFormat());
+ if (track != null) {
+ mSubtitleTrackIndices.add(new Pair<>(i, track));
+ }
+ }
+ }
+ // Select first tracks as default
+ if (mVideoTrackIndices.size() > 0) {
+ mSelectedVideoTrackIndex = 0;
+ }
+ if (mAudioTrackIndices.size() > 0) {
+ mSelectedAudioTrackIndex = 0;
+ }
+
+ Bundle data = new Bundle();
+ data.putInt(MediaControlView2.KEY_VIDEO_TRACK_COUNT, mVideoTrackIndices.size());
+ data.putInt(MediaControlView2.KEY_AUDIO_TRACK_COUNT, mAudioTrackIndices.size());
+ data.putInt(MediaControlView2.KEY_SUBTITLE_TRACK_COUNT, mSubtitleTrackIndices.size());
+ if (mSubtitleTrackIndices.size() > 0) {
+ selectOrDeselectSubtitle(mSubtitleEnabled);
+ }
+ mMediaSession.sendSessionEvent(MediaControlView2.EVENT_UPDATE_TRACK_STATUS, data);
+ }
+
+ private OnSubtitleDataListener mSubtitleListener =
+ new OnSubtitleDataListener() {
+ @Override
+ public void onSubtitleData(MediaPlayer mp, SubtitleData data) {
+ if (DEBUG) {
+ Log.d(TAG, "onSubtitleData(): getTrackIndex: " + data.getTrackIndex()
+ + ", getCurrentPosition: " + mp.getCurrentPosition()
+ + ", getStartTimeUs(): " + data.getStartTimeUs()
+ + ", diff: "
+ + (data.getStartTimeUs() / 1000 - mp.getCurrentPosition())
+ + "ms, getDurationUs(): " + data.getDurationUs());
+
+ }
+ final int index = data.getTrackIndex();
+ if (index != mSelectedSubtitleTrackIndex) {
+ Log.d(TAG, "onSubtitleData(): getTrackIndex: " + data.getTrackIndex()
+ + ", selected track index: " + mSelectedSubtitleTrackIndex);
+ return;
+ }
+ for (Pair<Integer, SubtitleTrack> p : mSubtitleTrackIndices) {
+ if (p.first == index) {
+ SubtitleTrack track = p.second;
+ track.onData(data);
+ }
+ }
+ }
+ };
+
+ @Override
+ protected void doShowSubtitleCommand(Bundle args) {
+ int subtitleIndex = args.getInt(
+ MediaControlView2.KEY_SELECTED_SUBTITLE_INDEX,
+ INVALID_TRACK_INDEX);
+ if (subtitleIndex != INVALID_TRACK_INDEX) {
+ int subtitleTrackIndex = mSubtitleTrackIndices.get(subtitleIndex).first;
+ if (subtitleTrackIndex != mSelectedSubtitleTrackIndex) {
+ mSelectedSubtitleTrackIndex = subtitleTrackIndex;
+ mInstance.setSubtitleEnabled(true);
+ }
+ }
+ }
+
+ @Override
+ protected void doHideSubtitleCommand() {
+ mInstance.setSubtitleEnabled(false);
+ }
+}
diff --git a/media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoView2ImplBaseWithMp1.java b/media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoView2ImplBaseWithMp1.java
new file mode 100644
index 0000000..8fdb1b1
--- /dev/null
+++ b/media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoView2ImplBaseWithMp1.java
@@ -0,0 +1,1473 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.widget;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Point;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.AudioAttributes;
+import android.media.AudioFocusRequest;
+import android.media.AudioManager;
+import android.media.MediaMetadataRetriever;
+import android.media.MediaPlayer;
+import android.media.PlaybackParams;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaControllerCompat.PlaybackInfo;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.media.AudioAttributesCompat;
+import androidx.media.DataSourceDesc;
+import androidx.media.MediaItem2;
+import androidx.media.MediaMetadata2;
+import androidx.media.SessionToken2;
+import androidx.mediarouter.media.MediaControlIntent;
+import androidx.mediarouter.media.MediaItemStatus;
+import androidx.mediarouter.media.MediaRouteSelector;
+import androidx.mediarouter.media.MediaRouter;
+import androidx.palette.graphics.Palette;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * Base implementation of VideoView2.
+ */
+@RequiresApi(21) // TODO correct minSdk API use incompatibilities and remove before release.
+class VideoView2ImplBaseWithMp1
+ implements VideoView2Impl, VideoViewInterfaceWithMp1.SurfaceListener {
+ private static final String TAG = "VideoView2ImplBase_1";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+ private static final long DEFAULT_SHOW_CONTROLLER_INTERVAL_MS = 2000;
+
+ private static final int STATE_ERROR = -1;
+ private static final int STATE_IDLE = 0;
+ private static final int STATE_PREPARING = 1;
+ private static final int STATE_PREPARED = 2;
+ private static final int STATE_PLAYING = 3;
+ private static final int STATE_PAUSED = 4;
+ private static final int STATE_PLAYBACK_COMPLETED = 5;
+
+ private static final int INVALID_TRACK_INDEX = -1;
+ private static final float INVALID_SPEED = 0f;
+
+ private static final int SIZE_TYPE_EMBEDDED = 0;
+ private static final int SIZE_TYPE_FULL = 1;
+ private static final int SIZE_TYPE_MINIMAL = 2;
+
+ private AccessibilityManager mAccessibilityManager;
+ private AudioManager mAudioManager;
+ private AudioAttributes mAudioAttributes;
+ private int mAudioFocusType = AudioManager.AUDIOFOCUS_GAIN; // legacy focus gain
+ private boolean mAudioFocused = false;
+
+ private Pair<Executor, VideoView2.OnCustomActionListener> mCustomActionListenerRecord;
+ private VideoView2.OnViewTypeChangedListener mViewTypeChangedListener;
+
+ private VideoViewInterfaceWithMp1 mCurrentView;
+ private VideoTextureViewWithMp1 mTextureView;
+ private VideoSurfaceViewWithMp1 mSurfaceView;
+
+ protected MediaPlayer mMediaPlayer;
+ private DataSourceDesc mDsd;
+ private Uri mUri;
+ private Map<String, String> mHeaders;
+ private MediaControlView2 mMediaControlView;
+ protected MediaSessionCompat mMediaSession;
+ private MediaControllerCompat mMediaController;
+ private MediaMetadata2 mMediaMetadata;
+ private MediaMetadataRetriever mRetriever;
+ private boolean mNeedUpdateMediaType;
+ private Bundle mMediaTypeData;
+ private String mTitle;
+
+ private WindowManager mManager;
+ private Resources mResources;
+ private View mMusicView;
+ private Drawable mMusicAlbumDrawable;
+ private String mMusicTitleText;
+ private String mMusicArtistText;
+ private int mPrevWidth;
+ private int mPrevHeight;
+ private int mDominantColor;
+ private int mSizeType;
+
+ private PlaybackStateCompat.Builder mStateBuilder;
+ private List<PlaybackStateCompat.CustomAction> mCustomActionList;
+
+ private int mTargetState = STATE_IDLE;
+ private int mCurrentState = STATE_IDLE;
+ private int mCurrentBufferPercentage;
+ private long mSeekWhenPrepared; // recording the seek position while preparing
+
+ private int mVideoWidth;
+ private int mVideoHeight;
+
+ protected ArrayList<Integer> mVideoTrackIndices;
+ protected ArrayList<Integer> mAudioTrackIndices;
+
+ // selected video/audio/subtitle track index as MediaPlayer returns
+ protected int mSelectedVideoTrackIndex;
+ protected int mSelectedAudioTrackIndex;
+
+ private float mSpeed;
+ private float mFallbackSpeed; // keep the original speed before 'pause' is called.
+ private float mVolumeLevelFloat;
+ private int mVolumeLevel;
+ protected VideoView2 mInstance;
+
+ private long mShowControllerIntervalMs;
+
+ private MediaRouter mMediaRouter;
+ private MediaRouteSelector mRouteSelector;
+ private MediaRouter.RouteInfo mRoute;
+ private RoutePlayer mRoutePlayer;
+
+ private final MediaRouter.Callback mRouterCallback = new MediaRouter.Callback() {
+ @Override
+ public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) {
+ if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
+ // Stop local playback (if necessary)
+ resetPlayer();
+ mRoute = route;
+ mRoutePlayer = new RoutePlayer(mInstance.getContext(), route);
+ mRoutePlayer.setPlayerEventCallback(new RoutePlayer.PlayerEventCallback() {
+ @Override
+ public void onPlayerStateChanged(MediaItemStatus itemStatus) {
+ PlaybackStateCompat.Builder psBuilder = new PlaybackStateCompat.Builder();
+ psBuilder.setActions(RoutePlayer.PLAYBACK_ACTIONS);
+ long position = itemStatus.getContentPosition();
+ switch (itemStatus.getPlaybackState()) {
+ case MediaItemStatus.PLAYBACK_STATE_PENDING:
+ psBuilder.setState(PlaybackStateCompat.STATE_NONE, position, 0);
+ mCurrentState = STATE_IDLE;
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_PLAYING:
+ psBuilder.setState(PlaybackStateCompat.STATE_PLAYING, position, 1);
+ mCurrentState = STATE_PLAYING;
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_PAUSED:
+ psBuilder.setState(PlaybackStateCompat.STATE_PAUSED, position, 0);
+ mCurrentState = STATE_PAUSED;
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_BUFFERING:
+ psBuilder.setState(
+ PlaybackStateCompat.STATE_BUFFERING, position, 0);
+ mCurrentState = STATE_PAUSED;
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_FINISHED:
+ psBuilder.setState(PlaybackStateCompat.STATE_STOPPED, position, 0);
+ mCurrentState = STATE_PLAYBACK_COMPLETED;
+ break;
+ }
+
+ PlaybackStateCompat pbState = psBuilder.build();
+ mMediaSession.setPlaybackState(pbState);
+
+ MediaMetadataCompat.Builder mmBuilder = new MediaMetadataCompat.Builder();
+ mmBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION,
+ itemStatus.getContentDuration());
+ mMediaSession.setMetadata(mmBuilder.build());
+ }
+ });
+ // Start remote playback (if necessary)
+ // TODO: b/77556429
+ mRoutePlayer.openVideo(mUri);
+ }
+ }
+
+ @Override
+ public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route, int reason) {
+ if (mRoute != null && mRoutePlayer != null) {
+ mRoutePlayer.release();
+ mRoutePlayer = null;
+ }
+ if (mRoute == route) {
+ mRoute = null;
+ }
+ if (reason != MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) {
+ // TODO: Resume local playback (if necessary)
+ // TODO: b/77556429
+ openVideo(mUri, mHeaders);
+ }
+ }
+ };
+
+ @Override
+ public void initialize(
+ VideoView2 instance, Context context,
+ @Nullable AttributeSet attrs, int defStyleAttr) {
+ mInstance = instance;
+
+ mVideoWidth = 0;
+ mVideoHeight = 0;
+ mSpeed = 1.0f;
+ mFallbackSpeed = mSpeed;
+ mShowControllerIntervalMs = DEFAULT_SHOW_CONTROLLER_INTERVAL_MS;
+
+ mAccessibilityManager = (AccessibilityManager) context.getSystemService(
+ Context.ACCESSIBILITY_SERVICE);
+
+ mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ mAudioAttributes = new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_MEDIA)
+ .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE).build();
+
+ mInstance.setFocusable(true);
+ mInstance.setFocusableInTouchMode(true);
+ mInstance.requestFocus();
+
+ mTextureView = new VideoTextureViewWithMp1(context);
+ mSurfaceView = new VideoSurfaceViewWithMp1(context);
+ LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT);
+ mTextureView.setLayoutParams(params);
+ mSurfaceView.setLayoutParams(params);
+ mTextureView.setSurfaceListener(this);
+ mSurfaceView.setSurfaceListener(this);
+
+ mInstance.addView(mTextureView);
+ mInstance.addView(mSurfaceView);
+
+ boolean enableControlView = (attrs == null) || attrs.getAttributeBooleanValue(
+ "http://schemas.android.com/apk/res/android",
+ "enableControlView", true);
+ if (enableControlView) {
+ mMediaControlView = new MediaControlView2(context);
+ }
+
+ // Choose surface view by default
+ int viewType = (attrs == null) ? VideoView2.VIEW_TYPE_SURFACEVIEW
+ : attrs.getAttributeIntValue(
+ "http://schemas.android.com/apk/res/android",
+ "viewType", VideoView2.VIEW_TYPE_SURFACEVIEW);
+ if (viewType == VideoView2.VIEW_TYPE_SURFACEVIEW) {
+ Log.d(TAG, "viewType attribute is surfaceView.");
+ mTextureView.setVisibility(View.GONE);
+ mSurfaceView.setVisibility(View.VISIBLE);
+ mCurrentView = mSurfaceView;
+ } else if (viewType == VideoView2.VIEW_TYPE_TEXTUREVIEW) {
+ Log.d(TAG, "viewType attribute is textureView.");
+ mTextureView.setVisibility(View.VISIBLE);
+ mSurfaceView.setVisibility(View.GONE);
+ mCurrentView = mTextureView;
+ }
+
+ MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
+ builder.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
+ builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
+ mRouteSelector = builder.build();
+ }
+
+ /**
+ * Sets MediaControlView2 instance. It will replace the previously assigned MediaControlView2
+ * instance if any.
+ *
+ * @param mediaControlView a media control view2 instance.
+ * @param intervalMs a time interval in milliseconds until VideoView2 hides MediaControlView2.
+ */
+ @Override
+ public void setMediaControlView2(MediaControlView2 mediaControlView, long intervalMs) {
+ mMediaControlView = mediaControlView;
+ mShowControllerIntervalMs = intervalMs;
+ mMediaControlView.setRouteSelector(mRouteSelector);
+
+ if (mInstance.isAttachedToWindow()) {
+ attachMediaControlView();
+ }
+ }
+
+ /**
+ * Returns MediaControlView2 instance which is currently attached to VideoView2 by default or by
+ * {@link #setMediaControlView2} method.
+ */
+ @Override
+ public MediaControlView2 getMediaControlView2() {
+ return mMediaControlView;
+ }
+
+ /**
+ * Sets MediaMetadata2 instance. It will replace the previously assigned MediaMetadata2 instance
+ * if any.
+ *
+ * @param metadata a MediaMetadata2 instance.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void setMediaMetadata(MediaMetadata2 metadata) {
+ //mProvider.setMediaMetadata_impl(metadata);
+ }
+
+ /**
+ * Returns MediaMetadata2 instance which is retrieved from MediaPlayer inside VideoView2 by
+ * default or by {@link #setMediaMetadata} method.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public MediaMetadata2 getMediaMetadata() {
+ return mMediaMetadata;
+ }
+
+ /**
+ * Returns MediaController instance which is connected with MediaSession that VideoView2 is
+ * using. This method should be called when VideoView2 is attached to window, or it throws
+ * IllegalStateException, since internal MediaSession instance is not available until
+ * this view is attached to window. Please check {@link View#isAttachedToWindow}
+ * before calling this method.
+ *
+ * @throws IllegalStateException if interal MediaSession is not created yet.
+ * @hide TODO: remove
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public MediaControllerCompat getMediaController() {
+ if (mMediaSession == null) {
+ throw new IllegalStateException("MediaSession instance is not available.");
+ }
+ return mMediaController;
+ }
+
+ /**
+ * Returns {@link SessionToken2} so that developers create their own
+ * {@link androidx.media.MediaController2} instance. This method should be called when
+ * VideoView2 is attached to window, or it throws IllegalStateException.
+ *
+ * @throws IllegalStateException if interal MediaSession is not created yet.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public SessionToken2 getMediaSessionToken() {
+ //return mProvider.getMediaSessionToken_impl();
+ return null;
+ }
+
+ /**
+ * Shows or hides closed caption or subtitles if there is any.
+ * The first subtitle track will be chosen if there multiple subtitle tracks exist.
+ * Default behavior of VideoView2 is not showing subtitle.
+ * @param enable shows closed caption or subtitles if this value is true, or hides.
+ */
+ @Override
+ public void setSubtitleEnabled(boolean enable) {
+ // No-op on API < 28
+ }
+
+ /**
+ * Returns true if showing subtitle feature is enabled or returns false.
+ * Although there is no subtitle track or closed caption, it can return true, if the feature
+ * has been enabled by {@link #setSubtitleEnabled}.
+ */
+ @Override
+ public boolean isSubtitleEnabled() {
+ // Not supported on API < 28
+ return false;
+ }
+
+ /**
+ * Sets playback speed.
+ *
+ * It is expressed as a multiplicative factor, where normal speed is 1.0f. If it is less than
+ * or equal to zero, it will be just ignored and nothing will be changed. If it exceeds the
+ * maximum speed that internal engine supports, system will determine best handling or it will
+ * be reset to the normal speed 1.0f.
+ * @param speed the playback speed. It should be positive.
+ */
+ @Override
+ public void setSpeed(float speed) {
+ if (speed <= 0.0f) {
+ Log.e(TAG, "Unsupported speed (" + speed + ") is ignored.");
+ return;
+ }
+ mSpeed = speed;
+ if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
+ applySpeed();
+ }
+ updatePlaybackState();
+ }
+
+ /**
+ * Returns playback speed.
+ *
+ * It returns the same value that has been set by {@link #setSpeed}, if it was available value.
+ * If {@link #setSpeed} has not been called before, then the normal speed 1.0f will be returned.
+ */
+ @Override
+ public float getSpeed() {
+ return mSpeed;
+ }
+
+ /**
+ * Sets which type of audio focus will be requested during the playback, or configures playback
+ * to not request audio focus. Valid values for focus requests are
+ * {@link AudioManager#AUDIOFOCUS_GAIN}, {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT},
+ * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK}, and
+ * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}. Or use
+ * {@link AudioManager#AUDIOFOCUS_NONE} to express that audio focus should not be
+ * requested when playback starts. You can for instance use this when playing a silent animation
+ * through this class, and you don't want to affect other audio applications playing in the
+ * background.
+ *
+ * @param focusGain the type of audio focus gain that will be requested, or
+ * {@link AudioManager#AUDIOFOCUS_NONE} to disable the use audio focus during
+ * playback.
+ */
+ @Override
+ public void setAudioFocusRequest(int focusGain) {
+ if (focusGain != AudioManager.AUDIOFOCUS_NONE
+ && focusGain != AudioManager.AUDIOFOCUS_GAIN
+ && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
+ && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
+ && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) {
+ throw new IllegalArgumentException("Illegal audio focus type " + focusGain);
+ }
+ mAudioFocusType = focusGain;
+ }
+
+ /**
+ * Sets the {@link AudioAttributesCompat} to be used during the playback of the video.
+ *
+ * @param attributes non-null <code>AudioAttributesCompat</code>.
+ */
+ @Override
+ public void setAudioAttributes(@NonNull AudioAttributesCompat attributes) {
+ if (attributes == null) {
+ throw new IllegalArgumentException("Illegal null AudioAttributesCompat");
+ }
+ mAudioAttributes = (AudioAttributes) attributes.unwrap();
+ }
+
+ /**
+ * Sets video path.
+ *
+ * @param path the path of the video.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void setVideoPath(String path) {
+ setVideoUri(Uri.parse(path));
+ }
+
+ /**
+ * Sets video URI.
+ *
+ * @param uri the URI of the video.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void setVideoUri(Uri uri) {
+ setVideoUri(uri, null);
+ }
+
+ /**
+ * Sets video URI using specific headers.
+ *
+ * @param uri the URI of the video.
+ * @param headers the headers for the URI request.
+ * Note that the cross domain redirection is allowed by default, but that can be
+ * changed with key/value pairs through the headers parameter with
+ * "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
+ * to disallow or allow cross domain redirection.
+ */
+ @Override
+ public void setVideoUri(Uri uri, @Nullable Map<String, String> headers) {
+ mSeekWhenPrepared = 0;
+ openVideo(uri, headers);
+ }
+
+ /**
+ * Sets {@link MediaItem2} object to render using VideoView2. Alternative way to set media
+ * object to VideoView2 is {@link #setDataSource}.
+ * @param mediaItem the MediaItem2 to play
+ * @see #setDataSource
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void setMediaItem(@NonNull MediaItem2 mediaItem) {
+ }
+
+ /**
+ * Sets {@link DataSourceDesc} object to render using VideoView2.
+ * @param dataSource the {@link DataSourceDesc} object to play.
+ * @see #setMediaItem
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void setDataSource(@NonNull DataSourceDesc dataSource) {
+ }
+
+ /**
+ * Selects which view will be used to render video between SurfacView and TextureView.
+ *
+ * @param viewType the view type to render video
+ * <ul>
+ * <li>{@link #VideoView2.VIEW_TYPE_SURFACEVIEW}
+ * <li>{@link #VideoView2.VIEW_TYPE_TEXTUREVIEW}
+ * </ul>
+ */
+ @Override
+ public void setViewType(@VideoView2.ViewType int viewType) {
+ if (viewType == mCurrentView.getViewType()) {
+ return;
+ }
+ VideoViewInterfaceWithMp1 targetView;
+ if (viewType == VideoView2.VIEW_TYPE_TEXTUREVIEW) {
+ Log.d(TAG, "switching to TextureView");
+ targetView = mTextureView;
+ } else if (viewType == VideoView2.VIEW_TYPE_SURFACEVIEW) {
+ Log.d(TAG, "switching to SurfaceView");
+ targetView = mSurfaceView;
+ } else {
+ throw new IllegalArgumentException("Unknown view type: " + viewType);
+ }
+ ((View) targetView).setVisibility(View.VISIBLE);
+ targetView.takeOver(mCurrentView);
+ mInstance.requestLayout();
+ }
+
+ /**
+ * Returns view type.
+ *
+ * @return view type. See {@see setViewType}.
+ */
+ @Override
+ @VideoView2.ViewType
+ public int getViewType() {
+ return mCurrentView.getViewType();
+ }
+
+ /**
+ * Sets custom actions which will be shown as custom buttons in {@link MediaControlView2}.
+ *
+ * @param actionList A list of {@link PlaybackStateCompat.CustomAction}. The return value of
+ * {@link PlaybackStateCompat.CustomAction#getIcon()} will be used to draw
+ * buttons in {@link MediaControlView2}.
+ * @param executor executor to run callbacks on.
+ * @param listener A listener to be called when a custom button is clicked.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void setCustomActions(List<PlaybackStateCompat.CustomAction> actionList,
+ Executor executor, VideoView2.OnCustomActionListener listener) {
+ mCustomActionList = actionList;
+ mCustomActionListenerRecord = new Pair<>(executor, listener);
+
+ // Create a new playback builder in order to clear existing the custom actions.
+ mStateBuilder = null;
+ updatePlaybackState();
+ }
+
+ /**
+ * Registers a callback to be invoked when a view type change is done.
+ * {@see #setViewType(int)}
+ * @param l The callback that will be run
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void setOnViewTypeChangedListener(VideoView2.OnViewTypeChangedListener l) {
+ mViewTypeChangedListener = l;
+ }
+
+ @Override
+ public void onAttachedToWindowImpl() {
+ // Create MediaSession
+ mMediaSession = new MediaSessionCompat(mInstance.getContext(), "VideoView2MediaSession");
+ mMediaSession.setCallback(new MediaSessionCallback());
+ mMediaSession.setActive(true);
+ mMediaController = mMediaSession.getController();
+ attachMediaControlView();
+ if (mCurrentState == STATE_PREPARED) {
+ extractTracks();
+ extractMetadata();
+ extractAudioMetadata();
+ if (mNeedUpdateMediaType) {
+ mMediaSession.sendSessionEvent(
+ MediaControlView2.EVENT_UPDATE_MEDIA_TYPE_STATUS,
+ mMediaTypeData);
+ mNeedUpdateMediaType = false;
+ }
+ }
+
+ mMediaRouter = MediaRouter.getInstance(mInstance.getContext());
+ mMediaRouter.setMediaSessionCompat(mMediaSession);
+ mMediaRouter.addCallback(mRouteSelector, mRouterCallback,
+ MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
+ }
+
+ @Override
+ public void onDetachedFromWindowImpl() {
+ mMediaSession.release();
+ mMediaSession = null;
+ mMediaController = null;
+ }
+
+ @Override
+ public void onTouchEventImpl(MotionEvent ev) {
+ if (DEBUG) {
+ Log.d(TAG, "onTouchEvent(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
+ if (ev.getAction() == MotionEvent.ACTION_UP && mMediaControlView != null) {
+ if (!isMusicMediaType() || mSizeType != SIZE_TYPE_FULL) {
+ toggleMediaControlViewVisibility();
+ }
+ }
+ }
+
+ @Override
+ public void onTrackballEventImpl(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_UP && mMediaControlView != null) {
+ if (!isMusicMediaType() || mSizeType != SIZE_TYPE_FULL) {
+ toggleMediaControlViewVisibility();
+ }
+ }
+ }
+
+ @Override
+ public void onMeasureImpl(int widthMeasureSpec, int heightMeasureSpec) {
+ if (isMusicMediaType()) {
+ int currWidth = mInstance.getMeasuredWidth();
+ int currHeight = mInstance.getMeasuredHeight();
+ if (mPrevWidth != currWidth || mPrevHeight != currHeight) {
+ Point screenSize = new Point();
+ mManager.getDefaultDisplay().getSize(screenSize);
+ int screenWidth = screenSize.x;
+ int screenHeight = screenSize.y;
+
+ if (currWidth == screenWidth && currHeight == screenHeight) {
+ int orientation = retrieveOrientation();
+ if (orientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
+ inflateMusicView(R.layout.full_landscape_music);
+ } else {
+ inflateMusicView(R.layout.full_portrait_music);
+ }
+
+ if (mSizeType != SIZE_TYPE_FULL) {
+ mSizeType = SIZE_TYPE_FULL;
+ // Remove existing mFadeOut callback
+ mMediaControlView.removeCallbacks(mFadeOut);
+ mMediaControlView.setVisibility(View.VISIBLE);
+ }
+ } else {
+ if (mSizeType != SIZE_TYPE_EMBEDDED) {
+ mSizeType = SIZE_TYPE_EMBEDDED;
+ inflateMusicView(R.layout.embedded_music);
+ // Add new mFadeOut callback
+ mMediaControlView.postDelayed(mFadeOut, mShowControllerIntervalMs);
+ }
+ }
+ mPrevWidth = currWidth;
+ mPrevHeight = currHeight;
+ }
+ }
+ }
+
+ ///////////////////////////////////////////////////
+ // Implements VideoViewInterfaceWithMp1.SurfaceListener
+ ///////////////////////////////////////////////////
+
+ @Override
+ public void onSurfaceCreated(View view, int width, int height) {
+ if (DEBUG) {
+ Log.d(TAG, "onSurfaceCreated(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState + ", width/height: " + width + "/" + height
+ + ", " + view.toString());
+ }
+ if (needToStart()) {
+ mMediaController.getTransportControls().play();
+ }
+ }
+
+ @Override
+ public void onSurfaceDestroyed(View view) {
+ if (DEBUG) {
+ Log.d(TAG, "onSurfaceDestroyed(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState + ", " + view.toString());
+ }
+ }
+
+ @Override
+ public void onSurfaceChanged(View view, int width, int height) {
+ if (DEBUG) {
+ Log.d(TAG, "onSurfaceChanged(). width/height: " + width + "/" + height
+ + ", " + view.toString());
+ }
+ }
+
+ @Override
+ public void onSurfaceTakeOverDone(VideoViewInterfaceWithMp1 view) {
+ if (DEBUG) {
+ Log.d(TAG, "onSurfaceTakeOverDone(). Now current view is: " + view);
+ }
+ mCurrentView = view;
+ if (mViewTypeChangedListener != null) {
+ mViewTypeChangedListener.onViewTypeChanged(mInstance, view.getViewType());
+ }
+ if (needToStart()) {
+ mMediaController.getTransportControls().play();
+ }
+ }
+
+ ///////////////////////////////////////////////////
+ // Protected or private methods
+ ///////////////////////////////////////////////////
+
+ private void attachMediaControlView() {
+ // Get MediaController from MediaSession and set it inside MediaControlView
+ mMediaControlView.setController(mMediaSession.getController());
+
+ LayoutParams params =
+ new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ mInstance.addView(mMediaControlView, params);
+ }
+
+ protected boolean isInPlaybackState() {
+ return (mMediaPlayer != null || mRoutePlayer != null)
+ && mCurrentState != STATE_ERROR
+ && mCurrentState != STATE_IDLE
+ && mCurrentState != STATE_PREPARING;
+ }
+
+ private boolean needToStart() {
+ return (mMediaPlayer != null || mRoutePlayer != null)
+ && isAudioGranted()
+ && isWaitingPlayback();
+ }
+
+ private boolean isMusicMediaType() {
+ return mVideoTrackIndices != null && mVideoTrackIndices.size() == 0;
+ }
+
+ private boolean isWaitingPlayback() {
+ return mCurrentState != STATE_PLAYING && mTargetState == STATE_PLAYING;
+ }
+
+ private boolean isAudioGranted() {
+ return mAudioFocused || mAudioFocusType == AudioManager.AUDIOFOCUS_NONE;
+ }
+
+ private AudioManager.OnAudioFocusChangeListener mAudioFocusListener =
+ new AudioManager.OnAudioFocusChangeListener() {
+ @Override
+ public void onAudioFocusChange(int focusChange) {
+ switch (focusChange) {
+ case AudioManager.AUDIOFOCUS_GAIN:
+ mAudioFocused = true;
+ if (needToStart()) {
+ mMediaController.getTransportControls().play();
+ }
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS:
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+ mAudioFocused = false;
+ if (isInPlaybackState() && mMediaPlayer.isPlaying()) {
+ mMediaController.getTransportControls().pause();
+ } else {
+ mTargetState = STATE_PAUSED;
+ }
+ }
+ }
+ };
+
+ @SuppressWarnings("deprecation")
+ private void requestAudioFocus(int focusType) {
+ int result;
+ if (android.os.Build.VERSION.SDK_INT >= 26) {
+ AudioFocusRequest focusRequest;
+ focusRequest = new AudioFocusRequest.Builder(focusType)
+ .setAudioAttributes(mAudioAttributes)
+ .setOnAudioFocusChangeListener(mAudioFocusListener)
+ .build();
+ result = mAudioManager.requestAudioFocus(focusRequest);
+ } else {
+ result = mAudioManager.requestAudioFocus(mAudioFocusListener,
+ AudioManager.STREAM_MUSIC,
+ focusType);
+ }
+ if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
+ mAudioFocused = false;
+ } else if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+ mAudioFocused = true;
+ } else if (result == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
+ mAudioFocused = false;
+ }
+ }
+
+ // Creates a MediaPlayer instance and prepare playback.
+ private void openVideo(Uri uri, Map<String, String> headers) {
+ resetPlayer();
+ mUri = uri;
+ if (isRemotePlayback()) {
+ // TODO: b/77556429
+ mRoutePlayer.openVideo(uri);
+ return;
+ }
+
+ try {
+ Log.d(TAG, "openVideo(): creating new MediaPlayer instance.");
+ mMediaPlayer = new MediaPlayer();
+ final Context context = mInstance.getContext();
+ setupMediaPlayer(context, uri, headers);
+
+ // we don't set the target state here either, but preserve the
+ // target state that was there before.
+ mCurrentState = STATE_PREPARING;
+ mMediaPlayer.prepareAsync();
+
+ // Save file name as title since the file may not have a title Metadata.
+ mTitle = uri.getPath();
+ String scheme = uri.getScheme();
+ if (scheme != null && scheme.equals("file")) {
+ mTitle = uri.getLastPathSegment();
+ mRetriever = new MediaMetadataRetriever();
+ mRetriever.setDataSource(context, uri);
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "openVideo(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
+ } catch (IOException | IllegalArgumentException ex) {
+ Log.w(TAG, "Unable to open content: " + uri, ex);
+ mCurrentState = STATE_ERROR;
+ mTargetState = STATE_ERROR;
+ mErrorListener.onError(mMediaPlayer,
+ MediaPlayer.MEDIA_ERROR_UNKNOWN, MediaPlayer.MEDIA_ERROR_IO);
+ }
+ }
+
+ /**
+ * Used in openVideo(). Setup MediaPlayer and related objects before calling prepare.
+ */
+ protected void setupMediaPlayer(Context context, Uri uri, Map<String, String> headers)
+ throws IOException {
+ mSurfaceView.setMediaPlayer(mMediaPlayer);
+ mTextureView.setMediaPlayer(mMediaPlayer);
+ mCurrentView.assignSurfaceToMediaPlayer(mMediaPlayer);
+
+ mMediaPlayer.setOnPreparedListener(mPreparedListener);
+ mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
+ mMediaPlayer.setOnCompletionListener(mCompletionListener);
+ mMediaPlayer.setOnSeekCompleteListener(mSeekCompleteListener);
+ mMediaPlayer.setOnErrorListener(mErrorListener);
+ mMediaPlayer.setOnInfoListener(mInfoListener);
+ mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);
+
+ mCurrentBufferPercentage = -1;
+ mMediaPlayer.setDataSource(context, uri, headers);
+ mMediaPlayer.setAudioAttributes(mAudioAttributes);
+ }
+
+ /*
+ * Reset the media player in any state
+ */
+ @SuppressWarnings("deprecation")
+ private void resetPlayer() {
+ if (mMediaPlayer != null) {
+ mMediaPlayer.reset();
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ mTextureView.setMediaPlayer(null);
+ mSurfaceView.setMediaPlayer(null);
+ mCurrentState = STATE_IDLE;
+ mTargetState = STATE_IDLE;
+ if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
+ mAudioManager.abandonAudioFocus(null);
+ }
+ }
+ mVideoWidth = 0;
+ mVideoHeight = 0;
+ }
+
+ private void updatePlaybackState() {
+ if (mStateBuilder == null) {
+ long playbackActions = PlaybackStateCompat.ACTION_PLAY
+ | PlaybackStateCompat.ACTION_PAUSE
+ | PlaybackStateCompat.ACTION_REWIND | PlaybackStateCompat.ACTION_FAST_FORWARD
+ | PlaybackStateCompat.ACTION_SEEK_TO;
+ mStateBuilder = new PlaybackStateCompat.Builder();
+ mStateBuilder.setActions(playbackActions);
+
+ if (mCustomActionList != null) {
+ for (PlaybackStateCompat.CustomAction action : mCustomActionList) {
+ mStateBuilder.addCustomAction(action);
+ }
+ }
+ }
+ mStateBuilder.setState(getCorrespondingPlaybackState(),
+ mMediaPlayer.getCurrentPosition(), mSpeed);
+ if (mCurrentState != STATE_ERROR
+ && mCurrentState != STATE_IDLE
+ && mCurrentState != STATE_PREPARING) {
+ if (mCurrentBufferPercentage == -1) {
+ mStateBuilder.setBufferedPosition(-1);
+ } else {
+ mStateBuilder.setBufferedPosition(
+ (long) (mCurrentBufferPercentage / 100.0 * mMediaPlayer.getDuration()));
+ }
+ }
+
+ // Set PlaybackState for MediaSession
+ if (mMediaSession != null) {
+ PlaybackStateCompat state = mStateBuilder.build();
+ mMediaSession.setPlaybackState(state);
+ }
+ }
+
+ private int getCorrespondingPlaybackState() {
+ switch (mCurrentState) {
+ case STATE_ERROR:
+ return PlaybackStateCompat.STATE_ERROR;
+ case STATE_IDLE:
+ return PlaybackStateCompat.STATE_NONE;
+ case STATE_PREPARING:
+ return PlaybackStateCompat.STATE_CONNECTING;
+ case STATE_PREPARED:
+ return PlaybackStateCompat.STATE_PAUSED;
+ case STATE_PLAYING:
+ return PlaybackStateCompat.STATE_PLAYING;
+ case STATE_PAUSED:
+ return PlaybackStateCompat.STATE_PAUSED;
+ case STATE_PLAYBACK_COMPLETED:
+ return PlaybackStateCompat.STATE_STOPPED;
+ default:
+ return -1;
+ }
+ }
+
+ private final Runnable mFadeOut = new Runnable() {
+ @Override
+ public void run() {
+ if (mCurrentState == STATE_PLAYING) {
+ mMediaControlView.setVisibility(View.GONE);
+ }
+ }
+ };
+
+ private void showController() {
+ if (mMediaControlView == null || !isInPlaybackState()
+ || (isMusicMediaType() && mSizeType == SIZE_TYPE_FULL)) {
+ return;
+ }
+ mMediaControlView.removeCallbacks(mFadeOut);
+ mMediaControlView.setVisibility(View.VISIBLE);
+ if (mShowControllerIntervalMs != 0
+ && !mAccessibilityManager.isTouchExplorationEnabled()) {
+ mMediaControlView.postDelayed(mFadeOut, mShowControllerIntervalMs);
+ }
+ }
+
+ private void toggleMediaControlViewVisibility() {
+ if (mMediaControlView.getVisibility() == View.VISIBLE) {
+ mMediaControlView.removeCallbacks(mFadeOut);
+ mMediaControlView.setVisibility(View.GONE);
+ } else {
+ showController();
+ }
+ }
+
+ private void applySpeed() {
+ if (android.os.Build.VERSION.SDK_INT < 23) {
+ return;
+ }
+ PlaybackParams params = mMediaPlayer.getPlaybackParams().allowDefaults();
+ if (mSpeed != params.getSpeed()) {
+ try {
+ params.setSpeed(mSpeed);
+ mMediaPlayer.setPlaybackParams(params);
+ mFallbackSpeed = mSpeed;
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "PlaybackParams has unsupported value: " + e);
+ float fallbackSpeed = mMediaPlayer.getPlaybackParams().allowDefaults().getSpeed();
+ if (fallbackSpeed > 0.0f) {
+ mFallbackSpeed = fallbackSpeed;
+ }
+ mSpeed = mFallbackSpeed;
+ }
+ }
+ }
+
+ private boolean isRemotePlayback() {
+ if (mMediaController == null) {
+ return false;
+ }
+ PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo();
+ return playbackInfo != null
+ && playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE;
+ }
+
+ protected void extractTracks() {
+ MediaPlayer.TrackInfo[] trackInfos = mMediaPlayer.getTrackInfo();
+ mVideoTrackIndices = new ArrayList<>();
+ mAudioTrackIndices = new ArrayList<>();
+ for (int i = 0; i < trackInfos.length; ++i) {
+ int trackType = trackInfos[i].getTrackType();
+ if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO) {
+ mVideoTrackIndices.add(i);
+ } else if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO) {
+ mAudioTrackIndices.add(i);
+ }
+ }
+ // Select first tracks as default
+ if (mVideoTrackIndices.size() > 0) {
+ mSelectedVideoTrackIndex = 0;
+ }
+ if (mAudioTrackIndices.size() > 0) {
+ mSelectedAudioTrackIndex = 0;
+ }
+
+ Bundle data = new Bundle();
+ data.putInt(MediaControlView2.KEY_VIDEO_TRACK_COUNT, mVideoTrackIndices.size());
+ data.putInt(MediaControlView2.KEY_AUDIO_TRACK_COUNT, mAudioTrackIndices.size());
+ mMediaSession.sendSessionEvent(MediaControlView2.EVENT_UPDATE_TRACK_STATUS, data);
+ }
+
+ protected void doShowSubtitleCommand(Bundle args) {
+ // No-op
+ }
+
+ protected void doHideSubtitleCommand() {
+ // No-op
+ }
+
+ private void extractMetadata() {
+ if (mRetriever == null) {
+ return;
+ }
+ // Get and set duration and title values as MediaMetadata for MediaControlView2
+ MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+ String title = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
+ if (title != null) {
+ mTitle = title;
+ }
+ builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mTitle);
+ builder.putLong(
+ MediaMetadataCompat.METADATA_KEY_DURATION, mMediaPlayer.getDuration());
+
+ if (mMediaSession != null) {
+ mMediaSession.setMetadata(builder.build());
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private void extractAudioMetadata() {
+ if (mRetriever == null || !isMusicMediaType()) {
+ return;
+ }
+
+ mResources = mInstance.getResources();
+ mManager = (WindowManager) mInstance.getContext().getApplicationContext()
+ .getSystemService(Context.WINDOW_SERVICE);
+
+ byte[] album = mRetriever.getEmbeddedPicture();
+ if (album != null) {
+ Bitmap bitmap = BitmapFactory.decodeByteArray(album, 0, album.length);
+ mMusicAlbumDrawable = new BitmapDrawable(bitmap);
+
+ Palette.Builder builder = Palette.from(bitmap);
+ builder.generate(new Palette.PaletteAsyncListener() {
+ @Override
+ public void onGenerated(Palette palette) {
+ mDominantColor = palette.getDominantColor(0);
+ if (mMusicView != null) {
+ mMusicView.setBackgroundColor(mDominantColor);
+ }
+ }
+ });
+ } else {
+ mMusicAlbumDrawable = mResources.getDrawable(R.drawable.ic_default_album_image);
+ }
+
+ String title = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
+ if (title != null) {
+ mMusicTitleText = title;
+ } else {
+ mMusicTitleText = mResources.getString(R.string.mcv2_music_title_unknown_text);
+ }
+
+ String artist = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);
+ if (artist != null) {
+ mMusicArtistText = artist;
+ } else {
+ mMusicArtistText = mResources.getString(R.string.mcv2_music_artist_unknown_text);
+ }
+
+ // Send title and artist string to MediaControlView2
+ MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+ builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mMusicTitleText);
+ builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, mMusicArtistText);
+ mMediaSession.setMetadata(builder.build());
+
+ // Display Embedded mode as default
+ mInstance.removeView(mSurfaceView);
+ mInstance.removeView(mTextureView);
+ inflateMusicView(R.layout.embedded_music);
+ }
+
+ private int retrieveOrientation() {
+ DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
+ int width = dm.widthPixels;
+ int height = dm.heightPixels;
+
+ return (height > width)
+ ? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+ : ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+ }
+
+ private void inflateMusicView(int layoutId) {
+ mInstance.removeView(mMusicView);
+
+ LayoutInflater inflater = (LayoutInflater) mInstance.getContext()
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View v = inflater.inflate(layoutId, null);
+ v.setBackgroundColor(mDominantColor);
+
+ ImageView albumView = v.findViewById(R.id.album);
+ if (albumView != null) {
+ albumView.setImageDrawable(mMusicAlbumDrawable);
+ }
+
+ TextView titleView = v.findViewById(R.id.title);
+ if (titleView != null) {
+ titleView.setText(mMusicTitleText);
+ }
+
+ TextView artistView = v.findViewById(R.id.artist);
+ if (artistView != null) {
+ artistView.setText(mMusicArtistText);
+ }
+
+ mMusicView = v;
+ mInstance.addView(mMusicView, 0);
+ }
+
+ private MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener =
+ new MediaPlayer.OnVideoSizeChangedListener() {
+ @Override
+ public void onVideoSizeChanged(
+ MediaPlayer mp, int width, int height) {
+ if (DEBUG) {
+ Log.d(TAG, "onVideoSizeChanged(): size: " + width + "/" + height);
+ }
+ mVideoWidth = mp.getVideoWidth();
+ mVideoHeight = mp.getVideoHeight();
+ if (DEBUG) {
+ Log.d(TAG, "onVideoSizeChanged(): mVideoSize:" + mVideoWidth + "/"
+ + mVideoHeight);
+ }
+ if (mVideoWidth != 0 && mVideoHeight != 0) {
+ mInstance.requestLayout();
+ }
+ }
+ };
+
+ private MediaPlayer.OnPreparedListener mPreparedListener =
+ new MediaPlayer.OnPreparedListener() {
+ @Override
+ public void onPrepared(MediaPlayer mp) {
+ if (DEBUG) {
+ Log.d(TAG, "OnPreparedListener(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
+ mCurrentState = STATE_PREPARED;
+ // Create and set playback state for MediaControlView2
+ updatePlaybackState();
+
+ if (mMediaSession != null) {
+ extractTracks();
+ extractMetadata();
+ extractAudioMetadata();
+ }
+
+ if (mMediaControlView != null) {
+ mMediaControlView.setEnabled(true);
+ }
+ int videoWidth = mp.getVideoWidth();
+ int videoHeight = mp.getVideoHeight();
+
+ // mSeekWhenPrepared may be changed after seekTo() call
+ long seekToPosition = mSeekWhenPrepared;
+ if (seekToPosition != 0) {
+ mMediaController.getTransportControls().seekTo(seekToPosition);
+ }
+
+ if (videoWidth != 0 && videoHeight != 0) {
+ if (videoWidth != mVideoWidth || videoHeight != mVideoHeight) {
+ mVideoWidth = videoWidth;
+ mVideoHeight = videoHeight;
+ mInstance.requestLayout();
+ }
+
+ if (needToStart()) {
+ mMediaController.getTransportControls().play();
+ }
+ } else {
+ // We don't know the video size yet, but should start anyway.
+ // The video size might be reported to us later.
+ if (needToStart()) {
+ mMediaController.getTransportControls().play();
+ }
+ }
+ // Get and set duration and title values as MediaMetadata for MediaControlView2
+ MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+
+ builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mTitle);
+ builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, mMediaPlayer.getDuration());
+
+ if (mMediaSession != null) {
+ mMediaSession.setMetadata(builder.build());
+
+ if (mNeedUpdateMediaType) {
+ mMediaSession.sendSessionEvent(
+ MediaControlView2.EVENT_UPDATE_MEDIA_TYPE_STATUS, mMediaTypeData);
+ mNeedUpdateMediaType = false;
+ }
+ }
+ }
+ };
+
+ private MediaPlayer.OnSeekCompleteListener mSeekCompleteListener =
+ new MediaPlayer.OnSeekCompleteListener() {
+ @Override
+ public void onSeekComplete(MediaPlayer mp) {
+ updatePlaybackState();
+ }
+ };
+
+ private MediaPlayer.OnCompletionListener mCompletionListener =
+ new MediaPlayer.OnCompletionListener() {
+ @Override
+ @SuppressWarnings("deprecation")
+ public void onCompletion(MediaPlayer mp) {
+ mCurrentState = STATE_PLAYBACK_COMPLETED;
+ mTargetState = STATE_PLAYBACK_COMPLETED;
+ updatePlaybackState();
+ if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
+ mAudioManager.abandonAudioFocus(null);
+ }
+ }
+ };
+
+ private MediaPlayer.OnInfoListener mInfoListener = new MediaPlayer.OnInfoListener() {
+ @Override
+ public boolean onInfo(MediaPlayer mp, int what, int extra) {
+ if (what == MediaPlayer.MEDIA_INFO_METADATA_UPDATE) {
+ extractTracks();
+ }
+ return true;
+ }
+ };
+
+ private MediaPlayer.OnErrorListener mErrorListener = new MediaPlayer.OnErrorListener() {
+ @Override
+ public boolean onError(MediaPlayer mp, int frameworkErr, int implErr) {
+ if (DEBUG) {
+ Log.d(TAG, "Error: " + frameworkErr + "," + implErr);
+ }
+ mCurrentState = STATE_ERROR;
+ mTargetState = STATE_ERROR;
+ updatePlaybackState();
+
+ if (mMediaControlView != null) {
+ mMediaControlView.setVisibility(View.GONE);
+ }
+ return true;
+ }
+ };
+
+ private MediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener =
+ new MediaPlayer.OnBufferingUpdateListener() {
+ @Override
+ public void onBufferingUpdate(MediaPlayer mp, int percent) {
+ mCurrentBufferPercentage = percent;
+ updatePlaybackState();
+ }
+ };
+
+ private class MediaSessionCallback extends MediaSessionCompat.Callback {
+ @Override
+ public void onCommand(String command, Bundle args, ResultReceiver receiver) {
+ if (isRemotePlayback()) {
+ mRoutePlayer.onCommand(command, args, receiver);
+ } else {
+ switch (command) {
+ case MediaControlView2.COMMAND_SHOW_SUBTITLE:
+ doShowSubtitleCommand(args);
+ break;
+ case MediaControlView2.COMMAND_HIDE_SUBTITLE:
+ doHideSubtitleCommand();
+ break;
+ case MediaControlView2.COMMAND_SELECT_AUDIO_TRACK:
+ int audioIndex = args.getInt(MediaControlView2.KEY_SELECTED_AUDIO_INDEX,
+ INVALID_TRACK_INDEX);
+ if (audioIndex != INVALID_TRACK_INDEX) {
+ int audioTrackIndex = mAudioTrackIndices.get(audioIndex);
+ if (audioTrackIndex != mSelectedAudioTrackIndex) {
+ mSelectedAudioTrackIndex = audioTrackIndex;
+ mMediaPlayer.selectTrack(mSelectedAudioTrackIndex);
+ }
+ }
+ break;
+ case MediaControlView2.COMMAND_SET_PLAYBACK_SPEED:
+ float speed = args.getFloat(
+ MediaControlView2.KEY_PLAYBACK_SPEED, INVALID_SPEED);
+ if (speed != INVALID_SPEED && speed != mSpeed) {
+ setSpeed(speed);
+ mSpeed = speed;
+ }
+ break;
+ case MediaControlView2.COMMAND_MUTE:
+ mVolumeLevel = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
+ mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 0, 0);
+ break;
+ case MediaControlView2.COMMAND_UNMUTE:
+ mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mVolumeLevel, 0);
+ break;
+ }
+ }
+ showController();
+ }
+
+ @Override
+ public void onCustomAction(final String action, final Bundle extras) {
+ mCustomActionListenerRecord.first.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCustomActionListenerRecord.second.onCustomAction(action, extras);
+ }
+ });
+ showController();
+ }
+
+ @Override
+ public void onPlay() {
+ if (!isAudioGranted()) {
+ requestAudioFocus(mAudioFocusType);
+ }
+
+ if ((isInPlaybackState() && mCurrentView.hasAvailableSurface()) || isMusicMediaType()) {
+ if (isRemotePlayback()) {
+ mRoutePlayer.onPlay();
+ } else {
+ applySpeed();
+ mMediaPlayer.start();
+ mCurrentState = STATE_PLAYING;
+ updatePlaybackState();
+ }
+ mCurrentState = STATE_PLAYING;
+ }
+ mTargetState = STATE_PLAYING;
+ if (DEBUG) {
+ Log.d(TAG, "onPlay(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
+ showController();
+ }
+
+ @Override
+ public void onPause() {
+ if (isInPlaybackState()) {
+ if (isRemotePlayback()) {
+ mRoutePlayer.onPlay();
+ mCurrentState = STATE_PAUSED;
+ } else if (mMediaPlayer.isPlaying()) {
+ mMediaPlayer.pause();
+ mCurrentState = STATE_PAUSED;
+ updatePlaybackState();
+ }
+ }
+ mTargetState = STATE_PAUSED;
+ if (DEBUG) {
+ Log.d(TAG, "onPause(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
+ showController();
+ }
+
+ @Override
+ public void onSeekTo(long pos) {
+ if (isInPlaybackState()) {
+ if (isRemotePlayback()) {
+ mRoutePlayer.onPlay();
+ } else {
+ if (android.os.Build.VERSION.SDK_INT < 26) {
+ mMediaPlayer.seekTo((int) pos);
+ } else {
+ mMediaPlayer.seekTo(pos, MediaPlayer.SEEK_PREVIOUS_SYNC);
+ }
+ mSeekWhenPrepared = 0;
+ }
+ } else {
+ mSeekWhenPrepared = pos;
+ }
+ showController();
+ }
+
+ @Override
+ public void onStop() {
+ if (isRemotePlayback()) {
+ mRoutePlayer.onPlay();
+ } else {
+ resetPlayer();
+ }
+ showController();
+ }
+ }
+}
diff --git a/media-widget/src/main/java/androidx/media/widget/VideoViewInterface.java b/media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoViewInterfaceWithMp1.java
similarity index 92%
copy from media-widget/src/main/java/androidx/media/widget/VideoViewInterface.java
copy to media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoViewInterfaceWithMp1.java
index 81b40a9..a3bac28 100644
--- a/media-widget/src/main/java/androidx/media/widget/VideoViewInterface.java
+++ b/media-widget/src/main/java/androidx/media/widget/impl_with_mp1/VideoViewInterfaceWithMp1.java
@@ -21,7 +21,7 @@
import androidx.annotation.NonNull;
-interface VideoViewInterface {
+interface VideoViewInterfaceWithMp1 {
/**
* Assigns the view's surface to the given MediaPlayer instance.
*
@@ -44,7 +44,7 @@
*
* @param oldView The view that MediaPlayer is currently rendering on.
*/
- void takeOver(@NonNull VideoViewInterface oldView);
+ void takeOver(@NonNull VideoViewInterfaceWithMp1 oldView);
/**
* Indicates if the view's surface is available.
@@ -61,6 +61,6 @@
void onSurfaceCreated(View view, int width, int height);
void onSurfaceDestroyed(View view);
void onSurfaceChanged(View view, int width, int height);
- void onSurfaceTakeOverDone(VideoViewInterface view);
+ void onSurfaceTakeOverDone(VideoViewInterfaceWithMp1 view);
}
}
diff --git a/media-widget/src/main/res/layout/embedded_settings_list_item.xml b/media-widget/src/main/res/layout/embedded_settings_list_item.xml
index 1156dca..c690c48 100644
--- a/media-widget/src/main/res/layout/embedded_settings_list_item.xml
+++ b/media-widget/src/main/res/layout/embedded_settings_list_item.xml
@@ -16,48 +16,50 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
- android:layout_height="@dimen/mcv2_embedded_settings_height"
+ android:layout_height="@dimen/mcv2_settings_height"
android:orientation="horizontal"
android:background="@color/black_opacity_70">
<LinearLayout
android:layout_width="wrap_content"
- android:layout_height="@dimen/mcv2_embedded_settings_height"
+ android:layout_height="@dimen/mcv2_settings_height"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:id="@+id/icon"
- android:layout_width="@dimen/mcv2_embedded_settings_icon_size"
- android:layout_height="@dimen/mcv2_embedded_settings_icon_size"
+ android:layout_width="@dimen/mcv2_settings_icon_size"
+ android:layout_height="@dimen/mcv2_settings_icon_size"
android:layout_margin="8dp"
android:gravity="center" />
</LinearLayout>
- <RelativeLayout
+ <LinearLayout
android:layout_width="wrap_content"
- android:layout_height="@dimen/mcv2_embedded_settings_height"
- android:gravity="center"
- android:orientation="vertical">
+ android:layout_height="match_parent"
+ android:gravity="center|left">
- <TextView
- android:id="@+id/main_text"
+ <LinearLayout
android:layout_width="wrap_content"
- android:layout_height="@dimen/mcv2_embedded_settings_text_height"
- android:gravity="center"
- android:paddingLeft="2dp"
- android:textColor="@color/white"
- android:textSize="@dimen/mcv2_embedded_settings_main_text_size"/>
+ android:layout_height="wrap_content"
+ android:gravity="center|left"
+ android:orientation="vertical">
- <TextView
- android:id="@+id/sub_text"
- android:layout_width="wrap_content"
- android:layout_height="@dimen/mcv2_embedded_settings_text_height"
- android:layout_below="@id/main_text"
- android:gravity="center"
- android:paddingLeft="2dp"
- android:textColor="@color/white_opacity_70"
- android:textSize="@dimen/mcv2_embedded_settings_sub_text_size"/>
- </RelativeLayout>
+ <TextView
+ android:id="@+id/main_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingLeft="2dp"
+ android:textColor="@color/white"
+ android:textSize="@dimen/mcv2_settings_main_text_size"/>
+
+ <TextView
+ android:id="@+id/sub_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingLeft="2dp"
+ android:textColor="@color/white_opacity_70"
+ android:textSize="@dimen/mcv2_settings_sub_text_size"/>
+ </LinearLayout>
+ </LinearLayout>
</LinearLayout>
-
diff --git a/media-widget/src/main/res/layout/embedded_sub_settings_list_item.xml b/media-widget/src/main/res/layout/embedded_sub_settings_list_item.xml
index 5947a72..c455504 100644
--- a/media-widget/src/main/res/layout/embedded_sub_settings_list_item.xml
+++ b/media-widget/src/main/res/layout/embedded_sub_settings_list_item.xml
@@ -16,20 +16,20 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
- android:layout_height="@dimen/mcv2_embedded_settings_height"
+ android:layout_height="@dimen/mcv2_settings_height"
android:orientation="horizontal"
android:background="@color/black_opacity_70">
<LinearLayout
android:layout_width="wrap_content"
- android:layout_height="@dimen/mcv2_embedded_settings_height"
+ android:layout_height="@dimen/mcv2_settings_height"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:id="@+id/check"
- android:layout_width="@dimen/mcv2_embedded_settings_icon_size"
- android:layout_height="@dimen/mcv2_embedded_settings_icon_size"
+ android:layout_width="@dimen/mcv2_settings_icon_size"
+ android:layout_height="@dimen/mcv2_settings_icon_size"
android:layout_margin="8dp"
android:gravity="center"
android:src="@drawable/ic_check"/>
@@ -37,17 +37,17 @@
<RelativeLayout
android:layout_width="wrap_content"
- android:layout_height="@dimen/mcv2_embedded_settings_height"
+ android:layout_height="@dimen/mcv2_settings_height"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
- android:layout_height="@dimen/mcv2_embedded_settings_text_height"
+ android:layout_height="@dimen/mcv2_settings_text_height"
android:gravity="center"
android:paddingLeft="2dp"
android:textColor="@color/white"
- android:textSize="@dimen/mcv2_embedded_settings_main_text_size"/>
+ android:textSize="@dimen/mcv2_settings_main_text_size"/>
</RelativeLayout>
</LinearLayout>
diff --git a/media-widget/src/main/res/layout/embedded_transport_controls.xml b/media-widget/src/main/res/layout/embedded_transport_controls.xml
index a3a5957..89b98b7 100644
--- a/media-widget/src/main/res/layout/embedded_transport_controls.xml
+++ b/media-widget/src/main/res/layout/embedded_transport_controls.xml
@@ -15,12 +15,10 @@
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
- android:paddingLeft="@dimen/mcv2_transport_controls_padding"
- android:paddingRight="@dimen/mcv2_transport_controls_padding"
android:visibility="visible">
<ImageButton android:id="@+id/prev" style="@style/EmbeddedTransportControlsButton.Previous" />
diff --git a/media-widget/src/main/res/layout/full_settings_list_item.xml b/media-widget/src/main/res/layout/full_settings_list_item.xml
index f92ea5e..c4406d9 100644
--- a/media-widget/src/main/res/layout/full_settings_list_item.xml
+++ b/media-widget/src/main/res/layout/full_settings_list_item.xml
@@ -16,47 +16,50 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
- android:layout_height="@dimen/mcv2_full_settings_height"
+ android:layout_height="@dimen/mcv2_settings_height"
android:orientation="horizontal"
android:background="@color/black_opacity_70">
<LinearLayout
android:layout_width="wrap_content"
- android:layout_height="@dimen/mcv2_full_settings_height"
+ android:layout_height="@dimen/mcv2_settings_height"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:id="@+id/icon"
- android:layout_width="@dimen/mcv2_full_settings_icon_size"
- android:layout_height="@dimen/mcv2_full_settings_icon_size"
+ android:layout_width="@dimen/mcv2_settings_icon_size"
+ android:layout_height="@dimen/mcv2_settings_icon_size"
android:layout_margin="8dp"
android:gravity="center"/>
</LinearLayout>
- <RelativeLayout
+ <LinearLayout
android:layout_width="wrap_content"
- android:layout_height="@dimen/mcv2_full_settings_height"
- android:gravity="center"
- android:orientation="vertical">
+ android:layout_height="match_parent"
+ android:gravity="center|left">
- <TextView
- android:id="@+id/main_text"
+ <LinearLayout
android:layout_width="wrap_content"
- android:layout_height="@dimen/mcv2_full_settings_text_height"
- android:paddingLeft="2dp"
- android:gravity="center"
- android:textColor="@color/white"
- android:textSize="@dimen/mcv2_full_settings_main_text_size"/>
+ android:layout_height="wrap_content"
+ android:gravity="center|left"
+ android:orientation="vertical">
- <TextView
- android:id="@+id/sub_text"
- android:layout_width="wrap_content"
- android:layout_height="@dimen/mcv2_full_settings_text_height"
- android:layout_below="@id/main_text"
- android:gravity="center"
- android:paddingLeft="2dp"
- android:textColor="@color/white_opacity_70"
- android:textSize="@dimen/mcv2_full_settings_sub_text_size"/>
- </RelativeLayout>
+ <TextView
+ android:id="@+id/main_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingLeft="2dp"
+ android:textColor="@color/white"
+ android:textSize="@dimen/mcv2_settings_main_text_size"/>
+
+ <TextView
+ android:id="@+id/sub_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingLeft="2dp"
+ android:textColor="@color/white_opacity_70"
+ android:textSize="@dimen/mcv2_settings_sub_text_size"/>
+ </LinearLayout>
+ </LinearLayout>
</LinearLayout>
diff --git a/media-widget/src/main/res/layout/full_sub_settings_list_item.xml b/media-widget/src/main/res/layout/full_sub_settings_list_item.xml
index 49128d0..3bd4ed1 100644
--- a/media-widget/src/main/res/layout/full_sub_settings_list_item.xml
+++ b/media-widget/src/main/res/layout/full_sub_settings_list_item.xml
@@ -16,20 +16,20 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
- android:layout_height="@dimen/mcv2_full_settings_height"
+ android:layout_height="@dimen/mcv2_settings_height"
android:orientation="horizontal"
android:background="@color/black_opacity_70">
<LinearLayout
android:layout_width="wrap_content"
- android:layout_height="@dimen/mcv2_full_settings_height"
+ android:layout_height="@dimen/mcv2_settings_height"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:id="@+id/check"
- android:layout_width="@dimen/mcv2_full_settings_icon_size"
- android:layout_height="@dimen/mcv2_full_settings_icon_size"
+ android:layout_width="@dimen/mcv2_settings_icon_size"
+ android:layout_height="@dimen/mcv2_settings_icon_size"
android:layout_margin="8dp"
android:gravity="center"
android:src="@drawable/ic_check"/>
@@ -37,17 +37,17 @@
<RelativeLayout
android:layout_width="wrap_content"
- android:layout_height="@dimen/mcv2_full_settings_height"
+ android:layout_height="@dimen/mcv2_settings_height"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
- android:layout_height="@dimen/mcv2_full_settings_text_height"
+ android:layout_height="@dimen/mcv2_settings_text_height"
android:gravity="center"
android:paddingLeft="2dp"
android:textColor="@color/white"
- android:textSize="@dimen/mcv2_full_settings_main_text_size"/>
+ android:textSize="@dimen/mcv2_settings_main_text_size"/>
</RelativeLayout>
</LinearLayout>
\ No newline at end of file
diff --git a/media-widget/src/main/res/layout/full_transport_controls.xml b/media-widget/src/main/res/layout/full_transport_controls.xml
index 0914785..f5d8b00 100644
--- a/media-widget/src/main/res/layout/full_transport_controls.xml
+++ b/media-widget/src/main/res/layout/full_transport_controls.xml
@@ -15,12 +15,10 @@
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
- android:paddingLeft="@dimen/mcv2_transport_controls_padding"
- android:paddingRight="@dimen/mcv2_transport_controls_padding"
android:visibility="visible">
<ImageButton android:id="@+id/prev" style="@style/FullTransportControlsButton.Previous" />
diff --git a/media-widget/src/main/res/layout/media_controller.xml b/media-widget/src/main/res/layout/media_controller.xml
index 8749a7b..7fec8cc 100644
--- a/media-widget/src/main/res/layout/media_controller.xml
+++ b/media-widget/src/main/res/layout/media_controller.xml
@@ -29,19 +29,16 @@
android:id="@+id/title_bar_left"
android:gravity="center"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentLeft="true"
+ android:layout_height="match_parent"
+ android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:orientation="horizontal">
<ImageButton
android:id="@+id/back"
android:clickable="true"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
- android:paddingLeft="5dip"
android:visibility="visible"
style="@style/TitleBarButton.Back"/>
@@ -53,8 +50,7 @@
android:layout_toRightOf="@id/back"
android:layout_centerVertical="true"
android:maxLines="1"
- android:paddingLeft="5dip"
- android:paddingRight="5dip"
+ android:paddingStart="@dimen/mcv2_embedded_icon_padding"
android:textSize="15sp"
android:textColor="#FFFFFFFF"/>
</LinearLayout>
@@ -99,13 +95,12 @@
style="@style/TitleBarButton.Launch" />
</LinearLayout>
- <!-- TODO (b/77158231): Causes java.lang.ClassNotFoundException as of Apr 02 2018 -->
- <!--view class="androidx.mediarouter.app.MediaRouteButton"
+ <view class="androidx.mediarouter.app.MediaRouteButton"
android:id="@+id/cast"
android:layout_centerVertical="true"
android:visibility="gone"
android:contentDescription="@string/mr_button_content_description"
- style="@style/TitleBarButton" /-->
+ style="@style/TitleBarButton" />
</LinearLayout>
</RelativeLayout>
@@ -157,9 +152,9 @@
<RelativeLayout
android:id="@+id/bottom_bar"
android:layout_width="match_parent"
- android:layout_height="44dp"
- android:orientation="horizontal"
- android:background="@color/bottom_bar_background">
+ android:layout_height="@dimen/mcv2_bottom_bar_height"
+ android:background="@color/bottom_bar_background"
+ android:orientation="horizontal">
<LinearLayout
android:id="@+id/bottom_bar_left"
@@ -170,12 +165,12 @@
<TextView
android:id="@+id/ad_skip_time"
- android:gravity="center"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="4dp"
- android:textSize="12sp"
+ android:gravity="center"
android:textColor="#FFFFFF"
+ android:textSize="12sp"
android:visibility="gone" />
</LinearLayout>
@@ -184,19 +179,21 @@
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_toRightOf="@id/bottom_bar_left"
+ android:gravity="center"
android:paddingLeft="10dp"
- android:paddingRight="10dp"
- android:gravity="center" >
+ android:paddingRight="10dp">
<TextView
android:id="@+id/time_current"
- style="@style/TimeText.Current"/>
+ style="@style/TimeText.Current" />
+
<TextView
android:id="@+id/time_interpunct"
- style="@style/TimeText.Interpunct"/>
+ style="@style/TimeText.Interpunct" />
+
<TextView
android:id="@+id/time_end"
- style="@style/TimeText.End"/>
+ style="@style/TimeText.End" />
</LinearLayout>
<LinearLayout
@@ -204,63 +201,68 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
- android:gravity="right">
+ android:layout_centerVertical="true">
<LinearLayout
android:id="@+id/basic_controls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:gravity="center_vertical"
- android:orientation="horizontal" >
+ android:gravity="center"
+ android:orientation="horizontal">
<TextView
android:id="@+id/ad_remaining"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
- android:textSize="12sp"
android:textColor="#FFFFFF"
+ android:textSize="12sp"
android:visibility="gone" />
<ImageButton
android:id="@+id/mute"
style="@style/BottomBarButton.Mute" />
+
<ImageButton
android:id="@+id/subtitle"
+ style="@style/BottomBarButton.CC"
android:scaleType="fitCenter"
- android:visibility="gone"
- style="@style/BottomBarButton.CC" />
+ android:visibility="gone" />
+
<ImageButton
android:id="@+id/fullscreen"
- style="@style/BottomBarButton.FullScreen"/>
+ style="@style/BottomBarButton.FullScreen" />
+
<ImageButton
android:id="@+id/overflow_right"
- style="@style/BottomBarButton.OverflowRight"/>
+ style="@style/BottomBarButton.OverflowRight" />
</LinearLayout>
<LinearLayout
android:id="@+id/extra_controls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:visibility="gone"
+ android:gravity="center"
android:orientation="horizontal"
- android:gravity="center_vertical">
+ android:visibility="gone">
<LinearLayout
android:id="@+id/custom_buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:orientation="horizontal"/>
+ android:orientation="horizontal" />
<ImageButton
android:id="@+id/video_quality"
style="@style/BottomBarButton.VideoQuality" />
+
<ImageButton
android:id="@+id/settings"
style="@style/BottomBarButton.Settings" />
+
<ImageButton
android:id="@+id/overflow_left"
- style="@style/BottomBarButton.OverflowLeft"/>
+ style="@style/BottomBarButton.OverflowLeft" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
diff --git a/media-widget/src/main/res/layout/minimal_transport_controls.xml b/media-widget/src/main/res/layout/minimal_transport_controls.xml
index 800c80b..a72adc7 100644
--- a/media-widget/src/main/res/layout/minimal_transport_controls.xml
+++ b/media-widget/src/main/res/layout/minimal_transport_controls.xml
@@ -15,8 +15,8 @@
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
diff --git a/media-widget/src/main/res/layout/settings_list.xml b/media-widget/src/main/res/layout/settings_list.xml
index ea30538..f08f81b 100644
--- a/media-widget/src/main/res/layout/settings_list.xml
+++ b/media-widget/src/main/res/layout/settings_list.xml
@@ -16,7 +16,8 @@
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="@dimen/mcv2_embedded_settings_width"
- android:layout_height="@dimen/mcv2_embedded_settings_height"
+ android:layout_height="@dimen/mcv2_settings_height"
+ android:layout_gravity="center_vertical"
android:divider="@null"
android:dividerHeight="0dp">
</ListView>
\ No newline at end of file
diff --git a/media-widget/src/main/res/values/dimens.xml b/media-widget/src/main/res/values/dimens.xml
index 796f345..f03c871 100644
--- a/media-widget/src/main/res/values/dimens.xml
+++ b/media-widget/src/main/res/values/dimens.xml
@@ -17,25 +17,19 @@
<resources>
<dimen name="mcv2_embedded_settings_width">150dp</dimen>
- <dimen name="mcv2_embedded_settings_height">36dp</dimen>
- <dimen name="mcv2_embedded_settings_icon_size">20dp</dimen>
- <dimen name="mcv2_embedded_settings_text_height">18dp</dimen>
- <dimen name="mcv2_embedded_settings_main_text_size">12sp</dimen>
- <dimen name="mcv2_embedded_settings_sub_text_size">10sp</dimen>
<dimen name="mcv2_full_settings_width">225dp</dimen>
- <dimen name="mcv2_full_settings_height">54dp</dimen>
- <dimen name="mcv2_full_settings_icon_size">30dp</dimen>
- <dimen name="mcv2_full_settings_text_height">27dp</dimen>
- <dimen name="mcv2_full_settings_main_text_size">16sp</dimen>
- <dimen name="mcv2_full_settings_sub_text_size">13sp</dimen>
+ <dimen name="mcv2_settings_height">48dp</dimen>
+ <dimen name="mcv2_settings_icon_size">24dp</dimen>
+ <dimen name="mcv2_settings_text_height">24dp</dimen>
+ <dimen name="mcv2_settings_main_text_size">14sp</dimen>
+ <dimen name="mcv2_settings_sub_text_size">12sp</dimen>
<dimen name="mcv2_settings_offset">8dp</dimen>
- <dimen name="mcv2_transport_controls_padding">4dp</dimen>
- <dimen name="mcv2_pause_icon_size">36dp</dimen>
- <dimen name="mcv2_full_icon_size">28dp</dimen>
- <dimen name="mcv2_embedded_icon_size">24dp</dimen>
- <dimen name="mcv2_minimal_icon_size">24dp</dimen>
+ <dimen name="mcv2_icon_size">48dp</dimen>
<dimen name="mcv2_icon_margin">10dp</dimen>
+ <dimen name="mcv2_pause_icon_padding">6dp</dimen>
+ <dimen name="mcv2_full_icon_padding">10dp</dimen>
+ <dimen name="mcv2_embedded_icon_padding">12dp</dimen>
<dimen name="mcv2_full_album_image_portrait_size">232dp</dimen>
<dimen name="mcv2_full_album_image_landscape_size">176dp</dimen>
@@ -43,4 +37,7 @@
<dimen name="mcv2_custom_progress_max_size">2dp</dimen>
<dimen name="mcv2_custom_progress_thumb_size">12dp</dimen>
<dimen name="mcv2_buffer_view_height">5dp</dimen>
+
+ <dimen name="mcv2_title_bar_height">54dp</dimen>
+ <dimen name="mcv2_bottom_bar_height">58dp</dimen>
</resources>
diff --git a/media-widget/src/main/res/values/styles.xml b/media-widget/src/main/res/values/styles.xml
index fc42bd4..2e03acb 100644
--- a/media-widget/src/main/res/values/styles.xml
+++ b/media-widget/src/main/res/values/styles.xml
@@ -2,91 +2,99 @@
<resources>
<style name="FullTransportControlsButton">
<item name="android:background">@null</item>
- <item name="android:layout_margin">@dimen/mcv2_icon_margin</item>
<item name="android:scaleType">fitXY</item>
</style>
<style name="FullTransportControlsButton.Previous">
<item name="android:src">@drawable/ic_skip_previous</item>
- <item name="android:layout_width">@dimen/mcv2_full_icon_size</item>
- <item name="android:layout_height">@dimen/mcv2_full_icon_size</item>
+ <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+ <item name="android:layout_height">@dimen/mcv2_icon_size</item>
<item name="android:contentDescription">@string/mcv2_previous_button_desc</item>
+ <item name="android:padding">@dimen/mcv2_full_icon_padding</item>
</style>
<style name="FullTransportControlsButton.Next">
<item name="android:src">@drawable/ic_skip_next</item>
- <item name="android:layout_width">@dimen/mcv2_full_icon_size</item>
- <item name="android:layout_height">@dimen/mcv2_full_icon_size</item>
+ <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+ <item name="android:layout_height">@dimen/mcv2_icon_size</item>
<item name="android:contentDescription">@string/mcv2_next_button_desc</item>
+ <item name="android:padding">@dimen/mcv2_full_icon_padding</item>
</style>
<style name="FullTransportControlsButton.Pause">
<item name="android:src">@drawable/ic_pause_circle_filled</item>
- <item name="android:layout_width">@dimen/mcv2_pause_icon_size</item>
- <item name="android:layout_height">@dimen/mcv2_pause_icon_size</item>
+ <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+ <item name="android:layout_height">@dimen/mcv2_icon_size</item>
<item name="android:contentDescription">@string/mcv2_pause_button_desc</item>
+ <item name="android:padding">@dimen/mcv2_pause_icon_padding</item>
</style>
<style name="FullTransportControlsButton.Ffwd">
<item name="android:src">@drawable/ic_forward_30</item>
- <item name="android:layout_width">@dimen/mcv2_full_icon_size</item>
- <item name="android:layout_height">@dimen/mcv2_full_icon_size</item>
+ <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+ <item name="android:layout_height">@dimen/mcv2_icon_size</item>
<item name="android:contentDescription">@string/mcv2_ffwd_button_desc</item>
+ <item name="android:padding">@dimen/mcv2_full_icon_padding</item>
</style>
<style name="FullTransportControlsButton.Rew">
<item name="android:src">@drawable/ic_rewind_10</item>
- <item name="android:layout_width">@dimen/mcv2_full_icon_size</item>
- <item name="android:layout_height">@dimen/mcv2_full_icon_size</item>
+ <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+ <item name="android:layout_height">@dimen/mcv2_icon_size</item>
<item name="android:contentDescription">@string/mcv2_rewind_button_desc</item>
+ <item name="android:padding">@dimen/mcv2_full_icon_padding</item>
</style>
<style name="EmbeddedTransportControlsButton">
<item name="android:background">@null</item>
- <item name="android:layout_margin">@dimen/mcv2_icon_margin</item>
<item name="android:scaleType">fitXY</item>
</style>
<style name="EmbeddedTransportControlsButton.Previous">
<item name="android:src">@drawable/ic_skip_previous</item>
- <item name="android:layout_width">@dimen/mcv2_embedded_icon_size</item>
- <item name="android:layout_height">@dimen/mcv2_embedded_icon_size</item>
+ <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+ <item name="android:layout_height">@dimen/mcv2_icon_size</item>
<item name="android:contentDescription">@string/mcv2_previous_button_desc</item>
+ <item name="android:padding">@dimen/mcv2_embedded_icon_padding</item>
</style>
<style name="EmbeddedTransportControlsButton.Next">
<item name="android:src">@drawable/ic_skip_next</item>
- <item name="android:layout_width">@dimen/mcv2_embedded_icon_size</item>
- <item name="android:layout_height">@dimen/mcv2_embedded_icon_size</item>
+ <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+ <item name="android:layout_height">@dimen/mcv2_icon_size</item>
<item name="android:contentDescription">@string/mcv2_next_button_desc</item>
+ <item name="android:padding">@dimen/mcv2_embedded_icon_padding</item>
</style>
<style name="EmbeddedTransportControlsButton.Pause">
<item name="android:src">@drawable/ic_pause_circle_filled</item>
- <item name="android:layout_width">@dimen/mcv2_pause_icon_size</item>
- <item name="android:layout_height">@dimen/mcv2_pause_icon_size</item>
+ <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+ <item name="android:layout_height">@dimen/mcv2_icon_size</item>
<item name="android:contentDescription">@string/mcv2_pause_button_desc</item>
+ <item name="android:padding">@dimen/mcv2_pause_icon_padding</item>
</style>
<style name="EmbeddedTransportControlsButton.Ffwd">
<item name="android:src">@drawable/ic_forward_30</item>
- <item name="android:layout_width">@dimen/mcv2_embedded_icon_size</item>
- <item name="android:layout_height">@dimen/mcv2_embedded_icon_size</item>
+ <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+ <item name="android:layout_height">@dimen/mcv2_icon_size</item>
<item name="android:contentDescription">@string/mcv2_ffwd_button_desc</item>
+ <item name="android:padding">@dimen/mcv2_embedded_icon_padding</item>
</style>
<style name="EmbeddedTransportControlsButton.Rew">
<item name="android:src">@drawable/ic_rewind_10</item>
- <item name="android:layout_width">@dimen/mcv2_embedded_icon_size</item>
- <item name="android:layout_height">@dimen/mcv2_embedded_icon_size</item>
+ <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+ <item name="android:layout_height">@dimen/mcv2_icon_size</item>
<item name="android:contentDescription">@string/mcv2_rewind_button_desc</item>
+ <item name="android:padding">@dimen/mcv2_embedded_icon_padding</item>
</style>
<style name="MinimalTransportControlsButton">
<item name="android:background">@null</item>
- <item name="android:layout_width">@dimen/mcv2_pause_icon_size</item>
- <item name="android:layout_height">@dimen/mcv2_pause_icon_size</item>
- <item name="android:layout_margin">@dimen/mcv2_icon_margin</item>
+ <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+ <item name="android:layout_height">@dimen/mcv2_icon_size</item>
+ <item name="android:padding">@dimen/mcv2_pause_icon_padding</item>
<item name="android:scaleType">fitXY</item>
<item name="android:src">@drawable/ic_pause_circle_filled</item>
<item name="android:contentDescription">@string/mcv2_pause_button_desc</item>
@@ -94,15 +102,15 @@
<style name="TitleBar">
<item name="android:layout_width">match_parent</item>
- <item name="android:layout_height">46dp</item>
- <item name="android:paddingLeft">5dp</item>
- <item name="android:paddingRight">5dp</item>
+ <item name="android:layout_height">@dimen/mcv2_title_bar_height</item>
</style>
<style name="TitleBarButton">
<item name="android:background">@null</item>
- <item name="android:layout_width">36dp</item>
- <item name="android:layout_height">36dp</item>
+ <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+ <item name="android:layout_height">@dimen/mcv2_icon_size</item>
+ <item name="android:padding">@dimen/mcv2_embedded_icon_padding</item>
+ <item name="android:scaleType">fitXY</item>
</style>
<style name="TitleBarButton.Back">
@@ -142,10 +150,10 @@
<style name="BottomBarButton">
<item name="android:background">@null</item>
- <item name="android:layout_width">@dimen/mcv2_embedded_icon_size</item>
- <item name="android:layout_height">@dimen/mcv2_embedded_icon_size</item>
- <item name="android:layout_margin">@dimen/mcv2_icon_margin</item>
+ <item name="android:layout_width">@dimen/mcv2_icon_size</item>
+ <item name="android:layout_height">@dimen/mcv2_icon_size</item>
<item name="android:gravity">center_horizontal</item>
+ <item name="android:padding">@dimen/mcv2_embedded_icon_padding</item>
<item name="android:scaleType">fitXY</item>
</style>
diff --git a/media/api/current.txt b/media/api/current.txt
index a9e2161..c3c28f5 100644
--- a/media/api/current.txt
+++ b/media/api/current.txt
@@ -300,6 +300,7 @@
method public void addOnActiveChangeListener(android.support.v4.media.session.MediaSessionCompat.OnActiveChangeListener);
method public static android.support.v4.media.session.MediaSessionCompat fromMediaSession(android.content.Context, java.lang.Object);
method public android.support.v4.media.session.MediaControllerCompat getController();
+ method public final androidx.media.MediaSessionManager.RemoteUserInfo getCurrentControllerInfo();
method public java.lang.Object getMediaSession();
method public java.lang.Object getRemoteControlClient();
method public android.support.v4.media.session.MediaSessionCompat.Token getSessionToken();
@@ -600,10 +601,38 @@
method public androidx.media.DataSourceDesc.Builder setStartPosition(long);
}
+ public abstract class Media2DataSource implements java.io.Closeable {
+ ctor public Media2DataSource();
+ method public abstract long getSize() throws java.io.IOException;
+ method public abstract int readAt(long, byte[], int, int) throws java.io.IOException;
+ }
+
+ public class MediaBrowser2 extends androidx.media.MediaController2 {
+ ctor public MediaBrowser2(android.content.Context, androidx.media.SessionToken2, java.util.concurrent.Executor, androidx.media.MediaBrowser2.BrowserCallback);
+ method public void getChildren(java.lang.String, int, int, android.os.Bundle);
+ method public void getItem(java.lang.String);
+ method public void getLibraryRoot(android.os.Bundle);
+ method public void getSearchResult(java.lang.String, int, int, android.os.Bundle);
+ method public void search(java.lang.String, android.os.Bundle);
+ method public void subscribe(java.lang.String, android.os.Bundle);
+ method public void unsubscribe(java.lang.String);
+ }
+
+ public static class MediaBrowser2.BrowserCallback extends androidx.media.MediaController2.ControllerCallback {
+ ctor public MediaBrowser2.BrowserCallback();
+ method public void onChildrenChanged(androidx.media.MediaBrowser2, java.lang.String, int, android.os.Bundle);
+ method public void onGetChildrenDone(androidx.media.MediaBrowser2, java.lang.String, int, int, java.util.List<androidx.media.MediaItem2>, android.os.Bundle);
+ method public void onGetItemDone(androidx.media.MediaBrowser2, java.lang.String, androidx.media.MediaItem2);
+ method public void onGetLibraryRootDone(androidx.media.MediaBrowser2, android.os.Bundle, java.lang.String, android.os.Bundle);
+ method public void onGetSearchResultDone(androidx.media.MediaBrowser2, java.lang.String, int, int, java.util.List<androidx.media.MediaItem2>, android.os.Bundle);
+ method public void onSearchResultChanged(androidx.media.MediaBrowser2, java.lang.String, int, android.os.Bundle);
+ }
+
public abstract class MediaBrowserServiceCompat extends android.app.Service {
ctor public MediaBrowserServiceCompat();
method public void dump(java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[]);
method public final android.os.Bundle getBrowserRootHints();
+ method public final androidx.media.MediaSessionManager.RemoteUserInfo getCurrentBrowserInfo();
method public android.support.v4.media.session.MediaSessionCompat.Token getSessionToken();
method public void notifyChildrenChanged(java.lang.String);
method public void notifyChildrenChanged(java.lang.String, android.os.Bundle);
@@ -739,6 +768,48 @@
method public androidx.media.MediaItem2.Builder setMetadata(androidx.media.MediaMetadata2);
}
+ public abstract class MediaLibraryService2 extends androidx.media.MediaSessionService2 {
+ ctor public MediaLibraryService2();
+ method public abstract androidx.media.MediaLibraryService2.MediaLibrarySession onCreateSession(java.lang.String);
+ field public static final java.lang.String SERVICE_INTERFACE = "android.media.MediaLibraryService2";
+ }
+
+ public static final class MediaLibraryService2.LibraryRoot {
+ ctor public MediaLibraryService2.LibraryRoot(java.lang.String, android.os.Bundle);
+ method public android.os.Bundle getExtras();
+ method public java.lang.String getRootId();
+ field public static final java.lang.String EXTRA_OFFLINE = "android.media.extra.OFFLINE";
+ field public static final java.lang.String EXTRA_RECENT = "android.media.extra.RECENT";
+ field public static final java.lang.String EXTRA_SUGGESTED = "android.media.extra.SUGGESTED";
+ }
+
+ public static final class MediaLibraryService2.MediaLibrarySession extends androidx.media.MediaSession2 {
+ method public void notifyChildrenChanged(androidx.media.MediaSession2.ControllerInfo, java.lang.String, int, android.os.Bundle);
+ method public void notifyChildrenChanged(java.lang.String, int, android.os.Bundle);
+ method public void notifySearchResultChanged(androidx.media.MediaSession2.ControllerInfo, java.lang.String, int, android.os.Bundle);
+ }
+
+ public static final class MediaLibraryService2.MediaLibrarySession.Builder {
+ ctor public MediaLibraryService2.MediaLibrarySession.Builder(androidx.media.MediaLibraryService2, java.util.concurrent.Executor, androidx.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback);
+ method public androidx.media.MediaLibraryService2.MediaLibrarySession build();
+ method public androidx.media.MediaLibraryService2.MediaLibrarySession.Builder setId(java.lang.String);
+ method public androidx.media.MediaLibraryService2.MediaLibrarySession.Builder setPlayer(androidx.media.MediaPlayerInterface);
+ method public androidx.media.MediaLibraryService2.MediaLibrarySession.Builder setPlaylistAgent(androidx.media.MediaPlaylistAgent);
+ method public androidx.media.MediaLibraryService2.MediaLibrarySession.Builder setSessionActivity(android.app.PendingIntent);
+ method public androidx.media.MediaLibraryService2.MediaLibrarySession.Builder setVolumeProvider(androidx.media.VolumeProviderCompat);
+ }
+
+ public static class MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback extends androidx.media.MediaSession2.SessionCallback {
+ ctor public MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback();
+ method public java.util.List<androidx.media.MediaItem2> onGetChildren(androidx.media.MediaLibraryService2.MediaLibrarySession, androidx.media.MediaSession2.ControllerInfo, java.lang.String, int, int, android.os.Bundle);
+ method public androidx.media.MediaItem2 onGetItem(androidx.media.MediaLibraryService2.MediaLibrarySession, androidx.media.MediaSession2.ControllerInfo, java.lang.String);
+ method public androidx.media.MediaLibraryService2.LibraryRoot onGetLibraryRoot(androidx.media.MediaLibraryService2.MediaLibrarySession, androidx.media.MediaSession2.ControllerInfo, android.os.Bundle);
+ method public java.util.List<androidx.media.MediaItem2> onGetSearchResult(androidx.media.MediaLibraryService2.MediaLibrarySession, androidx.media.MediaSession2.ControllerInfo, java.lang.String, int, int, android.os.Bundle);
+ method public void onSearch(androidx.media.MediaLibraryService2.MediaLibrarySession, androidx.media.MediaSession2.ControllerInfo, java.lang.String, android.os.Bundle);
+ method public void onSubscribe(androidx.media.MediaLibraryService2.MediaLibrarySession, androidx.media.MediaSession2.ControllerInfo, java.lang.String, android.os.Bundle);
+ method public void onUnsubscribe(androidx.media.MediaLibraryService2.MediaLibrarySession, androidx.media.MediaSession2.ControllerInfo, java.lang.String);
+ }
+
public final class MediaMetadata2 {
method public boolean containsKey(java.lang.String);
method public static androidx.media.MediaMetadata2 fromBundle(android.os.Bundle);
@@ -810,8 +881,201 @@
method public androidx.media.MediaMetadata2.Builder setExtras(android.os.Bundle);
}
- public abstract class MediaPlayerBase implements java.lang.AutoCloseable {
- ctor public MediaPlayerBase();
+ public abstract class MediaPlayer2 {
+ method public abstract void attachAuxEffect(int);
+ method public abstract void clearDrmEventCallback();
+ method public abstract void clearMediaPlayer2EventCallback();
+ method public abstract void clearPendingCommands();
+ method public abstract void close();
+ method public static final androidx.media.MediaPlayer2 create();
+ method public abstract void deselectTrack(int);
+ method public abstract androidx.media.AudioAttributesCompat getAudioAttributes();
+ method public abstract int getAudioSessionId();
+ method public abstract long getBufferedPosition();
+ method public abstract androidx.media.DataSourceDesc getCurrentDataSource();
+ method public abstract long getCurrentPosition();
+ method public abstract androidx.media.MediaPlayer2.DrmInfo getDrmInfo();
+ method public abstract android.media.MediaDrm.KeyRequest getDrmKeyRequest(byte[], byte[], java.lang.String, int, java.util.Map<java.lang.String, java.lang.String>) throws androidx.media.MediaPlayer2.NoDrmSchemeException;
+ method public abstract java.lang.String getDrmPropertyString(java.lang.String) throws androidx.media.MediaPlayer2.NoDrmSchemeException;
+ method public abstract long getDuration();
+ method public float getMaxPlayerVolume();
+ method public abstract int getMediaPlayer2State();
+ method public abstract androidx.media.MediaPlayerInterface getMediaPlayerInterface();
+ method public abstract android.os.PersistableBundle getMetrics();
+ method public abstract android.media.PlaybackParams getPlaybackParams();
+ method public float getPlaybackSpeed();
+ method public abstract float getPlayerVolume();
+ method public abstract int getSelectedTrack(int);
+ method public abstract android.media.SyncParams getSyncParams();
+ method public abstract android.media.MediaTimestamp getTimestamp();
+ method public abstract java.util.List<androidx.media.MediaPlayer2.TrackInfo> getTrackInfo();
+ method public abstract int getVideoHeight();
+ method public abstract int getVideoWidth();
+ method public boolean isReversePlaybackSupported();
+ method public abstract void loopCurrent(boolean);
+ method public void notifyWhenCommandLabelReached(java.lang.Object);
+ method public abstract void pause();
+ method public abstract void play();
+ method public abstract void prepare();
+ method public abstract void prepareDrm(java.util.UUID) throws androidx.media.MediaPlayer2.ProvisioningNetworkErrorException, androidx.media.MediaPlayer2.ProvisioningServerErrorException, android.media.ResourceBusyException, android.media.UnsupportedSchemeException;
+ method public abstract byte[] provideDrmKeyResponse(byte[], byte[]) throws android.media.DeniedByServerException, androidx.media.MediaPlayer2.NoDrmSchemeException;
+ method public abstract void releaseDrm() throws androidx.media.MediaPlayer2.NoDrmSchemeException;
+ method public abstract void reset();
+ method public abstract void restoreDrmKeys(byte[]) throws androidx.media.MediaPlayer2.NoDrmSchemeException;
+ method public void seekTo(long);
+ method public abstract void seekTo(long, int);
+ method public abstract void selectTrack(int);
+ method public abstract void setAudioAttributes(androidx.media.AudioAttributesCompat);
+ method public abstract void setAudioSessionId(int);
+ method public abstract void setAuxEffectSendLevel(float);
+ method public abstract void setDataSource(androidx.media.DataSourceDesc);
+ method public abstract void setDrmEventCallback(java.util.concurrent.Executor, androidx.media.MediaPlayer2.DrmEventCallback);
+ method public abstract void setDrmPropertyString(java.lang.String, java.lang.String) throws androidx.media.MediaPlayer2.NoDrmSchemeException;
+ method public abstract void setMediaPlayer2EventCallback(java.util.concurrent.Executor, androidx.media.MediaPlayer2.MediaPlayer2EventCallback);
+ method public abstract void setNextDataSource(androidx.media.DataSourceDesc);
+ method public abstract void setNextDataSources(java.util.List<androidx.media.DataSourceDesc>);
+ method public abstract void setOnDrmConfigHelper(androidx.media.MediaPlayer2.OnDrmConfigHelper);
+ method public abstract void setPlaybackParams(android.media.PlaybackParams);
+ method public abstract void setPlaybackSpeed(float);
+ method public abstract void setPlayerVolume(float);
+ method public abstract void setSurface(android.view.Surface);
+ method public abstract void setSyncParams(android.media.SyncParams);
+ method public abstract void skipToNext();
+ field public static final int CALL_COMPLETED_ATTACH_AUX_EFFECT = 1; // 0x1
+ field public static final int CALL_COMPLETED_DESELECT_TRACK = 2; // 0x2
+ field public static final int CALL_COMPLETED_LOOP_CURRENT = 3; // 0x3
+ field public static final int CALL_COMPLETED_PAUSE = 4; // 0x4
+ field public static final int CALL_COMPLETED_PLAY = 5; // 0x5
+ field public static final int CALL_COMPLETED_PREPARE = 6; // 0x6
+ field public static final int CALL_COMPLETED_SEEK_TO = 14; // 0xe
+ field public static final int CALL_COMPLETED_SELECT_TRACK = 15; // 0xf
+ field public static final int CALL_COMPLETED_SET_AUDIO_ATTRIBUTES = 16; // 0x10
+ field public static final int CALL_COMPLETED_SET_AUDIO_SESSION_ID = 17; // 0x11
+ field public static final int CALL_COMPLETED_SET_AUX_EFFECT_SEND_LEVEL = 18; // 0x12
+ field public static final int CALL_COMPLETED_SET_DATA_SOURCE = 19; // 0x13
+ field public static final int CALL_COMPLETED_SET_NEXT_DATA_SOURCE = 22; // 0x16
+ field public static final int CALL_COMPLETED_SET_NEXT_DATA_SOURCES = 23; // 0x17
+ field public static final int CALL_COMPLETED_SET_PLAYBACK_PARAMS = 24; // 0x18
+ field public static final int CALL_COMPLETED_SET_PLAYBACK_SPEED = 25; // 0x19
+ field public static final int CALL_COMPLETED_SET_PLAYER_VOLUME = 26; // 0x1a
+ field public static final int CALL_COMPLETED_SET_SURFACE = 27; // 0x1b
+ field public static final int CALL_COMPLETED_SET_SYNC_PARAMS = 28; // 0x1c
+ field public static final int CALL_COMPLETED_SKIP_TO_NEXT = 29; // 0x1d
+ field public static final int CALL_STATUS_BAD_VALUE = 2; // 0x2
+ field public static final int CALL_STATUS_ERROR_IO = 4; // 0x4
+ field public static final int CALL_STATUS_ERROR_UNKNOWN = -2147483648; // 0x80000000
+ field public static final int CALL_STATUS_INVALID_OPERATION = 1; // 0x1
+ field public static final int CALL_STATUS_NO_ERROR = 0; // 0x0
+ field public static final int CALL_STATUS_PERMISSION_DENIED = 3; // 0x3
+ field public static final int MEDIAPLAYER2_STATE_ERROR = 1005; // 0x3ed
+ field public static final int MEDIAPLAYER2_STATE_IDLE = 1001; // 0x3e9
+ field public static final int MEDIAPLAYER2_STATE_PAUSED = 1003; // 0x3eb
+ field public static final int MEDIAPLAYER2_STATE_PLAYING = 1004; // 0x3ec
+ field public static final int MEDIAPLAYER2_STATE_PREPARED = 1002; // 0x3ea
+ field public static final int MEDIA_ERROR_IO = -1004; // 0xfffffc14
+ field public static final int MEDIA_ERROR_MALFORMED = -1007; // 0xfffffc11
+ field public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 200; // 0xc8
+ field public static final int MEDIA_ERROR_TIMED_OUT = -110; // 0xffffff92
+ field public static final int MEDIA_ERROR_UNKNOWN = 1; // 0x1
+ field public static final int MEDIA_ERROR_UNSUPPORTED = -1010; // 0xfffffc0e
+ field public static final int MEDIA_INFO_AUDIO_NOT_PLAYING = 804; // 0x324
+ field public static final int MEDIA_INFO_AUDIO_RENDERING_START = 4; // 0x4
+ field public static final int MEDIA_INFO_BAD_INTERLEAVING = 800; // 0x320
+ field public static final int MEDIA_INFO_BUFFERING_END = 702; // 0x2be
+ field public static final int MEDIA_INFO_BUFFERING_START = 701; // 0x2bd
+ field public static final int MEDIA_INFO_BUFFERING_UPDATE = 704; // 0x2c0
+ field public static final int MEDIA_INFO_METADATA_UPDATE = 802; // 0x322
+ field public static final int MEDIA_INFO_NOT_SEEKABLE = 801; // 0x321
+ field public static final int MEDIA_INFO_PLAYBACK_COMPLETE = 5; // 0x5
+ field public static final int MEDIA_INFO_PLAYLIST_END = 6; // 0x6
+ field public static final int MEDIA_INFO_PREPARED = 100; // 0x64
+ field public static final int MEDIA_INFO_SUBTITLE_TIMED_OUT = 902; // 0x386
+ field public static final int MEDIA_INFO_UNKNOWN = 1; // 0x1
+ field public static final int MEDIA_INFO_UNSUPPORTED_SUBTITLE = 901; // 0x385
+ field public static final int MEDIA_INFO_VIDEO_NOT_PLAYING = 805; // 0x325
+ field public static final int MEDIA_INFO_VIDEO_RENDERING_START = 3; // 0x3
+ field public static final int MEDIA_INFO_VIDEO_TRACK_LAGGING = 700; // 0x2bc
+ field public static final int PREPARE_DRM_STATUS_PREPARATION_ERROR = 3; // 0x3
+ field public static final int PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR = 1; // 0x1
+ field public static final int PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR = 2; // 0x2
+ field public static final int PREPARE_DRM_STATUS_SUCCESS = 0; // 0x0
+ field public static final int SEEK_CLOSEST = 3; // 0x3
+ field public static final int SEEK_CLOSEST_SYNC = 2; // 0x2
+ field public static final int SEEK_NEXT_SYNC = 1; // 0x1
+ field public static final int SEEK_PREVIOUS_SYNC = 0; // 0x0
+ field public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT = 1; // 0x1
+ }
+
+ public static abstract class MediaPlayer2.DrmEventCallback {
+ ctor public MediaPlayer2.DrmEventCallback();
+ method public void onDrmInfo(androidx.media.MediaPlayer2, androidx.media.DataSourceDesc, androidx.media.MediaPlayer2.DrmInfo);
+ method public void onDrmPrepared(androidx.media.MediaPlayer2, androidx.media.DataSourceDesc, int);
+ }
+
+ public static abstract class MediaPlayer2.DrmInfo {
+ ctor public MediaPlayer2.DrmInfo();
+ method public abstract java.util.Map<java.util.UUID, byte[]> getPssh();
+ method public abstract java.util.List<java.util.UUID> getSupportedSchemes();
+ }
+
+ public static abstract class MediaPlayer2.MediaPlayer2EventCallback {
+ ctor public MediaPlayer2.MediaPlayer2EventCallback();
+ method public void onCallCompleted(androidx.media.MediaPlayer2, androidx.media.DataSourceDesc, int, int);
+ method public void onCommandLabelReached(androidx.media.MediaPlayer2, java.lang.Object);
+ method public void onError(androidx.media.MediaPlayer2, androidx.media.DataSourceDesc, int, int);
+ method public void onInfo(androidx.media.MediaPlayer2, androidx.media.DataSourceDesc, int, int);
+ method public void onMediaTimeDiscontinuity(androidx.media.MediaPlayer2, androidx.media.DataSourceDesc, android.media.MediaTimestamp);
+ method public void onSubtitleData(androidx.media.MediaPlayer2, androidx.media.DataSourceDesc, android.media.SubtitleData);
+ method public void onTimedMetaDataAvailable(androidx.media.MediaPlayer2, androidx.media.DataSourceDesc, android.media.TimedMetaData);
+ method public void onVideoSizeChanged(androidx.media.MediaPlayer2, androidx.media.DataSourceDesc, int, int);
+ }
+
+ public static final class MediaPlayer2.MetricsConstants {
+ field public static final java.lang.String CODEC_AUDIO = "android.media.mediaplayer.audio.codec";
+ field public static final java.lang.String CODEC_VIDEO = "android.media.mediaplayer.video.codec";
+ field public static final java.lang.String DURATION = "android.media.mediaplayer.durationMs";
+ field public static final java.lang.String ERRORS = "android.media.mediaplayer.err";
+ field public static final java.lang.String ERROR_CODE = "android.media.mediaplayer.errcode";
+ field public static final java.lang.String FRAMES = "android.media.mediaplayer.frames";
+ field public static final java.lang.String FRAMES_DROPPED = "android.media.mediaplayer.dropped";
+ field public static final java.lang.String HEIGHT = "android.media.mediaplayer.height";
+ field public static final java.lang.String MIME_TYPE_AUDIO = "android.media.mediaplayer.audio.mime";
+ field public static final java.lang.String MIME_TYPE_VIDEO = "android.media.mediaplayer.video.mime";
+ field public static final java.lang.String PLAYING = "android.media.mediaplayer.playingMs";
+ field public static final java.lang.String WIDTH = "android.media.mediaplayer.width";
+ }
+
+ public static class MediaPlayer2.NoDrmSchemeException extends android.media.MediaDrmException {
+ ctor public MediaPlayer2.NoDrmSchemeException(java.lang.String);
+ }
+
+ public static abstract interface MediaPlayer2.OnDrmConfigHelper {
+ method public abstract void onDrmConfig(androidx.media.MediaPlayer2, androidx.media.DataSourceDesc);
+ }
+
+ public static class MediaPlayer2.ProvisioningNetworkErrorException extends android.media.MediaDrmException {
+ ctor public MediaPlayer2.ProvisioningNetworkErrorException(java.lang.String);
+ }
+
+ public static class MediaPlayer2.ProvisioningServerErrorException extends android.media.MediaDrmException {
+ ctor public MediaPlayer2.ProvisioningServerErrorException(java.lang.String);
+ }
+
+ public static abstract class MediaPlayer2.TrackInfo {
+ ctor public MediaPlayer2.TrackInfo();
+ method public abstract android.media.MediaFormat getFormat();
+ method public abstract java.lang.String getLanguage();
+ method public abstract int getTrackType();
+ method public abstract java.lang.String toString();
+ field public static final int MEDIA_TRACK_TYPE_AUDIO = 2; // 0x2
+ field public static final int MEDIA_TRACK_TYPE_METADATA = 5; // 0x5
+ field public static final int MEDIA_TRACK_TYPE_SUBTITLE = 4; // 0x4
+ field public static final int MEDIA_TRACK_TYPE_UNKNOWN = 0; // 0x0
+ field public static final int MEDIA_TRACK_TYPE_VIDEO = 1; // 0x1
+ }
+
+ public abstract class MediaPlayerInterface implements java.lang.AutoCloseable {
+ ctor public MediaPlayerInterface();
method public abstract androidx.media.AudioAttributesCompat getAudioAttributes();
method public long getBufferedPosition();
method public abstract int getBufferingState();
@@ -827,7 +1091,7 @@
method public abstract void pause();
method public abstract void play();
method public abstract void prepare();
- method public abstract void registerPlayerEventCallback(java.util.concurrent.Executor, androidx.media.MediaPlayerBase.PlayerEventCallback);
+ method public abstract void registerPlayerEventCallback(java.util.concurrent.Executor, androidx.media.MediaPlayerInterface.PlayerEventCallback);
method public abstract void reset();
method public abstract void seekTo(long);
method public abstract void setAudioAttributes(androidx.media.AudioAttributesCompat);
@@ -837,7 +1101,7 @@
method public abstract void setPlaybackSpeed(float);
method public abstract void setPlayerVolume(float);
method public abstract void skipToNext();
- method public abstract void unregisterPlayerEventCallback(androidx.media.MediaPlayerBase.PlayerEventCallback);
+ method public abstract void unregisterPlayerEventCallback(androidx.media.MediaPlayerInterface.PlayerEventCallback);
field public static final int BUFFERING_STATE_BUFFERING_AND_PLAYABLE = 1; // 0x1
field public static final int BUFFERING_STATE_BUFFERING_AND_STARVED = 2; // 0x2
field public static final int BUFFERING_STATE_BUFFERING_COMPLETE = 3; // 0x3
@@ -849,14 +1113,14 @@
field public static final long UNKNOWN_TIME = -1L; // 0xffffffffffffffffL
}
- public static abstract class MediaPlayerBase.PlayerEventCallback {
- ctor public MediaPlayerBase.PlayerEventCallback();
- method public void onBufferingStateChanged(androidx.media.MediaPlayerBase, androidx.media.DataSourceDesc, int);
- method public void onCurrentDataSourceChanged(androidx.media.MediaPlayerBase, androidx.media.DataSourceDesc);
- method public void onMediaPrepared(androidx.media.MediaPlayerBase, androidx.media.DataSourceDesc);
- method public void onPlaybackSpeedChanged(androidx.media.MediaPlayerBase, float);
- method public void onPlayerStateChanged(androidx.media.MediaPlayerBase, int);
- method public void onSeekCompleted(androidx.media.MediaPlayerBase, long);
+ public static abstract class MediaPlayerInterface.PlayerEventCallback {
+ ctor public MediaPlayerInterface.PlayerEventCallback();
+ method public void onBufferingStateChanged(androidx.media.MediaPlayerInterface, androidx.media.DataSourceDesc, int);
+ method public void onCurrentDataSourceChanged(androidx.media.MediaPlayerInterface, androidx.media.DataSourceDesc);
+ method public void onMediaPrepared(androidx.media.MediaPlayerInterface, androidx.media.DataSourceDesc);
+ method public void onPlaybackSpeedChanged(androidx.media.MediaPlayerInterface, float);
+ method public void onPlayerStateChanged(androidx.media.MediaPlayerInterface, int);
+ method public void onSeekCompleted(androidx.media.MediaPlayerInterface, long);
}
public abstract class MediaPlaylistAgent {
@@ -911,7 +1175,7 @@
method public long getCurrentPosition();
method public long getDuration();
method public float getPlaybackSpeed();
- method public androidx.media.MediaPlayerBase getPlayer();
+ method public androidx.media.MediaPlayerInterface getPlayer();
method public int getPlayerState();
method public java.util.List<androidx.media.MediaItem2> getPlaylist();
method public androidx.media.MediaPlaylistAgent getPlaylistAgent();
@@ -942,7 +1206,7 @@
method public void skipToNextItem();
method public void skipToPlaylistItem(androidx.media.MediaItem2);
method public void skipToPreviousItem();
- method public void updatePlayer(androidx.media.MediaPlayerBase, androidx.media.MediaPlaylistAgent, androidx.media.VolumeProviderCompat);
+ method public void updatePlayer(androidx.media.MediaPlayerInterface, androidx.media.MediaPlaylistAgent, androidx.media.VolumeProviderCompat);
method public void updatePlaylistMetadata(androidx.media.MediaMetadata2);
field public static final int ERROR_CODE_ACTION_ABORTED = 10; // 0xa
field public static final int ERROR_CODE_APP_ERROR = 1; // 0x1
@@ -963,7 +1227,7 @@
ctor public MediaSession2.Builder(android.content.Context);
method public androidx.media.MediaSession2 build();
method public androidx.media.MediaSession2.Builder setId(java.lang.String);
- method public androidx.media.MediaSession2.Builder setPlayer(androidx.media.MediaPlayerBase);
+ method public androidx.media.MediaSession2.Builder setPlayer(androidx.media.MediaPlayerInterface);
method public androidx.media.MediaSession2.Builder setPlaylistAgent(androidx.media.MediaPlaylistAgent);
method public androidx.media.MediaSession2.Builder setSessionActivity(android.app.PendingIntent);
method public androidx.media.MediaSession2.Builder setSessionCallback(java.util.concurrent.Executor, androidx.media.MediaSession2.SessionCallback);
@@ -999,19 +1263,19 @@
public static abstract class MediaSession2.SessionCallback {
ctor public MediaSession2.SessionCallback();
- method public void onBufferingStateChanged(androidx.media.MediaSession2, androidx.media.MediaPlayerBase, androidx.media.MediaItem2, int);
+ method public void onBufferingStateChanged(androidx.media.MediaSession2, androidx.media.MediaPlayerInterface, androidx.media.MediaItem2, int);
method public boolean onCommandRequest(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, androidx.media.SessionCommand2);
method public androidx.media.SessionCommandGroup2 onConnect(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo);
- method public void onCurrentMediaItemChanged(androidx.media.MediaSession2, androidx.media.MediaPlayerBase, androidx.media.MediaItem2);
+ method public void onCurrentMediaItemChanged(androidx.media.MediaSession2, androidx.media.MediaPlayerInterface, androidx.media.MediaItem2);
method public void onCustomCommand(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, androidx.media.SessionCommand2, android.os.Bundle, android.os.ResultReceiver);
method public void onDisconnected(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo);
method public void onFastForward(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo);
- method public void onMediaPrepared(androidx.media.MediaSession2, androidx.media.MediaPlayerBase, androidx.media.MediaItem2);
+ method public void onMediaPrepared(androidx.media.MediaSession2, androidx.media.MediaPlayerInterface, androidx.media.MediaItem2);
method public void onPlayFromMediaId(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, java.lang.String, android.os.Bundle);
method public void onPlayFromSearch(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, java.lang.String, android.os.Bundle);
method public void onPlayFromUri(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, android.net.Uri, android.os.Bundle);
- method public void onPlaybackSpeedChanged(androidx.media.MediaSession2, androidx.media.MediaPlayerBase, float);
- method public void onPlayerStateChanged(androidx.media.MediaSession2, androidx.media.MediaPlayerBase, int);
+ method public void onPlaybackSpeedChanged(androidx.media.MediaSession2, androidx.media.MediaPlayerInterface, float);
+ method public void onPlayerStateChanged(androidx.media.MediaSession2, androidx.media.MediaPlayerInterface, int);
method public void onPlaylistChanged(androidx.media.MediaSession2, androidx.media.MediaPlaylistAgent, java.util.List<androidx.media.MediaItem2>, androidx.media.MediaMetadata2);
method public void onPlaylistMetadataChanged(androidx.media.MediaSession2, androidx.media.MediaPlaylistAgent, androidx.media.MediaMetadata2);
method public void onPrepareFromMediaId(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, java.lang.String, android.os.Bundle);
@@ -1019,7 +1283,7 @@
method public void onPrepareFromUri(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, android.net.Uri, android.os.Bundle);
method public void onRepeatModeChanged(androidx.media.MediaSession2, androidx.media.MediaPlaylistAgent, int);
method public void onRewind(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo);
- method public void onSeekCompleted(androidx.media.MediaSession2, androidx.media.MediaPlayerBase, long);
+ method public void onSeekCompleted(androidx.media.MediaSession2, androidx.media.MediaPlayerInterface, long);
method public void onSelectRoute(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, android.os.Bundle);
method public void onSetRating(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, java.lang.String, androidx.media.Rating2);
method public void onShuffleModeChanged(androidx.media.MediaSession2, androidx.media.MediaPlaylistAgent, int);
@@ -1027,6 +1291,35 @@
method public void onUnsubscribeRoutesInfo(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo);
}
+ public final class MediaSessionManager {
+ method public static androidx.media.MediaSessionManager getSessionManager(android.content.Context);
+ method public boolean isTrustedForMediaControl(androidx.media.MediaSessionManager.RemoteUserInfo);
+ }
+
+ public static final class MediaSessionManager.RemoteUserInfo {
+ ctor public MediaSessionManager.RemoteUserInfo(java.lang.String, int, int);
+ method public java.lang.String getPackageName();
+ method public int getPid();
+ method public int getUid();
+ field public static java.lang.String LEGACY_CONTROLLER;
+ }
+
+ public abstract class MediaSessionService2 extends android.app.Service {
+ ctor public MediaSessionService2();
+ method public final androidx.media.MediaSession2 getSession();
+ method public android.os.IBinder onBind(android.content.Intent);
+ method public abstract androidx.media.MediaSession2 onCreateSession(java.lang.String);
+ method public androidx.media.MediaSessionService2.MediaNotification onUpdateNotification();
+ field public static final java.lang.String SERVICE_INTERFACE = "android.media.MediaSessionService2";
+ field public static final java.lang.String SERVICE_META_DATA = "android.media.session";
+ }
+
+ public static class MediaSessionService2.MediaNotification {
+ ctor public MediaSessionService2.MediaNotification(int, android.app.Notification);
+ method public android.app.Notification getNotification();
+ method public int getNotificationId();
+ }
+
public final class Rating2 {
method public static androidx.media.Rating2 fromBundle(android.os.Bundle);
method public float getPercentRating();
@@ -1057,6 +1350,13 @@
method public java.lang.String getCustomCommand();
method public android.os.Bundle getExtras();
field public static final int COMMAND_CODE_CUSTOM = 0; // 0x0
+ field public static final int COMMAND_CODE_LIBRARY_GET_CHILDREN = 29; // 0x1d
+ field public static final int COMMAND_CODE_LIBRARY_GET_ITEM = 30; // 0x1e
+ field public static final int COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT = 31; // 0x1f
+ field public static final int COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT = 32; // 0x20
+ field public static final int COMMAND_CODE_LIBRARY_SEARCH = 33; // 0x21
+ field public static final int COMMAND_CODE_LIBRARY_SUBSCRIBE = 34; // 0x22
+ field public static final int COMMAND_CODE_LIBRARY_UNSUBSCRIBE = 35; // 0x23
field public static final int COMMAND_CODE_PLAYBACK_PAUSE = 2; // 0x2
field public static final int COMMAND_CODE_PLAYBACK_PLAY = 1; // 0x1
field public static final int COMMAND_CODE_PLAYBACK_PREPARE = 6; // 0x6
@@ -1106,6 +1406,7 @@
}
public final class SessionToken2 {
+ ctor public SessionToken2(android.content.Context, android.content.ComponentName);
method public static androidx.media.SessionToken2 fromBundle(android.os.Bundle);
method public java.lang.String getId();
method public java.lang.String getPackageName();
@@ -1113,7 +1414,9 @@
method public int getType();
method public int getUid();
method public android.os.Bundle toBundle();
+ field public static final int TYPE_LIBRARY_SERVICE = 2; // 0x2
field public static final int TYPE_SESSION = 0; // 0x0
+ field public static final int TYPE_SESSION_SERVICE = 1; // 0x1
}
public abstract class VolumeProviderCompat {
diff --git a/media/src/androidTest/java/androidx/media/MediaBrowser2Test.java b/media/src/androidTest/java/androidx/media/MediaBrowser2Test.java
index 4b75986..211b5a9 100644
--- a/media/src/androidTest/java/androidx/media/MediaBrowser2Test.java
+++ b/media/src/androidTest/java/androidx/media/MediaBrowser2Test.java
@@ -29,9 +29,12 @@
import static org.junit.Assert.assertNotEquals;
import android.content.Context;
+import android.os.Build;
import android.os.Bundle;
import android.os.Process;
import android.os.ResultReceiver;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.SdkSuppress;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
@@ -65,17 +68,15 @@
* {@link MediaController2} works cleanly.
*/
// TODO(jaewan): Implement host-side test so browser and service can run in different processes.
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
@RunWith(AndroidJUnit4.class)
@SmallTest
-@Ignore
public class MediaBrowser2Test extends MediaController2Test {
private static final String TAG = "MediaBrowser2Test";
@Override
TestControllerInterface onCreateController(final @NonNull SessionToken2 token,
- @Nullable ControllerCallback callback) throws InterruptedException {
- final BrowserCallback browserCallback =
- callback != null ? (BrowserCallback) callback : new BrowserCallback() {};
+ final @Nullable ControllerCallback callback) throws InterruptedException {
final AtomicReference<TestControllerInterface> controller = new AtomicReference<>();
sHandler.postAndSync(new Runnable() {
@Override
@@ -84,7 +85,7 @@
// Looper. Otherwise, MediaBrowserCompat will post all the commands to the handler
// and commands wouldn't be run if tests codes waits on the test handler.
controller.set(new TestMediaBrowser(
- mContext, token, new TestBrowserCallback(browserCallback)));
+ mContext, token, new TestBrowserCallback(callback)));
}
});
return controller.get();
@@ -120,7 +121,9 @@
Bundle rootHints, String rootMediaId, Bundle rootExtra) {
assertTrue(TestUtils.equals(param, rootHints));
assertEquals(ROOT_ID, rootMediaId);
- assertTrue(TestUtils.equals(EXTRAS, rootExtra));
+ // Note that TestUtils#equals() cannot be used for this because
+ // MediaBrowserServiceCompat adds extra_client_version to the rootHints.
+ assertTrue(TestUtils.contains(rootExtra, EXTRAS));
latch.countDown();
}
};
@@ -259,7 +262,6 @@
assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
}
- @Ignore
@Test
public void testSearch() throws InterruptedException {
prepareLooper();
@@ -317,6 +319,7 @@
}
@Test
+ @LargeTest
public void testSearchTakesTime() throws InterruptedException {
prepareLooper();
final String query = MockMediaLibraryService2.SEARCH_QUERY_TAKES_TIME;
@@ -418,82 +421,201 @@
}
@Test
- public void testBrowserCallback_notifyChildrenChanged() throws InterruptedException {
+ public void testBrowserCallback_onChildrenChangedIsNotCalledWhenNotSubscribed()
+ throws InterruptedException {
+ // This test uses MediaLibrarySession.notifyChildrenChanged().
prepareLooper();
- // TODO(jaewan): Add test for the notifyChildrenChanged itself.
- final String testParentId1 = "testBrowserCallback_notifyChildrenChanged_unexpectedParent";
- final String testParentId2 = "testBrowserCallback_notifyChildrenChanged";
+ final String subscribedMediaId = "subscribedMediaId";
+ final String anotherMediaId = "anotherMediaId";
final int testChildrenCount = 101;
- final Bundle testExtras = new Bundle();
- testExtras.putString(testParentId1, testParentId1);
+ final CountDownLatch latch = new CountDownLatch(1);
- final CountDownLatch latch = new CountDownLatch(3);
- final MediaLibrarySessionCallback sessionCallback =
- new MediaLibrarySessionCallback() {
- @Override
- public SessionCommandGroup2 onConnect(@NonNull MediaSession2 session,
- @NonNull ControllerInfo controller) {
- if (Process.myUid() == controller.getUid()) {
- assertTrue(session instanceof MediaLibrarySession);
- if (mSession != null) {
- mSession.close();
- }
- mSession = session;
- // Shouldn't trigger onChildrenChanged() for the browser, because it
- // hasn't subscribed.
- ((MediaLibrarySession) session).notifyChildrenChanged(
- testParentId1, testChildrenCount, null);
- ((MediaLibrarySession) session).notifyChildrenChanged(
- controller, testParentId1, testChildrenCount, null);
- }
- return super.onConnect(session, controller);
- }
+ final MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback() {
+ @Override
+ public void onSubscribe(@NonNull MediaLibrarySession session,
+ @NonNull ControllerInfo controller, @NonNull String parentId,
+ @Nullable Bundle extras) {
+ if (Process.myUid() == controller.getUid()) {
+ // Shouldn't trigger onChildrenChanged() for the browser,
+ // because the browser didn't subscribe this media id.
+ session.notifyChildrenChanged(anotherMediaId, testChildrenCount, null);
+ }
+ }
- @Override
- public void onSubscribe(@NonNull MediaLibrarySession session,
- @NonNull ControllerInfo info, @NonNull String parentId,
- @Nullable Bundle extras) {
- if (Process.myUid() == info.getUid()) {
- session.notifyChildrenChanged(testParentId2, testChildrenCount, null);
- session.notifyChildrenChanged(info, testParentId2, testChildrenCount,
- testExtras);
- }
- }
+ @Override
+ public List<MediaItem2> onGetChildren(MediaLibrarySession session,
+ ControllerInfo controller, String parentId, int page, int pageSize,
+ Bundle extras) {
+ return TestUtils.createPlaylist(testChildrenCount);
+ }
};
- final BrowserCallback controllerCallbackProxy =
- new BrowserCallback() {
- @Override
- public void onChildrenChanged(MediaBrowser2 browser, String parentId,
- int itemCount, Bundle extras) {
- switch ((int) latch.getCount()) {
- case 3:
- assertEquals(testParentId2, parentId);
- assertEquals(testChildrenCount, itemCount);
- assertNull(extras);
- latch.countDown();
- break;
- case 2:
- assertEquals(testParentId2, parentId);
- assertEquals(testChildrenCount, itemCount);
- assertTrue(TestUtils.equals(testExtras, extras));
- latch.countDown();
- break;
- default:
- // Unexpected call.
- fail();
- }
- }
- };
+ final BrowserCallback controllerCallbackProxy = new BrowserCallback() {
+ @Override
+ public void onChildrenChanged(MediaBrowser2 browser, String parentId,
+ int itemCount, Bundle extras) {
+ // Unexpected call.
+ fail();
+ }
+ };
+
TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
final MediaBrowser2 browser = (MediaBrowser2) createController(
token, true, controllerCallbackProxy);
- assertTrue(mSession instanceof MediaLibrarySession);
- browser.subscribe(testParentId2, null);
- // This ensures that onChildrenChanged() is only called for the expected reasons.
+ browser.subscribe(subscribedMediaId, null);
+
+ // onChildrenChanged() should not be called.
assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
}
+ @Test
+ public void testBrowserCallback_onChildrenChangedIsCalledWhenSubscribed()
+ throws InterruptedException {
+ // This test uses MediaLibrarySession.notifyChildrenChanged().
+ prepareLooper();
+ final String expectedParentId = "expectedParentId";
+ final int testChildrenCount = 101;
+ final Bundle testExtras = TestUtils.createTestBundle();
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback() {
+ @Override
+ public void onSubscribe(@NonNull MediaLibrarySession session,
+ @NonNull ControllerInfo controller, @NonNull String parentId,
+ @Nullable Bundle extras) {
+ if (Process.myUid() == controller.getUid()) {
+ // Should trigger onChildrenChanged() for the browser.
+ session.notifyChildrenChanged(expectedParentId, testChildrenCount, testExtras);
+ }
+ }
+
+ @Override
+ public List<MediaItem2> onGetChildren(MediaLibrarySession session,
+ ControllerInfo controller, String parentId, int page, int pageSize,
+ Bundle extras) {
+ return TestUtils.createPlaylist(testChildrenCount);
+ }
+ };
+ final BrowserCallback controllerCallbackProxy = new BrowserCallback() {
+ @Override
+ public void onChildrenChanged(MediaBrowser2 browser, String parentId,
+ int itemCount, Bundle extras) {
+ assertEquals(expectedParentId, parentId);
+ assertEquals(testChildrenCount, itemCount);
+ assertTrue(TestUtils.equals(testExtras, extras));
+ latch.countDown();
+ }
+ };
+
+ TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ final MediaBrowser2 browser = (MediaBrowser2) createController(
+ token, true, controllerCallbackProxy);
+ browser.subscribe(expectedParentId, null);
+
+ // onChildrenChanged() should be called.
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testBrowserCallback_onChildrenChangedIsNotCalledWhenNotSubscribed2()
+ throws InterruptedException {
+ // This test uses MediaLibrarySession.notifyChildrenChanged(ControllerInfo).
+ prepareLooper();
+ final String subscribedMediaId = "subscribedMediaId";
+ final String anotherMediaId = "anotherMediaId";
+ final int testChildrenCount = 101;
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ final MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback() {
+ @Override
+ public void onSubscribe(@NonNull MediaLibrarySession session,
+ @NonNull ControllerInfo controller, @NonNull String parentId,
+ @Nullable Bundle extras) {
+ if (Process.myUid() == controller.getUid()) {
+ // Shouldn't trigger onChildrenChanged() for the browser,
+ // because the browser didn't subscribe this media id.
+ session.notifyChildrenChanged(
+ controller, anotherMediaId, testChildrenCount, null);
+ }
+ }
+
+ @Override
+ public List<MediaItem2> onGetChildren(MediaLibrarySession session,
+ ControllerInfo controller, String parentId, int page, int pageSize,
+ Bundle extras) {
+ return TestUtils.createPlaylist(testChildrenCount);
+ }
+ };
+ final BrowserCallback controllerCallbackProxy = new BrowserCallback() {
+ @Override
+ public void onChildrenChanged(MediaBrowser2 browser, String parentId,
+ int itemCount, Bundle extras) {
+ // Unexpected call.
+ fail();
+ }
+ };
+
+ TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ final MediaBrowser2 browser = (MediaBrowser2) createController(
+ token, true, controllerCallbackProxy);
+ browser.subscribe(subscribedMediaId, null);
+
+ // onChildrenChanged() should not be called.
+ assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testBrowserCallback_onChildrenChangedIsCalledWhenSubscribed2()
+ throws InterruptedException {
+ // This test uses MediaLibrarySession.notifyChildrenChanged(ControllerInfo).
+ prepareLooper();
+ final String expectedParentId = "expectedParentId";
+ final int testChildrenCount = 101;
+ final Bundle testExtras = TestUtils.createTestBundle();
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback() {
+ @Override
+ public void onSubscribe(@NonNull MediaLibrarySession session,
+ @NonNull ControllerInfo controller, @NonNull String parentId,
+ @Nullable Bundle extras) {
+ if (Process.myUid() == controller.getUid()) {
+ // Should trigger onChildrenChanged() for the browser.
+ session.notifyChildrenChanged(
+ controller, expectedParentId, testChildrenCount, testExtras);
+ }
+ }
+
+ @Override
+ public List<MediaItem2> onGetChildren(MediaLibrarySession session,
+ ControllerInfo controller, String parentId, int page, int pageSize,
+ Bundle extras) {
+ return TestUtils.createPlaylist(testChildrenCount);
+ }
+ };
+ final BrowserCallback controllerCallbackProxy = new BrowserCallback() {
+ @Override
+ public void onChildrenChanged(MediaBrowser2 browser, String parentId,
+ int itemCount, Bundle extras) {
+ assertEquals(expectedParentId, parentId);
+ assertEquals(testChildrenCount, itemCount);
+ assertTrue(TestUtils.equals(testExtras, extras));
+ latch.countDown();
+ }
+ };
+
+ TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ final MediaBrowser2 browser = (MediaBrowser2) createController(
+ token, true, controllerCallbackProxy);
+ browser.subscribe(expectedParentId, null);
+
+ // onChildrenChanged() should be called.
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
public static class TestBrowserCallback extends BrowserCallback
implements TestControllerCallbackInterface {
private final ControllerCallback mCallbackProxy;
@@ -504,7 +626,7 @@
TestBrowserCallback(ControllerCallback callbackProxy) {
if (callbackProxy == null) {
- throw new IllegalArgumentException("Callback proxy shouldn't be null. Test bug");
+ callbackProxy = new BrowserCallback() {};
}
mCallbackProxy = callbackProxy;
}
@@ -690,9 +812,9 @@
private final BrowserCallback mCallback;
public TestMediaBrowser(@NonNull Context context, @NonNull SessionToken2 token,
- @NonNull ControllerCallback callback) {
- super(context, token, sHandlerExecutor, (BrowserCallback) callback);
- mCallback = (BrowserCallback) callback;
+ @NonNull BrowserCallback callback) {
+ super(context, token, sHandlerExecutor, callback);
+ mCallback = callback;
}
@Override
diff --git a/media/src/androidTest/java/androidx/media/MediaController2Test.java b/media/src/androidTest/java/androidx/media/MediaController2Test.java
index 75c9e50..072381e 100644
--- a/media/src/androidTest/java/androidx/media/MediaController2Test.java
+++ b/media/src/androidTest/java/androidx/media/MediaController2Test.java
@@ -25,6 +25,7 @@
import static org.junit.Assert.fail;
import android.app.PendingIntent;
+import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.net.Uri;
@@ -41,15 +42,16 @@
import androidx.annotation.NonNull;
import androidx.media.MediaController2.ControllerCallback;
+import androidx.media.MediaController2.PlaybackInfo;
import androidx.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback;
import androidx.media.MediaSession2.ControllerInfo;
import androidx.media.MediaSession2.SessionCallback;
import androidx.media.TestServiceRegistry.SessionServiceCallback;
import androidx.media.TestUtils.SyncHandler;
+import androidx.testutils.PollingCheck;
import org.junit.After;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -65,7 +67,7 @@
// TODO(jaewan): Implement host-side test so controller and session can run in different processes.
// TODO(jaewan): Fix flaky failure -- see MediaController2Impl.getController()
// TODO(jaeawn): Revisit create/close session in the sHandler. It's no longer necessary.
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
@RunWith(AndroidJUnit4.class)
@SmallTest
@FlakyTest
@@ -77,6 +79,7 @@
MediaController2 mController;
MockPlayer mPlayer;
MockPlaylistAgent mMockAgent;
+ AudioManager mAudioManager;
@Before
@Override
@@ -111,6 +114,7 @@
.setSessionActivity(mIntent)
.setId(TAG).build();
mController = createController(mSession.getToken());
+ mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
TestServiceRegistry.getInstance().setHandler(sHandler);
}
@@ -207,11 +211,12 @@
@Test
public void testGettersAfterConnected() throws InterruptedException {
prepareLooper();
- final int state = MediaPlayerBase.PLAYER_STATE_PLAYING;
- final int bufferingState = MediaPlayerBase.BUFFERING_STATE_BUFFERING_COMPLETE;
+ final int state = MediaPlayerInterface.PLAYER_STATE_PLAYING;
+ final int bufferingState = MediaPlayerInterface.BUFFERING_STATE_BUFFERING_COMPLETE;
final long position = 150000;
final long bufferedPosition = 900000;
final float speed = 0.5f;
+ final long timeDiff = 102;
final MediaItem2 currentMediaItem = TestUtils.createMediaItemWithMetadata();
mPlayer.mLastPlayerState = state;
@@ -221,22 +226,60 @@
mPlayer.mPlaybackSpeed = speed;
mMockAgent.mCurrentMediaItem = currentMediaItem;
- long time1 = System.currentTimeMillis();
MediaController2 controller = createController(mSession.getToken());
- long time2 = System.currentTimeMillis();
+ controller.setTimeDiff(timeDiff);
assertEquals(state, controller.getPlayerState());
assertEquals(bufferedPosition, controller.getBufferedPosition());
assertEquals(speed, controller.getPlaybackSpeed(), 0.0f);
- long positionLowerBound = (long) (position + speed * (System.currentTimeMillis() - time2));
- long currentPosition = controller.getCurrentPosition();
- long positionUpperBound = (long) (position + speed * (System.currentTimeMillis() - time1));
- assertTrue("curPos=" + currentPosition + ", lowerBound=" + positionLowerBound
- + ", upperBound=" + positionUpperBound,
- positionLowerBound <= currentPosition && currentPosition <= positionUpperBound);
+ assertEquals(position + (long) (speed * timeDiff), controller.getCurrentPosition());
assertEquals(currentMediaItem, controller.getCurrentMediaItem());
}
@Test
+ public void testUpdatePlayer() throws InterruptedException {
+ prepareLooper();
+ final int testState = MediaPlayerInterface.PLAYER_STATE_PLAYING;
+ final List<MediaItem2> testPlaylist = TestUtils.createPlaylist(3);
+ final AudioAttributesCompat testAudioAttributes = new AudioAttributesCompat.Builder()
+ .setLegacyStreamType(AudioManager.STREAM_RING).build();
+ final CountDownLatch latch = new CountDownLatch(3);
+ mController = createController(mSession.getToken(), true, new ControllerCallback() {
+ @Override
+ public void onPlayerStateChanged(MediaController2 controller, int state) {
+ assertEquals(mController, controller);
+ assertEquals(testState, state);
+ latch.countDown();
+ }
+
+ @Override
+ public void onPlaylistChanged(MediaController2 controller, List<MediaItem2> list,
+ MediaMetadata2 metadata) {
+ assertEquals(mController, controller);
+ assertEquals(testPlaylist, list);
+ assertNull(metadata);
+ latch.countDown();
+ }
+
+ @Override
+ public void onPlaybackInfoChanged(MediaController2 controller, PlaybackInfo info) {
+ assertEquals(mController, controller);
+ assertEquals(testAudioAttributes, info.getAudioAttributes());
+ latch.countDown();
+ }
+ });
+
+ MockPlayer player = new MockPlayer(0);
+ player.mLastPlayerState = testState;
+ player.setAudioAttributes(testAudioAttributes);
+
+ MockPlaylistAgent agent = new MockPlaylistAgent();
+ agent.mPlaylist = testPlaylist;
+
+ mSession.updatePlayer(player, agent, null);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
public void testGetSessionActivity() {
prepareLooper();
PendingIntent sessionActivity = mController.getSessionActivity();
@@ -420,6 +463,77 @@
}
}
+
+ @Test
+ public void testControllerCallback_onSeekCompleted() throws InterruptedException {
+ prepareLooper();
+ final long testSeekPosition = 400;
+ final long testPosition = 500;
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onSeekCompleted(MediaController2 controller, long position) {
+ controller.setTimeDiff(Long.valueOf(0));
+ assertEquals(testSeekPosition, position);
+ assertEquals(testPosition, controller.getCurrentPosition());
+ latch.countDown();
+ }
+ };
+ final MediaController2 controller = createController(mSession.getToken(), true, callback);
+ mPlayer.mCurrentPosition = testPosition;
+ mPlayer.notifySeekCompleted(testSeekPosition);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testControllerCallback_onBufferingStateChanged() throws InterruptedException {
+ prepareLooper();
+ final List<MediaItem2> testPlaylist = TestUtils.createPlaylist(3);
+ final MediaItem2 testItem = testPlaylist.get(0);
+ final int testBufferingState = MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_PLAYABLE;
+ final long testBufferingPosition = 500;
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onBufferingStateChanged(MediaController2 controller, MediaItem2 item,
+ int state) {
+ controller.setTimeDiff(Long.valueOf(0));
+ assertEquals(testItem, item);
+ assertEquals(testBufferingState, state);
+ assertEquals(testBufferingState, controller.getBufferingState());
+ assertEquals(testBufferingPosition, controller.getBufferedPosition());
+ latch.countDown();
+ }
+ };
+ final MediaController2 controller = createController(mSession.getToken(), true, callback);
+ mSession.setPlaylist(testPlaylist, null);
+ mPlayer.mBufferedPosition = testBufferingPosition;
+ mPlayer.notifyBufferingStateChanged(testItem.getDataSourceDesc(), testBufferingState);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testControllerCallback_onPlayerStateChanged() throws InterruptedException {
+ prepareLooper();
+ final int testPlayerState = MediaPlayerInterface.PLAYER_STATE_PLAYING;
+ final long testPosition = 500;
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onPlayerStateChanged(MediaController2 controller, int state) {
+ controller.setTimeDiff(Long.valueOf(0));
+ assertEquals(testPlayerState, state);
+ assertEquals(testPlayerState, controller.getPlayerState());
+ assertEquals(testPosition, controller.getCurrentPosition());
+ latch.countDown();
+ }
+ };
+ final MediaController2 controller = createController(mSession.getToken(), true, callback);
+ mPlayer.mCurrentPosition = testPosition;
+ mPlayer.notifyPlaybackState(testPlayerState);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+
@Test
public void testAddPlaylistItem() throws InterruptedException {
prepareLooper();
@@ -572,7 +686,6 @@
@Test
public void testSetVolumeTo() throws Exception {
- // TODO(jaewan): Also test with local volume.
prepareLooper();
final int maxVolume = 100;
final int currentVolume = 23;
@@ -592,7 +705,6 @@
@Test
public void testAdjustVolume() throws Exception {
- // TODO(jaewan): Also test with local volume.
prepareLooper();
final int maxVolume = 100;
final int currentVolume = 23;
@@ -611,6 +723,87 @@
}
@Test
+ public void testSetVolumeWithLocalVolume() throws Exception {
+ prepareLooper();
+ if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
+ // This test is not eligible for this device.
+ return;
+ }
+
+ // Here, we intentionally choose STREAM_ALARM in order not to consider
+ // 'Do Not Disturb' or 'Volume limit'.
+ final int stream = AudioManager.STREAM_ALARM;
+ final int maxVolume = mAudioManager.getStreamMaxVolume(stream);
+ final int minVolume = 0;
+ if (maxVolume <= minVolume) {
+ return;
+ }
+
+ // Set stream of the session.
+ AudioAttributesCompat attrs = new AudioAttributesCompat.Builder()
+ .setLegacyStreamType(stream)
+ .build();
+ mPlayer.setAudioAttributes(attrs);
+ mSession.updatePlayer(mPlayer, null, null);
+
+ final int originalVolume = mAudioManager.getStreamVolume(stream);
+ final int targetVolume = originalVolume == minVolume
+ ? originalVolume + 1 : originalVolume - 1;
+
+ mController.setVolumeTo(targetVolume, AudioManager.FLAG_SHOW_UI);
+ new PollingCheck(WAIT_TIME_MS) {
+ @Override
+ protected boolean check() {
+ return targetVolume == mAudioManager.getStreamVolume(stream);
+ }
+ }.run();
+
+ // Set back to original volume.
+ mAudioManager.setStreamVolume(stream, originalVolume, 0 /* flags */);
+ }
+
+ @Test
+ public void testAdjustVolumeWithLocalVolume() throws Exception {
+ prepareLooper();
+ if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
+ // This test is not eligible for this device.
+ return;
+ }
+
+ // Here, we intentionally choose STREAM_ALARM in order not to consider
+ // 'Do Not Disturb' or 'Volume limit'.
+ final int stream = AudioManager.STREAM_ALARM;
+ final int maxVolume = mAudioManager.getStreamMaxVolume(stream);
+ final int minVolume = 0;
+ if (maxVolume <= minVolume) {
+ return;
+ }
+
+ // Set stream of the session.
+ AudioAttributesCompat attrs = new AudioAttributesCompat.Builder()
+ .setLegacyStreamType(stream)
+ .build();
+ mPlayer.setAudioAttributes(attrs);
+ mSession.updatePlayer(mPlayer, null, null);
+
+ final int originalVolume = mAudioManager.getStreamVolume(stream);
+ final int direction = originalVolume == minVolume
+ ? AudioManager.ADJUST_RAISE : AudioManager.ADJUST_LOWER;
+ final int targetVolume = originalVolume + direction;
+
+ mController.adjustVolume(direction, AudioManager.FLAG_SHOW_UI);
+ new PollingCheck(WAIT_TIME_MS) {
+ @Override
+ protected boolean check() {
+ return targetVolume == mAudioManager.getStreamVolume(stream);
+ }
+ }.run();
+
+ // Set back to original volume.
+ mAudioManager.setStreamVolume(stream, originalVolume, 0 /* flags */);
+ }
+
+ @Test
public void testGetPackageName() {
prepareLooper();
assertEquals(mContext.getPackageName(), mController.getSessionToken().getPackageName());
@@ -979,7 +1172,7 @@
testHandler.post(new Runnable() {
@Override
public void run() {
- final int state = MediaPlayerBase.PLAYER_STATE_ERROR;
+ final int state = MediaPlayerInterface.PLAYER_STATE_ERROR;
for (int i = 0; i < 100; i++) {
// triggers call from session to controller.
player.notifyPlaybackState(state);
@@ -1012,11 +1205,13 @@
}
});
}
- if (sessionThread != null) {
+
+ if (Build.VERSION.SDK_INT >= 18) {
sessionThread.quitSafely();
- }
- if (testThread != null) {
testThread.quitSafely();
+ } else {
+ sessionThread.quit();
+ testThread.quit();
}
}
}
@@ -1065,30 +1260,29 @@
};
TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
- mController = createController(TestUtils.getServiceToken(mContext, id));
+ final SessionCommand2 testCommand = new SessionCommand2("testConnectToService", null);
+ final CountDownLatch controllerLatch = new CountDownLatch(1);
+ mController = createController(TestUtils.getServiceToken(mContext, id), true,
+ new ControllerCallback() {
+ @Override
+ public void onCustomCommand(MediaController2 controller,
+ SessionCommand2 command, Bundle args, ResultReceiver receiver) {
+ if (testCommand.equals(command)) {
+ controllerLatch.countDown();
+ }
+ }
+ }
+ );
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
- // Test command from controller to session service
- // TODO: Re enable when transport control works
- /*
+ // Test command from controller to session service.
mController.play();
assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertTrue(mPlayer.mPlayCalled);
- */
- // Test command from session service to controller
- // TODO(jaewan): Add equivalent tests again
- /*
- final CountDownLatch latch = new CountDownLatch(1);
- mController.registerPlayerEventCallback((state) -> {
- assertNotNull(state);
- assertEquals(PlaybackState.STATE_REWINDING, state.getState());
- latch.countDown();
- }, sHandler);
- mPlayer.notifyPlaybackState(
- TestUtils.createPlaybackState(PlaybackState.STATE_REWINDING));
- assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
- */
+ // Test command from session service to controller.
+ mSession.sendCustomCommand(testCommand, null);
+ assertTrue(controllerLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
}
@Test
@@ -1097,15 +1291,11 @@
testControllerAfterSessionIsClosed(mSession.getToken().getId());
}
- // TODO(jaewan): Re-enable this test
- @Ignore
@Test
public void testControllerAfterSessionIsClosed_sessionService() throws InterruptedException {
prepareLooper();
- /*
- connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
+ testConnectToService(MockMediaSessionService2.ID);
testControllerAfterSessionIsClosed(MockMediaSessionService2.ID);
- */
}
@Test
@@ -1192,14 +1382,12 @@
testControllerAfterSessionIsClosed(id);
}
- @Ignore
@Test
public void testClose_sessionService() throws InterruptedException {
prepareLooper();
testCloseFromService(MockMediaSessionService2.ID);
}
- @Ignore
@Test
public void testClose_libraryService() throws InterruptedException {
prepareLooper();
diff --git a/media/src/androidTest/java/androidx/media/MediaMetadata2Test.java b/media/src/androidTest/java/androidx/media/MediaMetadata2Test.java
index f000f02..d583e47 100644
--- a/media/src/androidTest/java/androidx/media/MediaMetadata2Test.java
+++ b/media/src/androidTest/java/androidx/media/MediaMetadata2Test.java
@@ -19,7 +19,9 @@
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;
+import android.os.Build;
import android.os.Bundle;
+import android.support.test.filters.SdkSuppress;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
@@ -28,6 +30,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
@RunWith(AndroidJUnit4.class)
@SmallTest
public class MediaMetadata2Test {
diff --git a/media/src/androidTest/java/androidx/media/MediaPlayer2Test.java b/media/src/androidTest/java/androidx/media/MediaPlayer2Test.java
index f565e8a..0a35ddb 100644
--- a/media/src/androidTest/java/androidx/media/MediaPlayer2Test.java
+++ b/media/src/androidTest/java/androidx/media/MediaPlayer2Test.java
@@ -29,6 +29,7 @@
import android.media.MediaRecorder;
import android.media.MediaTimestamp;
import android.media.PlaybackParams;
+import android.media.SubtitleData;
import android.media.SyncParams;
import android.media.audiofx.AudioEffect;
import android.media.audiofx.Visualizer;
@@ -36,10 +37,13 @@
import android.os.Build;
import android.os.Environment;
import android.support.test.filters.LargeTest;
+import android.support.test.filters.MediumTest;
import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.util.Log;
+import androidx.media.MediaPlayerInterface.PlayerEventCallback;
import androidx.media.test.R;
import org.junit.After;
@@ -49,14 +53,18 @@
import java.io.BufferedReader;
import java.io.File;
+import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
+import java.util.concurrent.BlockingDeque;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
@@ -81,7 +89,7 @@
@Before
@Override
- public void setUp() throws Exception {
+ public void setUp() throws Throwable {
super.setUp();
mRecordedFilePath = new File(Environment.getExternalStorageDirectory(),
"mediaplayer_record.out").getAbsolutePath();
@@ -97,61 +105,8 @@
}
}
- // Bug 13652927
- public void testVorbisCrash() throws Exception {
- MediaPlayer2 mp = mPlayer;
- MediaPlayer2 mp2 = mPlayer2;
- AssetFileDescriptor afd2 = mResources.openRawResourceFd(R.raw.testmp3_2);
- mp2.setDataSource(new DataSourceDesc.Builder()
- .setDataSource(afd2.getFileDescriptor(), afd2.getStartOffset(), afd2.getLength())
- .build());
- final Monitor onPrepareCalled = new Monitor();
- final Monitor onErrorCalled = new Monitor();
- MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
- @Override
- public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
- if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
- onPrepareCalled.signal();
- }
- }
-
- @Override
- public void onError(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
- onErrorCalled.signal();
- }
- };
- mp2.setMediaPlayer2EventCallback(mExecutor, ecb);
- mp2.prepare();
- onPrepareCalled.waitForSignal();
- afd2.close();
- mp2.clearMediaPlayer2EventCallback();
-
- mp2.loopCurrent(true);
- mp2.play();
-
- for (int i = 0; i < 20; i++) {
- try {
- AssetFileDescriptor afd = mResources.openRawResourceFd(R.raw.bug13652927);
- mp.setDataSource(new DataSourceDesc.Builder()
- .setDataSource(afd.getFileDescriptor(), afd.getStartOffset(),
- afd.getLength())
- .build());
- mp.setMediaPlayer2EventCallback(mExecutor, ecb);
- onPrepareCalled.reset();
- mp.prepare();
- onErrorCalled.waitForSignal();
- afd.close();
- } catch (Exception e) {
- // expected to fail
- Log.i("@@@", "failed: " + e);
- }
- Thread.sleep(500);
- assertTrue("media player died",
- mp2.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
- mp.reset();
- }
- }
-
+ @Test
+ @MediumTest
public void testPlayNullSourcePath() throws Exception {
final Monitor onSetDataSourceCalled = new Monitor();
MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
@@ -172,6 +127,8 @@
onSetDataSourceCalled.waitForSignal();
}
+ @Test
+ @LargeTest
public void testPlayAudioFromDataURI() throws Exception {
final int mp3Duration = 34909;
final int tolerance = 70;
@@ -220,15 +177,12 @@
.setLegacyStreamType(AudioManager.STREAM_MUSIC)
.build();
mp.setAudioAttributes(attributes);
- /* FIXME: ensure screen is on while testing.
- mp.setWakeMode(mContext, PowerManager.PARTIAL_WAKE_LOCK);
- */
- assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertFalse(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
onPlayCalled.reset();
mp.play();
onPlayCalled.waitForSignal();
- assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertTrue(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
/* FIXME: what's API for checking loop state?
assertFalse(mp.isLooping());
@@ -253,11 +207,11 @@
// test pause and restart
mp.pause();
Thread.sleep(SLEEP_TIME);
- assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertFalse(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
onPlayCalled.reset();
mp.play();
onPlayCalled.waitForSignal();
- assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertTrue(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
// test stop and restart
mp.reset();
@@ -269,14 +223,14 @@
mp.prepare();
onPrepareCalled.waitForSignal();
- assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertFalse(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
onPlayCalled.reset();
mp.play();
onPlayCalled.waitForSignal();
- assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertTrue(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
// waiting to complete
- while (mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+ while (mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING) {
Thread.sleep(SLEEP_TIME);
}
} finally {
@@ -284,6 +238,8 @@
}
}
+ @Test
+ @LargeTest
public void testPlayAudio() throws Exception {
final int resid = R.raw.testmp3_2;
final int mp3Duration = 34909;
@@ -323,11 +279,11 @@
.build();
mp.setAudioAttributes(attributes);
- assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertFalse(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
onPlayCalled.reset();
mp.play();
onPlayCalled.waitForSignal();
- assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertTrue(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
//assertFalse(mp.isLooping());
onLoopCurrentCalled.reset();
@@ -348,11 +304,11 @@
// test pause and restart
mp.pause();
Thread.sleep(SLEEP_TIME);
- assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertFalse(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
onPlayCalled.reset();
mp.play();
onPlayCalled.waitForSignal();
- assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertTrue(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
// test stop and restart
mp.reset();
@@ -367,14 +323,14 @@
onPrepareCalled.waitForSignal();
afd.close();
- assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertFalse(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
onPlayCalled.reset();
mp.play();
onPlayCalled.waitForSignal();
- assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertTrue(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
// waiting to complete
- while (mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+ while (mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING) {
Thread.sleep(SLEEP_TIME);
}
} catch (Exception e) {
@@ -414,7 +370,6 @@
.setInternalLegacyStreamType(AudioManager.STREAM_MUSIC)
.build();
mp.setAudioAttributes(attributes);
- mp.setWakeMode(mContext, PowerManager.PARTIAL_WAKE_LOCK);
assertFalse(mp.isPlaying());
onPlayCalled.reset();
@@ -445,6 +400,8 @@
}
*/
+ @Test
+ @LargeTest
public void testPlayAudioLooping() throws Exception {
final int resid = R.raw.testmp3;
@@ -462,8 +419,10 @@
@Override
public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd,
int what, int extra) {
- Log.i("@@@", "got oncompletion");
- onCompletionCalled.signal();
+ if (what == MediaPlayer2.MEDIA_INFO_PLAYBACK_COMPLETE) {
+ Log.i("@@@", "got oncompletion");
+ onCompletionCalled.signal();
+ }
}
@Override
@@ -476,21 +435,21 @@
};
mp.setMediaPlayer2EventCallback(mExecutor, ecb);
- assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertFalse(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
onPlayCalled.reset();
mp.play();
onPlayCalled.waitForSignal();
- assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertTrue(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
long duration = mp.getDuration();
Thread.sleep(duration * 4); // allow for several loops
- assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertTrue(mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
assertEquals("wrong number of completion signals", 0,
onCompletionCalled.getNumSignal());
mp.loopCurrent(false);
// wait for playback to finish
- while (mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+ while (mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING) {
Thread.sleep(SLEEP_TIME);
}
assertEquals("wrong number of completion signals", 1,
@@ -500,6 +459,8 @@
}
}
+ @Test
+ @LargeTest
public void testPlayMidi() throws Exception {
final int resid = R.raw.midi8sec;
final int midiDuration = 8000;
@@ -668,7 +629,7 @@
mp.play();
Thread.sleep(SLEEP_TIME);
assertFalse("player was still playing after " + SLEEP_TIME + " ms",
- mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ mp.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
assertTrue("nothing heard while test ran", listener.heardSound());
listener.reset();
mp.seekTo(0, MediaPlayer2.SEEK_PREVIOUS_SYNC);
@@ -730,7 +691,7 @@
*/
// TODO: uncomment out line below when MediaPlayer2 can seek to requested position.
// assertEquals(posAfter, posBefore, tolerance);
- assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertTrue(mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
Thread.sleep(SLEEP_TIME);
@@ -745,7 +706,7 @@
posAfter = mPlayer.getCurrentPosition();
// TODO: uncomment out line below when MediaPlayer2 can seek to requested position.
// assertEquals(posAfter, posBefore, tolerance);
- assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertTrue(mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
Thread.sleep(SLEEP_TIME);
@@ -755,7 +716,7 @@
// TODO: uncomment out line below when MediaPlayer2 can seek to requested position.
// assertEquals(posAfter, posBefore, tolerance);
- assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertTrue(mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
Thread.sleep(SLEEP_TIME);
}
@@ -867,7 +828,19 @@
assertEquals(Integer.parseInt(rotation), angle);
}
+ @Test
+ @LargeTest
+ public void testSkipToNext() throws Exception {
+ testPlaylist(true);
+ }
+
+ @Test
+ @LargeTest
public void testPlaylist() throws Exception {
+ testPlaylist(false);
+ }
+
+ private void testPlaylist(boolean skip) throws Exception {
if (!checkLoadResource(
R.raw.video_480x360_mp4_h264_1000kbps_30fps_aac_stereo_128kbps_44100hz)) {
return; // skip
@@ -915,9 +888,18 @@
mPlayer.play();
- mOnCompletionCalled.waitForSignal();
- onCompletion2Called.waitForSignal();
+ if (skip) {
+ mPlayer.skipToNext();
+ mPlayer.skipToNext();
+ } else {
+ mOnCompletionCalled.waitForSignal();
+ onCompletion2Called.waitForSignal();
+ }
onCompletion1Called.waitForSignal();
+ if (skip) {
+ assertFalse("first dsd completed", mOnCompletionCalled.isSignalled());
+ assertFalse("second dsd completed", onCompletion2Called.isSignalled());
+ }
mPlayer.reset();
}
@@ -1035,7 +1017,7 @@
playbackRate, pbp.getSpeed(),
FLOAT_TOLERANCE + playbackRate * sync.getTolerance());
assertTrue("MediaPlayer2 should still be playing",
- mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
long playedMediaDurationMs = mPlayer.getCurrentPosition();
int diff = Math.abs((int) (playedMediaDurationMs / playbackRate) - playTime);
@@ -1452,14 +1434,14 @@
}
}
+ @Test
+ @LargeTest
public void testDeselectTrackForSubtitleTracks() throws Throwable {
if (!checkLoadResource(R.raw.testvideo_with_2_subtitle_tracks)) {
return; // skip;
}
- /* FIXME: find out counter part of waitForIdleSync.
- getInstrumentation().waitForIdleSync();
- */
+ mInstrumentation.waitForIdleSync();
MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
@Override
@@ -1482,21 +1464,18 @@
mOnDeselectTrackCalled.signal();
}
}
- };
- synchronized (mEventCbLock) {
- mEventCallbacks.add(ecb);
- }
- /* TODO: uncomment once API is available in supportlib.
- mPlayer.setOnSubtitleDataListener(new MediaPlayer2.OnSubtitleDataListener() {
@Override
- public void onSubtitleData(MediaPlayer2 mp, SubtitleData data) {
+ public void onSubtitleData(
+ MediaPlayer2 mp, DataSourceDesc dsd, SubtitleData data) {
if (data != null && data.getData() != null) {
mOnSubtitleDataCalled.signal();
}
}
- });
- */
+ };
+ synchronized (mEventCbLock) {
+ mEventCallbacks.add(ecb);
+ }
mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
@@ -1507,7 +1486,7 @@
mOnPlayCalled.reset();
mPlayer.play();
mOnPlayCalled.waitForSignal();
- assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertTrue(mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
// Closed caption tracks are in-band.
// So, those tracks will be found after processing a number of frames.
@@ -1539,22 +1518,13 @@
mPlayer.reset();
}
+ @Test
+ @LargeTest
public void testChangeSubtitleTrack() throws Throwable {
if (!checkLoadResource(R.raw.testvideo_with_2_subtitle_tracks)) {
return; // skip;
}
- /* TODO: uncomment once API is available in supportlib.
- mPlayer.setOnSubtitleDataListener(new MediaPlayer2.OnSubtitleDataListener() {
- @Override
- public void onSubtitleData(MediaPlayer2 mp, SubtitleData data) {
- if (data != null && data.getData() != null) {
- mOnSubtitleDataCalled.signal();
- }
- }
- });
- */
-
MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
@Override
public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
@@ -1571,6 +1541,14 @@
mOnPlayCalled.signal();
}
}
+
+ @Override
+ public void onSubtitleData(
+ MediaPlayer2 mp, DataSourceDesc dsd, SubtitleData data) {
+ if (data != null && data.getData() != null) {
+ mOnSubtitleDataCalled.signal();
+ }
+ }
};
synchronized (mEventCbLock) {
mEventCallbacks.add(ecb);
@@ -1585,7 +1563,7 @@
mOnPlayCalled.reset();
mPlayer.play();
mOnPlayCalled.waitForSignal();
- assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertTrue(mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
// Closed caption tracks are in-band.
// So, those tracks will be found after processing a number of frames.
@@ -1644,7 +1622,7 @@
mOnPlayCalled.reset();
mPlayer.play();
mOnPlayCalled.waitForSignal();
- assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertTrue(mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
// The media metadata will be changed while playing since closed caption tracks are in-band
// and those tracks will be found after processing a number of frames. These tracks will be
@@ -1660,10 +1638,77 @@
mPlayer.reset();
}
+ @Test
+ @LargeTest
+ public void testMediaTimeDiscontinuity() throws Exception {
+ if (!checkLoadResource(
+ R.raw.bbb_s1_320x240_mp4_h264_mp2_800kbps_30fps_aac_lc_5ch_240kbps_44100hz)) {
+ return; // skip
+ }
+
+ final BlockingDeque<MediaTimestamp> timestamps = new LinkedBlockingDeque<>();
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+ if (what == MediaPlayer2.CALL_COMPLETED_SEEK_TO) {
+ mOnSeekCompleteCalled.signal();
+ }
+ }
+ @Override
+ public void onMediaTimeDiscontinuity(
+ MediaPlayer2 mp, DataSourceDesc dsd, MediaTimestamp timestamp) {
+ timestamps.add(timestamp);
+ mOnMediaTimeDiscontinuityCalled.signal();
+ }
+ };
+ synchronized (mEventCbLock) {
+ mEventCallbacks.add(ecb);
+ }
+
+ mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+ mPlayer.prepare();
+
+ // Timestamp needs to be reported when playback starts.
+ mOnMediaTimeDiscontinuityCalled.reset();
+ mPlayer.play();
+ do {
+ assertTrue(mOnMediaTimeDiscontinuityCalled.waitForSignal(1000));
+ } while (Math.abs(timestamps.getLast().getMediaClockRate() - 1.0f) > 0.01f);
+
+ // Timestamp needs to be reported when seeking is done.
+ mOnSeekCompleteCalled.reset();
+ mOnMediaTimeDiscontinuityCalled.reset();
+ mPlayer.seekTo(3000);
+ mOnSeekCompleteCalled.waitForSignal();
+ do {
+ assertTrue(mOnMediaTimeDiscontinuityCalled.waitForSignal(1000));
+ } while (Math.abs(timestamps.getLast().getMediaClockRate() - 1.0f) > 0.01f);
+
+ // Timestamp needs to be updated when playback rate changes.
+ mOnMediaTimeDiscontinuityCalled.reset();
+ mPlayer.setPlaybackParams(new PlaybackParams().setSpeed(0.5f));
+ mOnMediaTimeDiscontinuityCalled.waitForSignal();
+ do {
+ assertTrue(mOnMediaTimeDiscontinuityCalled.waitForSignal(1000));
+ } while (Math.abs(timestamps.getLast().getMediaClockRate() - 0.5f) > 0.01f);
+
+ // Timestamp needs to be updated when player is paused.
+ mOnMediaTimeDiscontinuityCalled.reset();
+ mPlayer.pause();
+ mOnMediaTimeDiscontinuityCalled.waitForSignal();
+ do {
+ assertTrue(mOnMediaTimeDiscontinuityCalled.waitForSignal(1000));
+ } while (Math.abs(timestamps.getLast().getMediaClockRate() - 0.0f) > 0.01f);
+
+ mPlayer.reset();
+ }
+
/*
* This test assumes the resources being tested are between 8 and 14 seconds long
* The ones being used here are 10 seconds long.
*/
+ @Test
+ @LargeTest
public void testResumeAtEnd() throws Throwable {
int testsRun = testResumeAtEnd(R.raw.loudsoftmp3)
+ testResumeAtEnd(R.raw.loudsoftwav)
@@ -1704,7 +1749,7 @@
// sleep long enough that we restart playback at least once, but no more
Thread.sleep(10000);
assertTrue("MediaPlayer2 should still be playing",
- mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
mPlayer.reset();
assertEquals("wrong number of repetitions", 1, mOnCompletionCalled.getNumSignal());
return 1;
@@ -1763,7 +1808,7 @@
mOnPlayCalled.reset();
mPlayer.play();
mOnPlayCalled.waitForSignal();
- while (mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+ while (mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING) {
Log.i("@@@@", "position: " + mPlayer.getCurrentPosition());
Thread.sleep(500);
}
@@ -1834,10 +1879,10 @@
assertFalse(mOnCompletionCalled.isSignalled());
mPlayer.play();
mOnPlayCalled.waitForSignal();
- while (mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+ while (mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING) {
Thread.sleep(SLEEP_TIME);
}
- assertFalse(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertFalse(mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
mOnCompletionCalled.waitForSignal();
assertFalse(mOnErrorCalled.isSignalled());
mPlayer.reset();
@@ -1873,32 +1918,34 @@
mEventCallbacks.add(ecb);
}
- assertEquals(MediaPlayerBase.BUFFERING_STATE_UNKNOWN, mPlayer.getBufferingState());
- assertEquals(MediaPlayerBase.PLAYER_STATE_IDLE, mPlayer.getPlayerState());
+ MediaPlayerInterface playerBase = mPlayer.getMediaPlayerInterface();
+ assertEquals(MediaPlayerInterface.BUFFERING_STATE_UNKNOWN, playerBase.getBufferingState());
+ assertEquals(MediaPlayerInterface.PLAYER_STATE_IDLE, playerBase.getPlayerState());
prepareCompleted.reset();
- mPlayer.prepare();
+ playerBase.prepare();
prepareCompleted.waitForSignal();
- assertEquals(MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
- mPlayer.getBufferingState());
- assertEquals(MediaPlayerBase.PLAYER_STATE_PAUSED, mPlayer.getPlayerState());
+ assertEquals(MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
+ playerBase.getBufferingState());
+ assertEquals(MediaPlayerInterface.PLAYER_STATE_PAUSED, playerBase.getPlayerState());
+ assertEquals(MediaPlayer2.MEDIAPLAYER2_STATE_PREPARED, mPlayer.getMediaPlayer2State());
playCompleted.reset();
- mPlayer.play();
+ playerBase.play();
playCompleted.waitForSignal();
- assertEquals(MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
- mPlayer.getBufferingState());
- assertEquals(MediaPlayerBase.PLAYER_STATE_PLAYING, mPlayer.getPlayerState());
+ assertEquals(MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
+ playerBase.getBufferingState());
+ assertEquals(MediaPlayerInterface.PLAYER_STATE_PLAYING, playerBase.getPlayerState());
pauseCompleted.reset();
- mPlayer.pause();
+ playerBase.pause();
pauseCompleted.waitForSignal();
- assertEquals(MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
- mPlayer.getBufferingState());
- assertEquals(MediaPlayerBase.PLAYER_STATE_PAUSED, mPlayer.getPlayerState());
+ assertEquals(MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
+ playerBase.getBufferingState());
+ assertEquals(MediaPlayerInterface.PLAYER_STATE_PAUSED, playerBase.getPlayerState());
- mPlayer.reset();
- assertEquals(MediaPlayerBase.BUFFERING_STATE_UNKNOWN, mPlayer.getBufferingState());
- assertEquals(MediaPlayerBase.PLAYER_STATE_IDLE, mPlayer.getPlayerState());
+ playerBase.reset();
+ assertEquals(MediaPlayerInterface.BUFFERING_STATE_UNKNOWN, playerBase.getBufferingState());
+ assertEquals(MediaPlayerInterface.PLAYER_STATE_IDLE, playerBase.getPlayerState());
}
@Test
@@ -1909,9 +1956,13 @@
if (!checkLoadResource(R.raw.testvideo)) {
return; // skip;
}
+ final DataSourceDesc dsd2 = createDataSourceDesc(
+ R.raw.video_480x360_mp4_h264_1000kbps_30fps_aac_stereo_128kbps_44100hz);
+ mPlayer.setNextDataSource(dsd2);
mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+ final Monitor onDsdChangedCalled = new Monitor();
final Monitor onPrepareCalled = new Monitor();
final Monitor onSeekCompleteCalled = new Monitor();
final Monitor onPlayerStateChangedCalled = new Monitor();
@@ -1921,69 +1972,79 @@
final Monitor onPlaybackSpeedChanged = new Monitor();
final AtomicReference<Float> playbackSpeed = new AtomicReference<>();
- MediaPlayerBase.PlayerEventCallback callback = new MediaPlayerBase.PlayerEventCallback() {
- // TODO: implement and add test case for onCurrentDataSourceChanged() callback.
+ PlayerEventCallback callback = new PlayerEventCallback() {
@Override
- public void onMediaPrepared(MediaPlayerBase mpb, DataSourceDesc dsd) {
+ public void onCurrentDataSourceChanged(MediaPlayerInterface mpb, DataSourceDesc dsd) {
+ onDsdChangedCalled.signal();
+ }
+
+ @Override
+ public void onMediaPrepared(MediaPlayerInterface mpb, DataSourceDesc dsd) {
onPrepareCalled.signal();
}
@Override
- public void onPlayerStateChanged(MediaPlayerBase mpb, int state) {
+ public void onPlayerStateChanged(MediaPlayerInterface mpb, int state) {
playerState.set(state);
onPlayerStateChangedCalled.signal();
}
@Override
- public void onBufferingStateChanged(MediaPlayerBase mpb, DataSourceDesc dsd,
+ public void onBufferingStateChanged(MediaPlayerInterface mpb, DataSourceDesc dsd,
int state) {
bufferingState.set(state);
onBufferingStateChangedCalled.signal();
}
@Override
- public void onPlaybackSpeedChanged(MediaPlayerBase mpb, float speed) {
+ public void onPlaybackSpeedChanged(MediaPlayerInterface mpb, float speed) {
playbackSpeed.set(speed);
onPlaybackSpeedChanged.signal();
}
@Override
- public void onSeekCompleted(MediaPlayerBase mpb, long position) {
+ public void onSeekCompleted(MediaPlayerInterface mpb, long position) {
onSeekCompleteCalled.signal();
}
};
+ MediaPlayerInterface basePlayer = mPlayer.getMediaPlayerInterface();
ExecutorService executor = Executors.newFixedThreadPool(1);
- mPlayer.registerPlayerEventCallback(executor, callback);
+ basePlayer.registerPlayerEventCallback(executor, callback);
onPrepareCalled.reset();
onPlayerStateChangedCalled.reset();
onBufferingStateChangedCalled.reset();
- mPlayer.prepare();
+ basePlayer.prepare();
do {
assertTrue(onBufferingStateChangedCalled.waitForSignal(1000));
- } while (bufferingState.get() != MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_STARVED);
+ } while (bufferingState.get()
+ != MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_STARVED);
assertTrue(onPrepareCalled.waitForSignal(1000));
do {
assertTrue(onPlayerStateChangedCalled.waitForSignal(1000));
- } while (playerState.get() != MediaPlayerBase.PLAYER_STATE_PAUSED);
+ } while (playerState.get() != MediaPlayerInterface.PLAYER_STATE_PAUSED);
do {
assertTrue(onBufferingStateChangedCalled.waitForSignal(1000));
- } while (bufferingState.get() != MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
+ } while (bufferingState.get()
+ != MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
onSeekCompleteCalled.reset();
- mPlayer.seekTo(mp4Duration >> 1, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+ basePlayer.seekTo(mp4Duration >> 1);
onSeekCompleteCalled.waitForSignal();
onPlaybackSpeedChanged.reset();
- mPlayer.setPlaybackSpeed(0.5f);
+ basePlayer.setPlaybackSpeed(0.5f);
do {
assertTrue(onPlaybackSpeedChanged.waitForSignal(1000));
} while (Math.abs(playbackSpeed.get() - 0.5f) > FLOAT_TOLERANCE);
- mPlayer.reset();
+ basePlayer.skipToNext();
+ assertTrue(onDsdChangedCalled.waitForSignal(1000));
- mPlayer.unregisterPlayerEventCallback(callback);
+ basePlayer.reset();
+
+ basePlayer.unregisterPlayerEventCallback(callback);
executor.shutdown();
}
@@ -2087,12 +2148,12 @@
.setDataSource(dataSource)
.build());
playLoadedVideo(null, null, -1);
- assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertTrue(mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
// Test pause and restart.
mPlayer.pause();
Thread.sleep(SLEEP_TIME);
- assertFalse(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertFalse(mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
@Override
@@ -2114,7 +2175,7 @@
mOnPlayCalled.reset();
mPlayer.play();
mOnPlayCalled.waitForSignal();
- assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertTrue(mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
// Test reset.
mPlayer.reset();
@@ -2131,12 +2192,12 @@
mOnPlayCalled.reset();
mPlayer.play();
mOnPlayCalled.waitForSignal();
- assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertTrue(mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING);
// Test seek. Note: the seek position is cached and returned as the
// current position so there's no point in comparing them.
mPlayer.seekTo(duration - SLEEP_TIME, MediaPlayer2.SEEK_PREVIOUS_SYNC);
- while (mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+ while (mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING) {
Thread.sleep(SLEEP_TIME);
}
}
@@ -2259,4 +2320,78 @@
mPlayer.play();
assertTrue(mOnErrorCalled.waitForSignal());
}
+
+ @Test
+ @SmallTest
+ public void testClearPendingCommands() throws Exception {
+ final Monitor readAllowed = new Monitor();
+ Media2DataSource dataSource = new Media2DataSource() {
+ @Override
+ public int readAt(long position, byte[] buffer, int offset, int size)
+ throws IOException {
+ try {
+ readAllowed.waitForSignal();
+ } catch (InterruptedException e) {
+ fail();
+ }
+ return -1;
+ }
+
+ @Override
+ public long getSize() throws IOException {
+ return -1; // Unknown size
+ }
+
+ @Override
+ public void close() throws IOException {}
+ };
+ final ArrayDeque<Integer> commandsCompleted = new ArrayDeque<>();
+ setOnErrorListener();
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+ mOnPrepareCalled.signal();
+ }
+ }
+
+ @Override
+ public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+ commandsCompleted.add(what);
+ }
+
+ @Override
+ public void onError(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ mOnErrorCalled.signal();
+ }
+ };
+ synchronized (mEventCbLock) {
+ mEventCallbacks.add(ecb);
+ }
+
+ mOnPrepareCalled.reset();
+ mOnErrorCalled.reset();
+
+ mPlayer.setDataSource(new DataSourceDesc.Builder()
+ .setDataSource(dataSource)
+ .build());
+
+ // prepare() will be pending until readAllowed is signaled.
+ mPlayer.prepare();
+
+ mPlayer.play();
+ mPlayer.pause();
+ mPlayer.play();
+ mPlayer.pause();
+ mPlayer.play();
+ mPlayer.seekTo(1000);
+
+ // Cause a failure on the pending prepare operation.
+ readAllowed.signal();
+ mOnErrorCalled.waitForSignal();
+ assertEquals(0, mOnPrepareCalled.getNumSignal());
+ assertEquals(1, commandsCompleted.size());
+ assertEquals(MediaPlayer2.CALL_COMPLETED_SET_DATA_SOURCE,
+ (int) commandsCompleted.peekFirst());
+ }
}
diff --git a/media/src/androidTest/java/androidx/media/MediaPlayer2TestBase.java b/media/src/androidTest/java/androidx/media/MediaPlayer2TestBase.java
index 215993a..77c5c04 100644
--- a/media/src/androidTest/java/androidx/media/MediaPlayer2TestBase.java
+++ b/media/src/androidTest/java/androidx/media/MediaPlayer2TestBase.java
@@ -15,20 +15,29 @@
*/
package androidx.media;
+import static android.content.Context.KEYGUARD_SERVICE;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import android.app.Instrumentation;
+import android.app.KeyguardManager;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
import android.media.AudioManager;
import android.media.MediaTimestamp;
+import android.media.SubtitleData;
import android.media.TimedMetaData;
import android.net.Uri;
+import android.os.PersistableBundle;
+import android.os.PowerManager;
+import android.support.test.InstrumentationRegistry;
import android.support.test.rule.ActivityTestRule;
import android.view.SurfaceHolder;
+import android.view.WindowManager;
import androidx.annotation.CallSuper;
@@ -41,6 +50,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Logger;
@@ -65,6 +75,7 @@
protected Monitor mOnCompletionCalled = new Monitor();
protected Monitor mOnInfoCalled = new Monitor();
protected Monitor mOnErrorCalled = new Monitor();
+ protected Monitor mOnMediaTimeDiscontinuityCalled = new Monitor();
protected int mCallStatus;
protected Context mContext;
@@ -75,34 +86,35 @@
protected MediaPlayer2 mPlayer = null;
protected MediaPlayer2 mPlayer2 = null;
protected MediaStubActivity mActivity;
+ protected Instrumentation mInstrumentation;
protected final Object mEventCbLock = new Object();
- protected List<MediaPlayer2.MediaPlayer2EventCallback> mEventCallbacks =
- new ArrayList<MediaPlayer2.MediaPlayer2EventCallback>();
+ protected List<MediaPlayer2.MediaPlayer2EventCallback> mEventCallbacks = new ArrayList<>();
protected final Object mEventCbLock2 = new Object();
- protected List<MediaPlayer2.MediaPlayer2EventCallback> mEventCallbacks2 =
- new ArrayList<MediaPlayer2.MediaPlayer2EventCallback>();
+ protected List<MediaPlayer2.MediaPlayer2EventCallback> mEventCallbacks2 = new ArrayList<>();
@Rule
public ActivityTestRule<MediaStubActivity> mActivityRule =
new ActivityTestRule<>(MediaStubActivity.class);
+ public PowerManager.WakeLock mScreenLock;
+ private KeyguardManager mKeyguardManager;
// convenience functions to create MediaPlayer2
- protected static MediaPlayer2 createMediaPlayer2(Context context, Uri uri) {
+ protected MediaPlayer2 createMediaPlayer2(Context context, Uri uri) {
return createMediaPlayer2(context, uri, null);
}
- protected static MediaPlayer2 createMediaPlayer2(Context context, Uri uri,
+ protected MediaPlayer2 createMediaPlayer2(Context context, Uri uri,
SurfaceHolder holder) {
AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
int s = am.generateAudioSessionId();
return createMediaPlayer2(context, uri, holder, null, s > 0 ? s : 0);
}
- protected static MediaPlayer2 createMediaPlayer2(Context context, Uri uri, SurfaceHolder holder,
+ protected MediaPlayer2 createMediaPlayer2(Context context, Uri uri, SurfaceHolder holder,
AudioAttributesCompat audioAttributes, int audioSessionId) {
try {
- MediaPlayer2 mp = MediaPlayer2.create();
+ MediaPlayer2 mp = createMediaPlayer2OnUiThread();
final AudioAttributesCompat aa = audioAttributes != null ? audioAttributes :
new AudioAttributesCompat.Builder().build();
mp.setAudioAttributes(aa);
@@ -144,13 +156,13 @@
return null;
}
- protected static MediaPlayer2 createMediaPlayer2(Context context, int resid) {
+ protected MediaPlayer2 createMediaPlayer2(Context context, int resid) {
AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
int s = am.generateAudioSessionId();
return createMediaPlayer2(context, resid, null, s > 0 ? s : 0);
}
- protected static MediaPlayer2 createMediaPlayer2(Context context, int resid,
+ protected MediaPlayer2 createMediaPlayer2(Context context, int resid,
AudioAttributesCompat audioAttributes, int audioSessionId) {
try {
AssetFileDescriptor afd = context.getResources().openRawResourceFd(resid);
@@ -158,7 +170,7 @@
return null;
}
- MediaPlayer2 mp = MediaPlayer2.create();
+ MediaPlayer2 mp = createMediaPlayer2OnUiThread();
final AudioAttributesCompat aa = audioAttributes != null ? audioAttributes :
new AudioAttributesCompat.Builder().build();
@@ -204,6 +216,20 @@
return null;
}
+ private MediaPlayer2 createMediaPlayer2OnUiThread() {
+ final MediaPlayer2[] mp = new MediaPlayer2[1];
+ try {
+ mActivityRule.runOnUiThread(new Runnable() {
+ public void run() {
+ mp[0] = MediaPlayer2.create();
+ }
+ });
+ } catch (Throwable throwable) {
+ fail("Failed to create MediaPlayer2 instance on UI thread.");
+ }
+ return mp[0];
+ }
+
public static class Monitor {
private int mNumSignal;
@@ -258,8 +284,23 @@
@Before
@CallSuper
- public void setUp() throws Exception {
+ public void setUp() throws Throwable {
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ mKeyguardManager = (KeyguardManager)
+ mInstrumentation.getTargetContext().getSystemService(KEYGUARD_SERVICE);
mActivity = mActivityRule.getActivity();
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // Keep screen on while testing.
+ mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ mActivity.setTurnScreenOn(true);
+ mActivity.setShowWhenLocked(true);
+ mKeyguardManager.requestDismissKeyguard(mActivity, null);
+ }
+ });
+ mInstrumentation.waitForIdleSync();
+
try {
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
@@ -344,11 +385,11 @@
}
@Override
- public void onMediaTimeChanged(MediaPlayer2 mp, DataSourceDesc dsd,
+ public void onMediaTimeDiscontinuity(MediaPlayer2 mp, DataSourceDesc dsd,
MediaTimestamp timestamp) {
synchronized (cbLock) {
for (MediaPlayer2.MediaPlayer2EventCallback ecb : ecbs) {
- ecb.onMediaTimeChanged(mp, dsd, timestamp);
+ ecb.onMediaTimeDiscontinuity(mp, dsd, timestamp);
}
}
}
@@ -361,6 +402,15 @@
}
}
}
+ @Override
+ public void onSubtitleData(MediaPlayer2 mp, DataSourceDesc dsd,
+ final SubtitleData data) {
+ synchronized (cbLock) {
+ for (MediaPlayer2.MediaPlayer2EventCallback ecb : ecbs) {
+ ecb.onSubtitleData(mp, dsd, data);
+ }
+ }
+ }
});
}
@@ -490,11 +540,7 @@
boolean audioOnly = (width != null && width.intValue() == -1)
|| (height != null && height.intValue() == -1);
-
mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
- /* FIXME: ensure that screen is on in activity level.
- mPlayer.setScreenOnWhilePlaying(true);
- */
synchronized (mEventCbLock) {
mEventCallbacks.add(new MediaPlayer2.MediaPlayer2EventCallback() {
@@ -559,13 +605,49 @@
if (playTime == -1) {
return;
} else if (playTime == 0) {
- while (mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+ while (mPlayer.getMediaPlayer2State() == MediaPlayer2.MEDIAPLAYER2_STATE_PLAYING) {
Thread.sleep(SLEEP_TIME);
}
} else {
Thread.sleep(playTime);
}
+ // validate a few MediaMetrics.
+ PersistableBundle metrics = mPlayer.getMetrics();
+ if (metrics == null) {
+ fail("MediaPlayer.getMetrics() returned null metrics");
+ } else if (metrics.isEmpty()) {
+ fail("MediaPlayer.getMetrics() returned empty metrics");
+ } else {
+
+ int size = metrics.size();
+ Set<String> keys = metrics.keySet();
+
+ if (keys == null) {
+ fail("MediaMetricsSet returned no keys");
+ } else if (keys.size() != size) {
+ fail("MediaMetricsSet.keys().size() mismatch MediaMetricsSet.size()");
+ }
+
+ // we played something; so one of these should be non-null
+ String vmime = metrics.getString(MediaPlayer2.MetricsConstants.MIME_TYPE_VIDEO, null);
+ String amime = metrics.getString(MediaPlayer2.MetricsConstants.MIME_TYPE_AUDIO, null);
+ if (vmime == null && amime == null) {
+ fail("getMetrics() returned neither video nor audio mime value");
+ }
+
+ long duration = metrics.getLong(MediaPlayer2.MetricsConstants.DURATION, -2);
+ if (duration == -2) {
+ fail("getMetrics() didn't return a duration");
+ }
+ long playing = metrics.getLong(MediaPlayer2.MetricsConstants.PLAYING, -2);
+ if (playing == -2) {
+ fail("getMetrics() didn't return a playing time");
+ }
+ if (!keys.contains(MediaPlayer2.MetricsConstants.PLAYING)) {
+ fail("MediaMetricsSet.keys() missing: " + MediaPlayer2.MetricsConstants.PLAYING);
+ }
+ }
mPlayer.reset();
}
diff --git a/media/src/androidTest/java/androidx/media/MediaSession2Test.java b/media/src/androidTest/java/androidx/media/MediaSession2Test.java
index 5e7ed0e..adcba6b 100644
--- a/media/src/androidTest/java/androidx/media/MediaSession2Test.java
+++ b/media/src/androidTest/java/androidx/media/MediaSession2Test.java
@@ -65,7 +65,7 @@
/**
* Tests {@link MediaSession2}.
*/
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
@RunWith(AndroidJUnit4.class)
@SmallTest
public class MediaSession2Test extends MediaSession2TestBase {
@@ -125,7 +125,7 @@
@Test
public void testPlayerStateChange() throws Exception {
prepareLooper();
- final int targetState = MediaPlayerBase.PLAYER_STATE_PLAYING;
+ final int targetState = MediaPlayerInterface.PLAYER_STATE_PLAYING;
final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
sHandler.postAndSync(new Runnable() {
@Override
@@ -136,7 +136,7 @@
.setSessionCallback(sHandlerExecutor, new SessionCallback() {
@Override
public void onPlayerStateChanged(MediaSession2 session,
- MediaPlayerBase player, int state) {
+ MediaPlayerInterface player, int state) {
assertEquals(targetState, state);
latchForSessionCallback.countDown();
}
@@ -166,7 +166,7 @@
final List<MediaItem2> playlist = TestUtils.createPlaylist(5);
final MediaItem2 targetItem = playlist.get(3);
- final int targetBufferingState = MediaPlayerBase.BUFFERING_STATE_BUFFERING_COMPLETE;
+ final int targetBufferingState = MediaPlayerInterface.BUFFERING_STATE_BUFFERING_COMPLETE;
final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
sHandler.postAndSync(new Runnable() {
@Override
@@ -179,7 +179,7 @@
.setSessionCallback(sHandlerExecutor, new SessionCallback() {
@Override
public void onBufferingStateChanged(MediaSession2 session,
- MediaPlayerBase player, MediaItem2 item, int state) {
+ MediaPlayerInterface player, MediaItem2 item, int state) {
assertEquals(targetItem, item);
assertEquals(targetBufferingState, state);
latchForSessionCallback.countDown();
@@ -200,13 +200,39 @@
}
});
- mPlayer.notifyBufferingState(targetItem, targetBufferingState);
+ mPlayer.notifyBufferingStateChanged(targetItem.getDataSourceDesc(), targetBufferingState);
assertTrue(latchForSessionCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
assertTrue(latchForControllerCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
assertEquals(targetBufferingState, controller.getBufferingState());
}
@Test
+ public void testSeekCompleted() throws Exception {
+ prepareLooper();
+ final long testPosition = 1001;
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onSeekCompleted(
+ MediaSession2 session, MediaPlayerInterface mpb, long position) {
+ assertEquals(mPlayer, mpb);
+ assertEquals(testPosition, position);
+ latch.countDown();
+ }
+ };
+
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setPlaylistAgent(mMockAgent)
+ .setId("testSeekCompleted")
+ .setSessionCallback(sHandlerExecutor, callback).build()) {
+ mPlayer.mCurrentPosition = testPosition;
+ mPlayer.notifySeekCompleted(testPosition);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
public void testCurrentDataSourceChanged() throws Exception {
prepareLooper();
final int listSize = 5;
@@ -214,9 +240,7 @@
mMockAgent.setPlaylist(list, null);
final MediaItem2 currentItem = list.get(3);
- mMockAgent.mCurrentMediaItem = currentItem;
-
- final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
+ final CountDownLatch latchForSessionCallback = new CountDownLatch(2);
try (MediaSession2 session = new MediaSession2.Builder(mContext)
.setPlayer(mPlayer)
.setPlaylistAgent(mMockAgent)
@@ -224,27 +248,44 @@
.setSessionCallback(sHandlerExecutor, new SessionCallback() {
@Override
public void onCurrentMediaItemChanged(MediaSession2 session,
- MediaPlayerBase player, MediaItem2 itemOut) {
- assertSame(currentItem, itemOut);
+ MediaPlayerInterface player, MediaItem2 item) {
+ switch ((int) latchForSessionCallback.getCount()) {
+ case 2:
+ assertEquals(currentItem, item);
+ break;
+ case 1:
+ assertNull(item);
+ }
latchForSessionCallback.countDown();
}
}).build()) {
- final CountDownLatch latchForControllerCallback = new CountDownLatch(1);
+ final CountDownLatch latchForControllerCallback = new CountDownLatch(2);
final MediaController2 controller =
createController(mSession.getToken(), true, new ControllerCallback() {
@Override
public void onCurrentMediaItemChanged(MediaController2 controller,
MediaItem2 item) {
- assertEquals(currentItem, item);
+ switch ((int) latchForControllerCallback.getCount()) {
+ case 2:
+ assertEquals(currentItem, item);
+ break;
+ case 1:
+ assertNull(item);
+ }
latchForControllerCallback.countDown();
}
});
+ // Player notifies with the unknown dsd. Should be ignored.
+ mPlayer.notifyCurrentDataSourceChanged(TestUtils.createMediaItemWithMetadata()
+ .getDataSourceDesc());
+ // Known DSD should be notified through the onCurrentMediaItemChanged.
mPlayer.notifyCurrentDataSourceChanged(currentItem.getDataSourceDesc());
+ // Null DSD becomes null MediaItem2.
+ mPlayer.notifyCurrentDataSourceChanged(null);
assertTrue(latchForSessionCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
assertTrue(latchForControllerCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
- assertEquals(currentItem, controller.getCurrentMediaItem());
}
}
@@ -264,7 +305,7 @@
.setId("testMediaPrepared")
.setSessionCallback(sHandlerExecutor, new SessionCallback() {
@Override
- public void onMediaPrepared(MediaSession2 session, MediaPlayerBase player,
+ public void onMediaPrepared(MediaSession2 session, MediaPlayerInterface player,
MediaItem2 itemOut) {
assertSame(currentItem, itemOut);
latchForSessionCallback.countDown();
@@ -285,7 +326,7 @@
mMockAgent.setPlaylist(list, null);
final MediaItem2 currentItem = list.get(3);
- final int buffState = MediaPlayerBase.BUFFERING_STATE_BUFFERING_COMPLETE;
+ final int buffState = MediaPlayerInterface.BUFFERING_STATE_BUFFERING_COMPLETE;
final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
try (MediaSession2 session = new MediaSession2.Builder(mContext)
@@ -295,7 +336,7 @@
.setSessionCallback(sHandlerExecutor, new SessionCallback() {
@Override
public void onBufferingStateChanged(MediaSession2 session,
- MediaPlayerBase player, MediaItem2 itemOut, int stateOut) {
+ MediaPlayerInterface player, MediaItem2 itemOut, int stateOut) {
assertSame(currentItem, itemOut);
assertEquals(buffState, stateOut);
latchForSessionCallback.countDown();
@@ -325,7 +366,7 @@
.setSessionCallback(sHandlerExecutor, new SessionCallback() {
@Override
public void onPlaybackSpeedChanged(MediaSession2 session,
- MediaPlayerBase player, float speedOut) {
+ MediaPlayerInterface player, float speedOut) {
assertEquals(speed, speedOut, 0.0f);
latchForSessionCallback.countDown();
}
@@ -352,7 +393,7 @@
@Test
public void testUpdatePlayer() throws Exception {
prepareLooper();
- final int targetState = MediaPlayerBase.PLAYER_STATE_PLAYING;
+ final int targetState = MediaPlayerInterface.PLAYER_STATE_PLAYING;
final CountDownLatch latch = new CountDownLatch(1);
sHandler.postAndSync(new Runnable() {
@Override
@@ -362,7 +403,7 @@
.setSessionCallback(sHandlerExecutor, new SessionCallback() {
@Override
public void onPlayerStateChanged(MediaSession2 session,
- MediaPlayerBase player, int state) {
+ MediaPlayerInterface player, int state) {
assertEquals(targetState, state);
latch.countDown();
}
@@ -383,12 +424,12 @@
assertNotNull(mSession.getPlaylistAgent());
assertNotEquals(agent, mSession.getPlaylistAgent());
- player.notifyPlaybackState(MediaPlayerBase.PLAYER_STATE_PLAYING);
+ player.notifyPlaybackState(MediaPlayerInterface.PLAYER_STATE_PLAYING);
assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
}
@Test
- public void testSetPlayer_playbackInfo() throws Exception {
+ public void testUpdatePlayer_playbackInfo() throws Exception {
prepareLooper();
MockPlayer player = new MockPlayer(0);
final AudioAttributesCompat attrs = new AudioAttributesCompat.Builder()
@@ -482,6 +523,47 @@
}
@Test
+ public void testGetDuration() throws Exception {
+ prepareLooper();
+ final long testDuration = 9999;
+ mPlayer.mDuration = testDuration;
+ assertEquals(testDuration, mSession.getDuration());
+ }
+
+ @Test
+ public void testSessionCallback_onMediaPrepared() throws Exception {
+ prepareLooper();
+ final long testDuration = 9999;
+ final List<MediaItem2> list = TestUtils.createPlaylist(2);
+ final MediaItem2 testItem = list.get(1);
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ mPlayer.mDuration = testDuration;
+ mMockAgent.setPlaylist(list, null);
+ mMockAgent.mCurrentMediaItem = testItem;
+
+ final SessionCallback sessionCallback = new SessionCallback() {
+ @Override
+ public void onMediaPrepared(MediaSession2 session, MediaPlayerInterface player,
+ MediaItem2 item) {
+ assertEquals(testItem, item);
+ assertEquals(testDuration,
+ item.getMetadata().getLong(MediaMetadata2.METADATA_KEY_DURATION));
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setPlaylistAgent(mMockAgent)
+ .setId("testSessionCallback")
+ .setSessionCallback(sHandlerExecutor, sessionCallback)
+ .build()) {
+ mPlayer.notifyMediaPrepared(testItem.getDataSourceDesc());
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
public void testSetPlaybackSpeed() throws Exception {
prepareLooper();
final float speed = 1.5f;
@@ -532,7 +614,7 @@
@Test
public void testGetPlayerState() {
prepareLooper();
- final int state = MediaPlayerBase.PLAYER_STATE_PLAYING;
+ final int state = MediaPlayerInterface.PLAYER_STATE_PLAYING;
mPlayer.mLastPlayerState = state;
assertEquals(state, mSession.getPlayerState());
}
@@ -540,7 +622,7 @@
@Test
public void testGetBufferingState() {
prepareLooper();
- final int bufferingState = MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_PLAYABLE;
+ final int bufferingState = MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_PLAYABLE;
mPlayer.mLastBufferingState = bufferingState;
assertEquals(bufferingState, mSession.getBufferingState());
}
@@ -744,7 +826,7 @@
mSession.updatePlayer(player, null, null);
mSession.updatePlayer(mPlayer, null, null);
- player.notifyPlaybackState(MediaPlayerBase.PLAYER_STATE_PAUSED);
+ player.notifyPlaybackState(MediaPlayerInterface.PLAYER_STATE_PAUSED);
assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
}
@@ -757,7 +839,7 @@
@Override
public void unregisterPlayerEventCallback(
- @NonNull MediaPlayerBase.PlayerEventCallback listener) {
+ @NonNull MediaPlayerInterface.PlayerEventCallback listener) {
// No-op.
}
}
diff --git a/media/src/androidTest/java/androidx/media/MediaSession2TestBase.java b/media/src/androidTest/java/androidx/media/MediaSession2TestBase.java
index 745ef3a..9c9fa8e 100644
--- a/media/src/androidTest/java/androidx/media/MediaSession2TestBase.java
+++ b/media/src/androidTest/java/androidx/media/MediaSession2TestBase.java
@@ -20,6 +20,7 @@
import static junit.framework.Assert.assertTrue;
import android.content.Context;
+import android.os.Build;
import android.os.Bundle;
import android.os.HandlerThread;
import android.os.Looper;
@@ -121,7 +122,11 @@
if (sHandler == null) {
return;
}
- sHandler.getLooper().quitSafely();
+ if (Build.VERSION.SDK_INT >= 18) {
+ sHandler.getLooper().quitSafely();
+ } else {
+ sHandler.getLooper().quit();
+ }
sHandler = null;
sHandlerExecutor = null;
}
diff --git a/media/src/androidTest/java/androidx/media/MediaSession2_PermissionTest.java b/media/src/androidTest/java/androidx/media/MediaSession2_PermissionTest.java
index 3895ea5..0bf93d4 100644
--- a/media/src/androidTest/java/androidx/media/MediaSession2_PermissionTest.java
+++ b/media/src/androidTest/java/androidx/media/MediaSession2_PermissionTest.java
@@ -48,9 +48,11 @@
import static org.junit.Assert.fail;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.os.Process;
import android.support.test.filters.MediumTest;
+import android.support.test.filters.SdkSuppress;
import android.support.test.runner.AndroidJUnit4;
import androidx.annotation.NonNull;
@@ -68,6 +70,7 @@
/**
* Tests whether {@link MediaSession2} receives commands that hasn't allowed.
*/
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
@RunWith(AndroidJUnit4.class)
@MediumTest
public class MediaSession2_PermissionTest extends MediaSession2TestBase {
diff --git a/media/src/androidTest/java/androidx/media/MediaSessionManager_MediaSession2Test.java b/media/src/androidTest/java/androidx/media/MediaSessionManager_MediaSession2Test.java
index 904b768..a0ce092 100644
--- a/media/src/androidTest/java/androidx/media/MediaSessionManager_MediaSession2Test.java
+++ b/media/src/androidTest/java/androidx/media/MediaSessionManager_MediaSession2Test.java
@@ -73,7 +73,7 @@
public void testGetMediaSession2Tokens_hasMediaController() throws InterruptedException {
prepareLooper();
final MockPlayer player = (MockPlayer) mSession.getPlayer();
- player.notifyPlaybackState(MediaPlayerBase.PLAYER_STATE_IDLE);
+ player.notifyPlaybackState(MediaPlayerInterface.PLAYER_STATE_IDLE);
MediaController2 controller = null;
// List<SessionToken2> tokens = mManager.getActiveSessionTokens();
@@ -89,7 +89,7 @@
// assertNotNull(controller);
//
// // Test if the found controller is correct one.
-// assertEquals(MediaPlayerBase.PLAYER_STATE_IDLE, controller.getPlayerState());
+// assertEquals(MediaPlayerInterface.PLAYER_STATE_IDLE, controller.getPlayerState());
// controller.play();
//
// assertTrue(player.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
diff --git a/media/src/androidTest/java/androidx/media/MockPlayer.java b/media/src/androidTest/java/androidx/media/MockPlayer.java
index 49b1a19..e280ed8 100644
--- a/media/src/androidTest/java/androidx/media/MockPlayer.java
+++ b/media/src/androidTest/java/androidx/media/MockPlayer.java
@@ -16,18 +16,17 @@
package androidx.media;
-import android.util.ArrayMap;
-
import androidx.annotation.NonNull;
+import androidx.collection.ArrayMap;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
/**
- * A mock implementation of {@link MediaPlayerBase} for testing.
+ * A mock implementation of {@link MediaPlayerInterface} for testing.
*/
-public class MockPlayer extends MediaPlayerBase {
+public class MockPlayer extends MediaPlayerInterface {
public final CountDownLatch mCountDownLatch;
public boolean mPlayCalled;
@@ -42,6 +41,7 @@
public float mPlaybackSpeed = 1.0f;
public @PlayerState int mLastPlayerState;
public @BuffState int mLastBufferingState;
+ public long mDuration;
public ArrayMap<PlayerEventCallback, Executor> mCallbacks = new ArrayMap<>();
@@ -128,6 +128,11 @@
}
@Override
+ public long getDuration() {
+ return mDuration;
+ }
+
+ @Override
public void registerPlayerEventCallback(@NonNull Executor executor,
@NonNull PlayerEventCallback callback) {
if (callback == null || executor == null) {
@@ -155,21 +160,6 @@
}
}
- public void notifyBufferingState(final MediaItem2 item, final int bufferingState) {
- mLastBufferingState = bufferingState;
- for (int i = 0; i < mCallbacks.size(); i++) {
- final PlayerEventCallback callback = mCallbacks.keyAt(i);
- final Executor executor = mCallbacks.valueAt(i);
- executor.execute(new Runnable() {
- @Override
- public void run() {
- callback.onBufferingStateChanged(
- MockPlayer.this, item.getDataSourceDesc(), bufferingState);
- }
- });
- }
- }
-
public void notifyCurrentDataSourceChanged(final DataSourceDesc dsd) {
for (int i = 0; i < mCallbacks.size(); i++) {
final PlayerEventCallback callback = mCallbacks.keyAt(i);
@@ -223,6 +213,19 @@
}
}
+ public void notifySeekCompleted(final long position) {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ final PlayerEventCallback callback = mCallbacks.keyAt(i);
+ final Executor executor = mCallbacks.valueAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ callback.onSeekCompleted(MockPlayer.this, position);
+ }
+ });
+ }
+ }
+
public void notifyError(int what) {
for (int i = 0; i < mCallbacks.size(); i++) {
final PlayerEventCallback callback = mCallbacks.keyAt(i);
diff --git a/media/src/androidTest/java/androidx/media/SessionToken2Test.java b/media/src/androidTest/java/androidx/media/SessionToken2Test.java
index 22881a8..bf96eb9 100644
--- a/media/src/androidTest/java/androidx/media/SessionToken2Test.java
+++ b/media/src/androidTest/java/androidx/media/SessionToken2Test.java
@@ -20,8 +20,10 @@
import android.content.ComponentName;
import android.content.Context;
+import android.os.Build;
import android.os.Process;
import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SdkSuppress;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
@@ -32,6 +34,7 @@
/**
* Tests {@link SessionToken2}.
*/
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
@RunWith(AndroidJUnit4.class)
@SmallTest
public class SessionToken2Test {
diff --git a/media/src/androidTest/java/androidx/media/TestUtils.java b/media/src/androidTest/java/androidx/media/TestUtils.java
index 1e3ba9b..2b02b75 100644
--- a/media/src/androidTest/java/androidx/media/TestUtils.java
+++ b/media/src/androidTest/java/androidx/media/TestUtils.java
@@ -25,10 +25,11 @@
import android.os.Handler;
import android.os.Looper;
+import androidx.core.util.ObjectsCompat;
+
import java.io.FileDescriptor;
import java.util.ArrayList;
import java.util.List;
-import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -68,18 +69,29 @@
* incorrect if any bundle contains a bundle.
*/
public static boolean equals(Bundle a, Bundle b) {
+ return contains(a, b) && contains(b, a);
+ }
+
+ /**
+ * Checks whether a Bundle contains another bundle.
+ *
+ * @param a a bundle
+ * @param b another bundle
+ * @return {@code true} if a contains b. {@code false} otherwise. This may be incorrect if any
+ * bundle contains a bundle.
+ */
+ public static boolean contains(Bundle a, Bundle b) {
if (a == b) {
return true;
}
if (a == null || b == null) {
+ return b == null;
+ }
+ if (!a.keySet().containsAll(b.keySet())) {
return false;
}
- if (!a.keySet().containsAll(b.keySet())
- || !b.keySet().containsAll(a.keySet())) {
- return false;
- }
- for (String key : a.keySet()) {
- if (!Objects.equals(a.get(key), b.get(key))) {
+ for (String key : b.keySet()) {
+ if (!ObjectsCompat.equals(a.get(key), b.get(key))) {
return false;
}
}
@@ -91,7 +103,7 @@
* <p>
* Caller's method name will be used for prefix of each media item's media id.
*
- * @param size lits size
+ * @param size list size
* @return the newly created playlist
*/
public static List<MediaItem2> createPlaylist(int size) {
@@ -100,11 +112,7 @@
for (int i = 0; i < size; i++) {
list.add(new MediaItem2.Builder(MediaItem2.FLAG_PLAYABLE)
.setMediaId(caller + "_item_" + (size + 1))
- .setDataSourceDesc(
- new DataSourceDesc.Builder()
- .setDataSource(new FileDescriptor())
- .build())
- .build());
+ .setDataSourceDesc(createDSD()).build());
}
return list;
}
@@ -117,7 +125,7 @@
*/
public static MediaItem2 createMediaItemWithMetadata() {
return new MediaItem2.Builder(MediaItem2.FLAG_PLAYABLE)
- .setMetadata(createMetadata()).build();
+ .setMetadata(createMetadata()).setDataSourceDesc(createDSD()).build();
}
/**
@@ -133,6 +141,21 @@
.putString(MediaMetadata2.METADATA_KEY_MEDIA_ID, mediaId).build();
}
+ private static DataSourceDesc createDSD() {
+ return new DataSourceDesc.Builder().setDataSource(new FileDescriptor()).build();
+ }
+
+ /**
+ * Create a bundle for testing purpose.
+ *
+ * @return the newly created bundle.
+ */
+ public static Bundle createTestBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putString("test_key", "test_value");
+ return bundle;
+ }
+
/**
* Handler that always waits until the Runnable finishes.
*/
diff --git a/media/src/main/java/android/support/v4/media/MediaBrowserCompat.java b/media/src/main/java/android/support/v4/media/MediaBrowserCompat.java
index a0e839b..aa039c3 100644
--- a/media/src/main/java/android/support/v4/media/MediaBrowserCompat.java
+++ b/media/src/main/java/android/support/v4/media/MediaBrowserCompat.java
@@ -15,6 +15,7 @@
*/
package android.support.v4.media;
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_ADD_SUBSCRIPTION;
import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_CONNECT;
@@ -32,6 +33,7 @@
import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_ID;
import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_LIST;
import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_SESSION_TOKEN;
+import static androidx.media.MediaBrowserProtocol.DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS;
import static androidx.media.MediaBrowserProtocol.DATA_OPTIONS;
import static androidx.media.MediaBrowserProtocol.DATA_PACKAGE_NAME;
import static androidx.media.MediaBrowserProtocol.DATA_RESULT_RECEIVER;
@@ -428,6 +430,20 @@
}
/**
+ * Gets the options which is passed to {@link MediaBrowserServiceCompat#notifyChildrenChanged(
+ * String, Bundle)} call that triggered {@link SubscriptionCallback#onChildrenLoaded}.
+ * This should be called inside of {@link SubscriptionCallback#onChildrenLoaded}.
+ *
+ * @return A bundle which is passed to {@link MediaBrowserServiceCompat#notifyChildrenChanged(
+ * String, Bundle)}
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public @Nullable Bundle getNotifyChildrenChangedOptions() {
+ return mImpl.getNotifyChildrenChangedOptions();
+ }
+
+ /**
* A class with information on a single media item for use in browsing/searching media.
* MediaItems are application dependent so we cannot guarantee that they contain the
* right values.
@@ -951,13 +967,15 @@
void search(@NonNull String query, Bundle extras, @NonNull SearchCallback callback);
void sendCustomAction(@NonNull String action, Bundle extras,
@Nullable CustomActionCallback callback);
+ @Nullable Bundle getNotifyChildrenChangedOptions();
}
interface MediaBrowserServiceCallbackImpl {
void onServiceConnected(Messenger callback, String root, MediaSessionCompat.Token session,
Bundle extra);
void onConnectionFailed(Messenger callback);
- void onLoadChildren(Messenger callback, String parentId, List list, Bundle options);
+ void onLoadChildren(Messenger callback, String parentId, List list, Bundle options,
+ Bundle notifyChildrenChangedOptions);
}
static class MediaBrowserImplBase
@@ -982,6 +1000,7 @@
private String mRootId;
private MediaSessionCompat.Token mMediaSessionToken;
private Bundle mExtras;
+ private Bundle mNotifyChildrenChangedOptions;
public MediaBrowserImplBase(Context context, ComponentName serviceComponent,
ConnectionCallback callback, Bundle rootHints) {
@@ -1372,7 +1391,7 @@
@Override
public void onLoadChildren(final Messenger callback, final String parentId,
- final List list, final Bundle options) {
+ final List list, final Bundle options, final Bundle notifyChildrenChangedOptions) {
// Check that there hasn't been a disconnect or a different ServiceConnection.
if (!isCurrent(callback, "onLoadChildren")) {
return;
@@ -1398,18 +1417,27 @@
if (list == null) {
subscriptionCallback.onError(parentId);
} else {
+ mNotifyChildrenChangedOptions = notifyChildrenChangedOptions;
subscriptionCallback.onChildrenLoaded(parentId, list);
+ mNotifyChildrenChangedOptions = null;
}
} else {
if (list == null) {
subscriptionCallback.onError(parentId, options);
} else {
+ mNotifyChildrenChangedOptions = notifyChildrenChangedOptions;
subscriptionCallback.onChildrenLoaded(parentId, list, options);
+ mNotifyChildrenChangedOptions = null;
}
}
}
}
+ @Override
+ public Bundle getNotifyChildrenChangedOptions() {
+ return mNotifyChildrenChangedOptions;
+ }
+
/**
* For debugging.
*/
@@ -1589,6 +1617,7 @@
protected ServiceBinderWrapper mServiceBinderWrapper;
protected Messenger mCallbacksMessenger;
private MediaSessionCompat.Token mMediaSessionToken;
+ private Bundle mNotifyChildrenChangedOptions;
MediaBrowserImplApi21(Context context, ComponentName serviceComponent,
ConnectionCallback callback, Bundle rootHints) {
@@ -1862,7 +1891,7 @@
mCallbacksMessenger = new Messenger(mHandler);
mHandler.setCallbacksMessenger(mCallbacksMessenger);
try {
- mServiceBinderWrapper.registerCallbackMessenger(mCallbacksMessenger);
+ mServiceBinderWrapper.registerCallbackMessenger(mContext, mCallbacksMessenger);
} catch (RemoteException e) {
Log.i(TAG, "Remote error registering client messenger." );
}
@@ -1901,7 +1930,8 @@
@Override
@SuppressWarnings("ReferenceEquality")
- public void onLoadChildren(Messenger callback, String parentId, List list, Bundle options) {
+ public void onLoadChildren(Messenger callback, String parentId, List list, Bundle options,
+ Bundle notifyChildrenChangedOptions) {
if (mCallbacksMessenger != callback) {
return;
}
@@ -1922,17 +1952,26 @@
if (list == null) {
subscriptionCallback.onError(parentId);
} else {
+ mNotifyChildrenChangedOptions = notifyChildrenChangedOptions;
subscriptionCallback.onChildrenLoaded(parentId, list);
+ mNotifyChildrenChangedOptions = null;
}
} else {
if (list == null) {
subscriptionCallback.onError(parentId, options);
} else {
+ mNotifyChildrenChangedOptions = notifyChildrenChangedOptions;
subscriptionCallback.onChildrenLoaded(parentId, list, options);
+ mNotifyChildrenChangedOptions = null;
}
}
}
}
+
+ @Override
+ public Bundle getNotifyChildrenChangedOptions() {
+ return mNotifyChildrenChangedOptions;
+ }
}
@RequiresApi(23)
@@ -2077,7 +2116,8 @@
serviceCallback.onLoadChildren(callbacksMessenger,
data.getString(DATA_MEDIA_ITEM_ID),
data.getParcelableArrayList(DATA_MEDIA_ITEM_LIST),
- data.getBundle(DATA_OPTIONS));
+ data.getBundle(DATA_OPTIONS),
+ data.getBundle(DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS));
break;
default:
Log.w(TAG, "Unhandled message: " + msg
@@ -2147,8 +2187,10 @@
sendRequest(CLIENT_MSG_GET_MEDIA_ITEM, data, callbacksMessenger);
}
- void registerCallbackMessenger(Messenger callbackMessenger) throws RemoteException {
+ void registerCallbackMessenger(Context context, Messenger callbackMessenger)
+ throws RemoteException {
Bundle data = new Bundle();
+ data.putString(DATA_PACKAGE_NAME, context.getPackageName());
data.putBundle(DATA_ROOT_HINTS, mRootHints);
sendRequest(CLIENT_MSG_REGISTER_CALLBACK_MESSENGER, data, callbackMessenger);
}
diff --git a/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java b/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java
index 1d78fc8..dd498be 100644
--- a/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java
+++ b/media/src/main/java/android/support/v4/media/session/MediaSessionCompat.java
@@ -18,6 +18,7 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static androidx.media.MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER;
import android.app.Activity;
import android.app.PendingIntent;
@@ -31,8 +32,10 @@
import android.media.MediaMetadataRetriever;
import android.media.Rating;
import android.media.RemoteControlClient;
+import android.media.session.MediaSession;
import android.net.Uri;
import android.os.BadParcelableException;
+import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
@@ -55,9 +58,12 @@
import android.view.ViewConfiguration;
import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.app.BundleCompat;
+import androidx.media.MediaSessionManager;
+import androidx.media.MediaSessionManager.RemoteUserInfo;
import androidx.media.VolumeProviderCompat;
import androidx.media.session.MediaButtonReceiver;
@@ -383,6 +389,11 @@
// Maximum size of the bitmap in dp.
private static final int MAX_BITMAP_SIZE_IN_DP = 320;
+ private static final String DATA_CALLING_PACKAGE = "data_calling_pkg";
+ private static final String DATA_CALLING_PID = "data_calling_pid";
+ private static final String DATA_CALLING_UID = "data_calling_uid";
+ private static final String DATA_EXTRAS = "data_extras";
+
// Maximum size of the bitmap in px. It shouldn't be changed.
static int sMaxBitmapSize;
@@ -449,7 +460,11 @@
mbrIntent = PendingIntent.getBroadcast(context,
0/* requestCode, ignored */, mediaButtonIntent, 0/* flags */);
}
- if (android.os.Build.VERSION.SDK_INT >= 21) {
+ if (android.os.Build.VERSION.SDK_INT >= 28) {
+ mImpl = new MediaSessionImplApi28(context, tag);
+ // Set default callback to respond to controllers' extra binder requests.
+ setCallback(new Callback() {});
+ } else if (android.os.Build.VERSION.SDK_INT >= 21) {
mImpl = new MediaSessionImplApi21(context, tag);
// Set default callback to respond to controllers' extra binder requests.
setCallback(new Callback() {});
@@ -802,6 +817,18 @@
}
/**
+ * Gets the controller information who sent the current request.
+ * <p>
+ * Note: This is only valid while in a request callback, such as {@link Callback#onPlay}.
+ *
+ * @throws IllegalStateException If this method is called outside of {@link Callback} methods.
+ * @see MediaSessionManager#isTrustedForMediaControl(RemoteUserInfo)
+ */
+ public final @NonNull RemoteUserInfo getCurrentControllerInfo() {
+ return mImpl.getCurrentControllerInfo();
+ }
+
+ /**
* Returns the name of the package that sent the last media button, transport control, or
* command from controllers and the system. This is only valid while in a request callback, such
* as {@link Callback#onPlay}. This method is not available and returns null on pre-N devices.
@@ -1822,6 +1849,7 @@
Object getRemoteControlClient();
String getCallingPackage();
+ RemoteUserInfo getCurrentControllerInfo();
}
static class MediaSessionImplBase implements MediaSessionImpl {
@@ -1918,30 +1946,19 @@
}
}
- void postToHandler(int what) {
- postToHandler(what, null);
- }
-
- void postToHandler(int what, int arg1) {
- postToHandler(what, null, arg1);
- }
-
- void postToHandler(int what, Object obj) {
- postToHandler(what, obj, null);
- }
-
- void postToHandler(int what, Object obj, int arg1) {
+ void postToHandler(int what, int arg1, int arg2, Object obj, Bundle extras) {
synchronized (mLock) {
if (mHandler != null) {
- mHandler.post(what, obj, arg1);
- }
- }
- }
-
- void postToHandler(int what, Object obj, Bundle extras) {
- synchronized (mLock) {
- if (mHandler != null) {
- mHandler.post(what, obj, extras);
+ Message msg = mHandler.obtainMessage(what, arg1, arg2, obj);
+ Bundle data = new Bundle();
+ data.putString(DATA_CALLING_PACKAGE, LEGACY_CONTROLLER);
+ data.putInt(DATA_CALLING_PID, Binder.getCallingPid());
+ data.putInt(DATA_CALLING_UID, Binder.getCallingUid());
+ if (extras != null) {
+ data.putBundle(DATA_EXTRAS, extras);
+ }
+ msg.setData(data);
+ msg.sendToTarget();
}
}
}
@@ -1959,6 +1976,7 @@
if (mVolumeProvider != null) {
mVolumeProvider.setCallback(null);
}
+ mLocalStream = stream;
mVolumeType = MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL;
ParcelableVolumeInfo info = new ParcelableVolumeInfo(mVolumeType, mLocalStream,
VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE,
@@ -2282,6 +2300,16 @@
sendExtras(extras);
}
+ @Override
+ public RemoteUserInfo getCurrentControllerInfo() {
+ synchronized (mLock) {
+ if (mHandler != null) {
+ return mHandler.getRemoteUserInfo();
+ }
+ }
+ return null;
+ }
+
// Registers/unregisters components as needed.
boolean update() {
boolean registeredRcc = false;
@@ -2792,6 +2820,26 @@
public boolean isTransportControlEnabled() {
return (mFlags & FLAG_HANDLES_TRANSPORT_CONTROLS) != 0;
}
+
+ void postToHandler(int what) {
+ MediaSessionImplBase.this.postToHandler(what, 0, 0, null, null);
+ }
+
+ void postToHandler(int what, int arg1) {
+ MediaSessionImplBase.this.postToHandler(what, arg1, 0, null, null);
+ }
+
+ void postToHandler(int what, Object obj) {
+ MediaSessionImplBase.this.postToHandler(what, 0, 0, obj, null);
+ }
+
+ void postToHandler(int what, Object obj, int arg1) {
+ MediaSessionImplBase.this.postToHandler(what, arg1, 0, obj, null);
+ }
+
+ void postToHandler(int what, Object obj, Bundle extras) {
+ MediaSessionImplBase.this.postToHandler(what, 0, 0, obj, extras);
+ }
}
private static final class Command {
@@ -2843,138 +2891,133 @@
private static final int KEYCODE_MEDIA_PAUSE = 127;
private static final int KEYCODE_MEDIA_PLAY = 126;
+ private RemoteUserInfo mRemoteUserInfo;
+
public MessageHandler(Looper looper) {
super(looper);
}
- public void post(int what, Object obj, Bundle bundle) {
- Message msg = obtainMessage(what, obj);
- msg.setData(bundle);
- msg.sendToTarget();
- }
-
- public void post(int what, Object obj) {
- obtainMessage(what, obj).sendToTarget();
- }
-
- public void post(int what) {
- post(what, null);
- }
-
- public void post(int what, Object obj, int arg1) {
- obtainMessage(what, arg1, 0, obj).sendToTarget();
- }
-
@Override
public void handleMessage(Message msg) {
MediaSessionCompat.Callback cb = mCallback;
if (cb == null) {
return;
}
- switch (msg.what) {
- case MSG_COMMAND:
- Command cmd = (Command) msg.obj;
- cb.onCommand(cmd.command, cmd.extras, cmd.stub);
- break;
- case MSG_MEDIA_BUTTON:
- KeyEvent keyEvent = (KeyEvent) msg.obj;
- Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
- intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
- // Let the Callback handle events first before using the default behavior
- if (!cb.onMediaButtonEvent(intent)) {
- onMediaButtonEvent(keyEvent, cb);
- }
- break;
- case MSG_PREPARE:
- cb.onPrepare();
- break;
- case MSG_PREPARE_MEDIA_ID:
- cb.onPrepareFromMediaId((String) msg.obj, msg.getData());
- break;
- case MSG_PREPARE_SEARCH:
- cb.onPrepareFromSearch((String) msg.obj, msg.getData());
- break;
- case MSG_PREPARE_URI:
- cb.onPrepareFromUri((Uri) msg.obj, msg.getData());
- break;
- case MSG_PLAY:
- cb.onPlay();
- break;
- case MSG_PLAY_MEDIA_ID:
- cb.onPlayFromMediaId((String) msg.obj, msg.getData());
- break;
- case MSG_PLAY_SEARCH:
- cb.onPlayFromSearch((String) msg.obj, msg.getData());
- break;
- case MSG_PLAY_URI:
- cb.onPlayFromUri((Uri) msg.obj, msg.getData());
- break;
- case MSG_SKIP_TO_ITEM:
- cb.onSkipToQueueItem((Long) msg.obj);
- break;
- case MSG_PAUSE:
- cb.onPause();
- break;
- case MSG_STOP:
- cb.onStop();
- break;
- case MSG_NEXT:
- cb.onSkipToNext();
- break;
- case MSG_PREVIOUS:
- cb.onSkipToPrevious();
- break;
- case MSG_FAST_FORWARD:
- cb.onFastForward();
- break;
- case MSG_REWIND:
- cb.onRewind();
- break;
- case MSG_SEEK_TO:
- cb.onSeekTo((Long) msg.obj);
- break;
- case MSG_RATE:
- cb.onSetRating((RatingCompat) msg.obj);
- break;
- case MSG_RATE_EXTRA:
- cb.onSetRating((RatingCompat) msg.obj, msg.getData());
- break;
- case MSG_CUSTOM_ACTION:
- cb.onCustomAction((String) msg.obj, msg.getData());
- break;
- case MSG_ADD_QUEUE_ITEM:
- cb.onAddQueueItem((MediaDescriptionCompat) msg.obj);
- break;
- case MSG_ADD_QUEUE_ITEM_AT:
- cb.onAddQueueItem((MediaDescriptionCompat) msg.obj, msg.arg1);
- break;
- case MSG_REMOVE_QUEUE_ITEM:
- cb.onRemoveQueueItem((MediaDescriptionCompat) msg.obj);
- break;
- case MSG_REMOVE_QUEUE_ITEM_AT:
- if (mQueue != null) {
- QueueItem item = (msg.arg1 >= 0 && msg.arg1 < mQueue.size())
- ? mQueue.get(msg.arg1) : null;
- if (item != null) {
- cb.onRemoveQueueItem(item.getDescription());
+
+ Bundle data = msg.getData();
+ mRemoteUserInfo = new RemoteUserInfo(data.getString(DATA_CALLING_PACKAGE),
+ data.getInt(DATA_CALLING_PID), data.getInt(DATA_CALLING_UID));
+ data = data.getBundle(DATA_EXTRAS);
+
+ try {
+ switch (msg.what) {
+ case MSG_COMMAND:
+ Command cmd = (Command) msg.obj;
+ cb.onCommand(cmd.command, cmd.extras, cmd.stub);
+ break;
+ case MSG_MEDIA_BUTTON:
+ KeyEvent keyEvent = (KeyEvent) msg.obj;
+ Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+ intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
+ // Let the Callback handle events first before using the default
+ // behavior
+ if (!cb.onMediaButtonEvent(intent)) {
+ onMediaButtonEvent(keyEvent, cb);
}
- }
- break;
- case MSG_ADJUST_VOLUME:
- adjustVolume(msg.arg1, 0);
- break;
- case MSG_SET_VOLUME:
- setVolumeTo(msg.arg1, 0);
- break;
- case MSG_SET_CAPTIONING_ENABLED:
- cb.onSetCaptioningEnabled((boolean) msg.obj);
- break;
- case MSG_SET_REPEAT_MODE:
- cb.onSetRepeatMode(msg.arg1);
- break;
- case MSG_SET_SHUFFLE_MODE:
- cb.onSetShuffleMode(msg.arg1);
- break;
+ break;
+ case MSG_PREPARE:
+ cb.onPrepare();
+ break;
+ case MSG_PREPARE_MEDIA_ID:
+ cb.onPrepareFromMediaId((String) msg.obj, data);
+ break;
+ case MSG_PREPARE_SEARCH:
+ cb.onPrepareFromSearch((String) msg.obj, data);
+ break;
+ case MSG_PREPARE_URI:
+ cb.onPrepareFromUri((Uri) msg.obj, data);
+ break;
+ case MSG_PLAY:
+ cb.onPlay();
+ break;
+ case MSG_PLAY_MEDIA_ID:
+ cb.onPlayFromMediaId((String) msg.obj, data);
+ break;
+ case MSG_PLAY_SEARCH:
+ cb.onPlayFromSearch((String) msg.obj, data);
+ break;
+ case MSG_PLAY_URI:
+ cb.onPlayFromUri((Uri) msg.obj, data);
+ break;
+ case MSG_SKIP_TO_ITEM:
+ cb.onSkipToQueueItem((Long) msg.obj);
+ break;
+ case MSG_PAUSE:
+ cb.onPause();
+ break;
+ case MSG_STOP:
+ cb.onStop();
+ break;
+ case MSG_NEXT:
+ cb.onSkipToNext();
+ break;
+ case MSG_PREVIOUS:
+ cb.onSkipToPrevious();
+ break;
+ case MSG_FAST_FORWARD:
+ cb.onFastForward();
+ break;
+ case MSG_REWIND:
+ cb.onRewind();
+ break;
+ case MSG_SEEK_TO:
+ cb.onSeekTo((Long) msg.obj);
+ break;
+ case MSG_RATE:
+ cb.onSetRating((RatingCompat) msg.obj);
+ break;
+ case MSG_RATE_EXTRA:
+ cb.onSetRating((RatingCompat) msg.obj, data);
+ break;
+ case MSG_CUSTOM_ACTION:
+ cb.onCustomAction((String) msg.obj, data);
+ break;
+ case MSG_ADD_QUEUE_ITEM:
+ cb.onAddQueueItem((MediaDescriptionCompat) msg.obj);
+ break;
+ case MSG_ADD_QUEUE_ITEM_AT:
+ cb.onAddQueueItem((MediaDescriptionCompat) msg.obj, msg.arg1);
+ break;
+ case MSG_REMOVE_QUEUE_ITEM:
+ cb.onRemoveQueueItem((MediaDescriptionCompat) msg.obj);
+ break;
+ case MSG_REMOVE_QUEUE_ITEM_AT:
+ if (mQueue != null) {
+ QueueItem item = (msg.arg1 >= 0 && msg.arg1 < mQueue.size())
+ ? mQueue.get(msg.arg1) : null;
+ if (item != null) {
+ cb.onRemoveQueueItem(item.getDescription());
+ }
+ }
+ break;
+ case MSG_ADJUST_VOLUME:
+ adjustVolume(msg.arg1, 0);
+ break;
+ case MSG_SET_VOLUME:
+ setVolumeTo(msg.arg1, 0);
+ break;
+ case MSG_SET_CAPTIONING_ENABLED:
+ cb.onSetCaptioningEnabled((boolean) msg.obj);
+ break;
+ case MSG_SET_REPEAT_MODE:
+ cb.onSetRepeatMode(msg.arg1);
+ break;
+ case MSG_SET_SHUFFLE_MODE:
+ cb.onSetShuffleMode(msg.arg1);
+ break;
+ }
+ } finally {
+ mRemoteUserInfo = null;
}
}
@@ -3028,6 +3071,10 @@
break;
}
}
+
+ RemoteUserInfo getRemoteUserInfo() {
+ return mRemoteUserInfo;
+ }
}
}
@@ -3050,7 +3097,8 @@
new RemoteControlClient.OnPlaybackPositionUpdateListener() {
@Override
public void onPlaybackPositionUpdate(long newPositionMs) {
- postToHandler(MessageHandler.MSG_SEEK_TO, newPositionMs);
+ postToHandler(
+ MessageHandler.MSG_SEEK_TO, -1, -1, newPositionMs, null);
}
};
mRcc.setPlaybackPositionUpdateListener(listener);
@@ -3135,8 +3183,8 @@
public void onMetadataUpdate(int key, Object newValue) {
if (key == MediaMetadataEditor.RATING_KEY_BY_USER
&& newValue instanceof Rating) {
- postToHandler(MessageHandler.MSG_RATE,
- RatingCompat.fromRating(newValue));
+ postToHandler(MessageHandler.MSG_RATE, -1, -1,
+ RatingCompat.fromRating(newValue), null);
}
}
};
@@ -3410,6 +3458,11 @@
}
}
+ @Override
+ public RemoteUserInfo getCurrentControllerInfo() {
+ return null;
+ }
+
class ExtraSession extends IMediaSession.Stub {
@Override
public void sendCommand(String command, Bundle args, ResultReceiverWrapper cb) {
@@ -3703,4 +3756,25 @@
}
}
}
+
+ @RequiresApi(28)
+ static class MediaSessionImplApi28 extends MediaSessionImplApi21 {
+ private MediaSession mSession;
+
+ MediaSessionImplApi28(Context context, String tag) {
+ super(context, tag);
+ }
+
+ MediaSessionImplApi28(Object mediaSession) {
+ super(mediaSession);
+ mSession = (MediaSession) mediaSession;
+ }
+
+ @Override
+ public final @NonNull RemoteUserInfo getCurrentControllerInfo() {
+ android.media.session.MediaSessionManager.RemoteUserInfo info =
+ mSession.getCurrentControllerInfo();
+ return new RemoteUserInfo(info.getPackageName(), info.getPid(), info.getUid());
+ }
+ }
}
diff --git a/media/src/main/java/androidx/media/AudioAttributesCompat.java b/media/src/main/java/androidx/media/AudioAttributesCompat.java
index 1fbb7be..69ef117 100644
--- a/media/src/main/java/androidx/media/AudioAttributesCompat.java
+++ b/media/src/main/java/androidx/media/AudioAttributesCompat.java
@@ -21,6 +21,7 @@
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.os.Build;
+import android.os.Bundle;
import android.util.SparseIntArray;
import androidx.annotation.IntDef;
@@ -228,6 +229,16 @@
private static final int FLAG_ALL_PUBLIC =
(FLAG_AUDIBILITY_ENFORCED | FLAG_HW_AV_SYNC | FLAG_LOW_LATENCY);
+ /** Keys to convert to (or create from) Bundle. */
+ private static final String AUDIO_ATTRIBUTES_FRAMEWORKS =
+ "androidx.media.audio_attrs.FRAMEWORKS";
+ private static final String AUDIO_ATTRIBUTES_USAGE = "androidx.media.audio_attrs.USAGE";
+ private static final String AUDIO_ATTRIBUTES_CONTENT_TYPE =
+ "androidx.media.audio_attrs.CONTENT_TYPE";
+ private static final String AUDIO_ATTRIBUTES_FLAGS = "androidx.media.audio_attrs.FLAGS";
+ private static final String AUDIO_ATTRIBUTES_LEGACY_STREAM_TYPE =
+ "androidx.media.audio_attrs.LEGACY_STREAM_TYPE";
+
int mUsage = USAGE_UNKNOWN;
int mContentType = CONTENT_TYPE_UNKNOWN;
int mFlags = 0x0;
@@ -377,6 +388,56 @@
}
/**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public @NonNull Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ if (Build.VERSION.SDK_INT >= 21) {
+ bundle.putParcelable(AUDIO_ATTRIBUTES_FRAMEWORKS, mAudioAttributesWrapper.unwrap());
+ } else {
+ bundle.putInt(AUDIO_ATTRIBUTES_USAGE, mUsage);
+ bundle.putInt(AUDIO_ATTRIBUTES_CONTENT_TYPE, mContentType);
+ bundle.putInt(AUDIO_ATTRIBUTES_FLAGS, mFlags);
+ if (mLegacyStream != null) {
+ bundle.putInt(AUDIO_ATTRIBUTES_LEGACY_STREAM_TYPE, mLegacyStream);
+ }
+ }
+ return bundle;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static AudioAttributesCompat fromBundle(Bundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+
+ if (Build.VERSION.SDK_INT >= 21) {
+ AudioAttributes frameworkAttrs = (AudioAttributes)
+ bundle.getParcelable(AUDIO_ATTRIBUTES_FRAMEWORKS);
+ return frameworkAttrs == null ? null : AudioAttributesCompat.wrap(frameworkAttrs);
+ } else {
+ int usage = bundle.getInt(AUDIO_ATTRIBUTES_USAGE, USAGE_UNKNOWN);
+ int contentType = bundle.getInt(AUDIO_ATTRIBUTES_CONTENT_TYPE, CONTENT_TYPE_UNKNOWN);
+ int flags = bundle.getInt(AUDIO_ATTRIBUTES_FLAGS, 0);
+
+ // Here, we do not use builder in order to 'copy' the exact state of the original one.
+ // Builder class guesses the usage based on other value (contentType/legacyStream), and
+ // overwrites it. So using builder cannot ensure the equality.
+ AudioAttributesCompat attr = new AudioAttributesCompat();
+ attr.mUsage = usage;
+ attr.mContentType = contentType;
+ attr.mFlags = flags;
+ attr.mLegacyStream = bundle.containsKey(AUDIO_ATTRIBUTES_LEGACY_STREAM_TYPE)
+ ? bundle.getInt(AUDIO_ATTRIBUTES_LEGACY_STREAM_TYPE) : null;
+ return attr;
+ }
+ }
+
+ /**
* Builder class for {@link AudioAttributesCompat} objects.
*
* <p>example:
diff --git a/media/src/main/java/androidx/media/Media2DataSource.java b/media/src/main/java/androidx/media/Media2DataSource.java
index 4990259..3855d56 100644
--- a/media/src/main/java/androidx/media/Media2DataSource.java
+++ b/media/src/main/java/androidx/media/Media2DataSource.java
@@ -17,15 +17,10 @@
package androidx.media;
-import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import androidx.annotation.RestrictTo;
-
import java.io.Closeable;
import java.io.IOException;
/**
- * @hide
* For supplying media data to the framework. Implement this if your app has
* special requirements for the way media data is obtained.
*
@@ -36,7 +31,6 @@
* Media2DataSource from another thread while it's being used by the framework.</p>
*
*/
-@RestrictTo(LIBRARY_GROUP)
public abstract class Media2DataSource implements Closeable {
/**
* Called to request data from the given position.
diff --git a/media/src/main/java/androidx/media/MediaBrowser2.java b/media/src/main/java/androidx/media/MediaBrowser2.java
index 6ef7fcf..beadf1f 100644
--- a/media/src/main/java/androidx/media/MediaBrowser2.java
+++ b/media/src/main/java/androidx/media/MediaBrowser2.java
@@ -17,13 +17,18 @@
package androidx.media;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static androidx.media.MediaConstants2.ARGUMENT_EXTRAS;
+import static androidx.media.MediaConstants2.ARGUMENT_PAGE;
+import static androidx.media.MediaConstants2.ARGUMENT_PAGE_SIZE;
import android.content.Context;
+import android.os.BadParcelableException;
import android.os.Bundle;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaBrowserCompat.ItemCallback;
import android.support.v4.media.MediaBrowserCompat.MediaItem;
import android.support.v4.media.MediaBrowserCompat.SubscriptionCallback;
+import android.util.Log;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
@@ -38,11 +43,12 @@
import java.util.concurrent.Executor;
/**
- * @hide
* Browses media content offered by a {@link MediaLibraryService2}.
*/
-@RestrictTo(LIBRARY_GROUP)
public class MediaBrowser2 extends MediaController2 {
+ static final String TAG = "MediaBrowser2";
+ static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
/**
* @hide
*/
@@ -50,11 +56,10 @@
public static final String EXTRA_ITEM_COUNT = "android.media.browse.extra.ITEM_COUNT";
/**
- * Key for Bundle version of {@link MediaSession2.ControllerInfo}.
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
- public static final String EXTRA_TARGET = "android.media.browse.extra.TARGET";
+ public static final String MEDIA_BROWSER2_SUBSCRIBE = "androidx.media.MEDIA_BROWSER2_SUBSCRIBE";
private final Object mLock = new Object();
@GuardedBy("mLock")
@@ -171,7 +176,7 @@
browser.disconnect();
}
mBrowserCompats.clear();
- // TODO: Ensure that ControllerCallback#onDisconnected() is called by super.close().
+ // Ensure that ControllerCallback#onDisconnected() is called by super.close().
super.close();
}
}
@@ -195,13 +200,20 @@
}
});
} else {
- MediaBrowserCompat newBrowser = new MediaBrowserCompat(getContext(),
- getSessionToken().getComponentName(), new GetLibraryRootCallback(extras),
- extras);
- newBrowser.connect();
- synchronized (mLock) {
- mBrowserCompats.put(extras, newBrowser);
- }
+ getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ // Do this on the callback executor to set the looper of MediaBrowserCompat's
+ // callback handler to this looper.
+ MediaBrowserCompat newBrowser = new MediaBrowserCompat(getContext(),
+ getSessionToken().getComponentName(),
+ new GetLibraryRootCallback(extras), extras);
+ synchronized (mLock) {
+ mBrowserCompats.put(extras, newBrowser);
+ }
+ newBrowser.connect();
+ }
+ });
}
}
@@ -218,15 +230,9 @@
if (parentId == null) {
throw new IllegalArgumentException("parentId shouldn't be null");
}
- // TODO: Document this behavior
- Bundle option;
- if (extras != null && (extras.containsKey(MediaBrowserCompat.EXTRA_PAGE)
- || extras.containsKey(MediaBrowserCompat.EXTRA_PAGE_SIZE))) {
- option = new Bundle(extras);
- option.remove(MediaBrowserCompat.EXTRA_PAGE);
- option.remove(MediaBrowserCompat.EXTRA_PAGE_SIZE);
- } else {
- option = extras;
+ MediaBrowserCompat browser = getBrowserCompat();
+ if (browser == null) {
+ return;
}
SubscribeCallback callback = new SubscribeCallback();
synchronized (mLock) {
@@ -237,11 +243,11 @@
}
list.add(callback);
}
- // TODO: Revisit using default browser is OK. Here's my concern.
- // Assume that MediaBrowser2 is connected with the MediaBrowserServiceCompat.
- // Since MediaBrowserServiceCompat can call MediaBrowserServiceCompat#
- // getBrowserRootHints(), the service may refuse calls from MediaBrowser2
- getBrowserCompat().subscribe(parentId, option, callback);
+
+ Bundle options = new Bundle();
+ options.putBundle(ARGUMENT_EXTRAS, extras);
+ options.putBoolean(MEDIA_BROWSER2_SUBSCRIBE, true);
+ browser.subscribe(parentId, options, callback);
}
/**
@@ -257,6 +263,10 @@
if (parentId == null) {
throw new IllegalArgumentException("parentId shouldn't be null");
}
+ MediaBrowserCompat browser = getBrowserCompat();
+ if (browser == null) {
+ return;
+ }
// Note: don't use MediaBrowserCompat#unsubscribe(String) here, to keep the subscription
// callback for getChildren.
synchronized (mLock) {
@@ -264,7 +274,6 @@
if (list == null) {
return;
}
- MediaBrowserCompat browser = getBrowserCompat();
for (int i = 0; i < list.size(); i++) {
browser.unsubscribe(parentId, list.get(i));
}
@@ -288,12 +297,15 @@
if (page < 1 || pageSize < 1) {
throw new IllegalArgumentException("Neither page nor pageSize should be less than 1");
}
- Bundle options = new Bundle(extras);
+ MediaBrowserCompat browser = getBrowserCompat();
+ if (browser == null) {
+ return;
+ }
+
+ Bundle options = MediaUtils2.createBundle(extras);
options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
- // TODO: Revisit using default browser is OK. See TODO in subscribe
- getBrowserCompat().subscribe(parentId, options,
- new GetChildrenCallback(parentId, page, pageSize));
+ browser.subscribe(parentId, options, new GetChildrenCallback(parentId, page, pageSize));
}
/**
@@ -303,8 +315,11 @@
* @param mediaId media id for specifying the item
*/
public void getItem(@NonNull final String mediaId) {
- // TODO: Revisit using default browser is OK. See TODO in subscribe
- getBrowserCompat().getItem(mediaId, new ItemCallback() {
+ MediaBrowserCompat browser = getBrowserCompat();
+ if (browser == null) {
+ return;
+ }
+ browser.getItem(mediaId, new ItemCallback() {
@Override
public void onItemLoaded(final MediaItem item) {
getCallbackExecutor().execute(new Runnable() {
@@ -338,7 +353,28 @@
* @param extras extra bundle
*/
public void search(@NonNull String query, @Nullable Bundle extras) {
- // TODO: Implement
+ MediaBrowserCompat browser = getBrowserCompat();
+ if (browser == null) {
+ return;
+ }
+ browser.search(query, extras, new MediaBrowserCompat.SearchCallback() {
+ @Override
+ public void onSearchResult(final String query, final Bundle extras,
+ final List<MediaItem> items) {
+ getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ getCallback().onSearchResultChanged(
+ MediaBrowser2.this, query, items.size(), extras);
+ }
+ });
+ }
+
+ @Override
+ public void onError(final String query, final Bundle extras) {
+ // Currently no way to tell failures in MediaBrowser2#search().
+ }
+ });
}
/**
@@ -351,9 +387,40 @@
* @param pageSize page size. Should be greater or equal to {@code 1}
* @param extras extra bundle
*/
- public void getSearchResult(@NonNull String query, int page, int pageSize,
- @Nullable Bundle extras) {
- // TODO: Implement
+ public void getSearchResult(final @NonNull String query, final int page, final int pageSize,
+ final @Nullable Bundle extras) {
+ MediaBrowserCompat browser = getBrowserCompat();
+ if (browser == null) {
+ return;
+ }
+ Bundle options = MediaUtils2.createBundle(extras);
+ options.putInt(ARGUMENT_PAGE, page);
+ options.putInt(ARGUMENT_PAGE_SIZE, pageSize);
+ browser.search(query, options, new MediaBrowserCompat.SearchCallback() {
+ @Override
+ public void onSearchResult(final String query, final Bundle extrasSent,
+ final List<MediaItem> items) {
+ getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ List<MediaItem2> item2List = MediaUtils2.toMediaItem2List(items);
+ getCallback().onGetSearchResultDone(
+ MediaBrowser2.this, query, page, pageSize, item2List, extras);
+ }
+ });
+ }
+
+ @Override
+ public void onError(final String query, final Bundle extrasSent) {
+ getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ getCallback().onGetSearchResultDone(
+ MediaBrowser2.this, query, page, pageSize, null, extras);
+ }
+ });
+ }
+ });
}
@Override
@@ -367,6 +434,20 @@
}
}
+ private Bundle getExtrasWithoutPagination(Bundle extras) {
+ if (extras == null) {
+ return null;
+ }
+ extras.setClassLoader(getContext().getClassLoader());
+ try {
+ extras.remove(MediaBrowserCompat.EXTRA_PAGE);
+ extras.remove(MediaBrowserCompat.EXTRA_PAGE_SIZE);
+ } catch (BadParcelableException e) {
+ // Pass through...
+ }
+ return extras;
+ }
+
private class GetLibraryRootCallback extends MediaBrowserCompat.ConnectionCallback {
private final Bundle mExtras;
@@ -433,11 +514,14 @@
// Currently no way to tell failures in MediaBrowser2#subscribe().
return;
}
+
+ final Bundle notifyChildrenChangedOptions =
+ getBrowserCompat().getNotifyChildrenChangedOptions();
getCallbackExecutor().execute(new Runnable() {
@Override
public void run() {
getCallback().onChildrenChanged(MediaBrowser2.this, parentId, itemCount,
- options);
+ notifyChildrenChangedOptions);
}
});
}
@@ -472,7 +556,7 @@
@Override
public void onChildrenLoaded(final String parentId, List<MediaItem> children,
- final Bundle options) {
+ Bundle options) {
final List<MediaItem2> items;
if (children == null) {
items = null;
@@ -482,12 +566,17 @@
items.add(MediaUtils2.createMediaItem2(children.get(i)));
}
}
+ final Bundle extras = getExtrasWithoutPagination(options);
getCallbackExecutor().execute(new Runnable() {
@Override
public void run() {
+ MediaBrowserCompat browser = getBrowserCompat();
+ if (browser == null) {
+ return;
+ }
getCallback().onGetChildrenDone(MediaBrowser2.this, parentId, mPage, mPageSize,
- items, options);
- getBrowserCompat().unsubscribe(mParentId, GetChildrenCallback.this);
+ items, extras);
+ browser.unsubscribe(mParentId, GetChildrenCallback.this);
}
});
}
diff --git a/media/src/main/java/androidx/media/MediaBrowserCompatUtils.java b/media/src/main/java/androidx/media/MediaBrowserCompatUtils.java
index c553256..b9491b6 100644
--- a/media/src/main/java/androidx/media/MediaBrowserCompatUtils.java
+++ b/media/src/main/java/androidx/media/MediaBrowserCompatUtils.java
@@ -70,12 +70,8 @@
endIndex2 = startIndex2 + pageSize2 - 1;
}
- if (startIndex1 <= startIndex2 && startIndex2 <= endIndex1) {
- return true;
- } else if (startIndex1 <= endIndex2 && endIndex2 <= endIndex1) {
- return true;
- }
- return false;
+ // For better readability, leaving the exclamation mark here.
+ return !(endIndex1 < startIndex2 || endIndex2 < startIndex1);
}
private MediaBrowserCompatUtils() {
diff --git a/media/src/main/java/androidx/media/MediaBrowserProtocol.java b/media/src/main/java/androidx/media/MediaBrowserProtocol.java
index 5c85880..eb5b449 100644
--- a/media/src/main/java/androidx/media/MediaBrowserProtocol.java
+++ b/media/src/main/java/androidx/media/MediaBrowserProtocol.java
@@ -15,6 +15,7 @@
*/
package androidx.media;
+import android.os.Bundle;
import android.support.v4.media.MediaBrowserCompat;
import androidx.annotation.RestrictTo;
@@ -29,10 +30,13 @@
public static final String DATA_CALLBACK_TOKEN = "data_callback_token";
public static final String DATA_CALLING_UID = "data_calling_uid";
+ public static final String DATA_CALLING_PID = "data_calling_pid";
public static final String DATA_MEDIA_ITEM_ID = "data_media_item_id";
public static final String DATA_MEDIA_ITEM_LIST = "data_media_item_list";
public static final String DATA_MEDIA_SESSION_TOKEN = "data_media_session_token";
public static final String DATA_OPTIONS = "data_options";
+ public static final String DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS =
+ "data_notify_children_changed_options";
public static final String DATA_PACKAGE_NAME = "data_package_name";
public static final String DATA_RESULT_RECEIVER = "data_result_receiver";
public static final String DATA_ROOT_HINTS = "data_root_hints";
@@ -94,6 +98,9 @@
* DATA_MEDIA_ITEM_LIST : An array list for the media item children
* DATA_OPTIONS : A bundle of service-specific arguments sent from the media browse to
* the media browser service
+ * DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS : A bundle of service-specific arguments sent from
+ * the media browser service to the media browser by calling
+ * {@link MediaBrowserServiceCompat#notifyChildrenChanged(String, Bundle)}
*/
public static final int SERVICE_MSG_ON_LOAD_CHILDREN = 3;
diff --git a/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java b/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java
index 8f24837..5da2287 100644
--- a/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java
+++ b/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java
@@ -27,12 +27,14 @@
import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_SEND_CUSTOM_ACTION;
import static androidx.media.MediaBrowserProtocol.CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER;
import static androidx.media.MediaBrowserProtocol.DATA_CALLBACK_TOKEN;
+import static androidx.media.MediaBrowserProtocol.DATA_CALLING_PID;
import static androidx.media.MediaBrowserProtocol.DATA_CALLING_UID;
import static androidx.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION;
import static androidx.media.MediaBrowserProtocol.DATA_CUSTOM_ACTION_EXTRAS;
import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_ID;
import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_ITEM_LIST;
import static androidx.media.MediaBrowserProtocol.DATA_MEDIA_SESSION_TOKEN;
+import static androidx.media.MediaBrowserProtocol.DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS;
import static androidx.media.MediaBrowserProtocol.DATA_OPTIONS;
import static androidx.media.MediaBrowserProtocol.DATA_PACKAGE_NAME;
import static androidx.media.MediaBrowserProtocol.DATA_RESULT_RECEIVER;
@@ -61,6 +63,7 @@
import android.os.Messenger;
import android.os.Parcel;
import android.os.RemoteException;
+import android.service.media.MediaBrowserService;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.session.IMediaSession;
import android.support.v4.media.session.MediaSessionCompat;
@@ -76,6 +79,7 @@
import androidx.collection.ArrayMap;
import androidx.core.app.BundleCompat;
import androidx.core.util.Pair;
+import androidx.media.MediaSessionManager.RemoteUserInfo;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -167,9 +171,10 @@
/** @hide */
@RestrictTo(LIBRARY)
@Retention(RetentionPolicy.SOURCE)
- @IntDef(flag=true, value = { RESULT_FLAG_OPTION_NOT_HANDLED,
- RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED, RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED })
- private @interface ResultFlags { }
+ @IntDef(flag = true, value = {RESULT_FLAG_OPTION_NOT_HANDLED,
+ RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED, RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED})
+ private @interface ResultFlags {
+ }
final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap<>();
ConnectionRecord mCurConnection;
@@ -182,6 +187,8 @@
void setSessionToken(MediaSessionCompat.Token token);
void notifyChildrenChanged(final String parentId, final Bundle options);
Bundle getBrowserRootHints();
+ RemoteUserInfo getCurrentBrowserInfo();
+ List<RemoteUserInfo> getSubscribingBrowsers(String parentId);
}
class MediaBrowserServiceImplBase implements MediaBrowserServiceImpl {
@@ -206,7 +213,7 @@
@Override
public void run() {
Iterator<ConnectionRecord> iter = mConnections.values().iterator();
- while (iter.hasNext()){
+ while (iter.hasNext()) {
ConnectionRecord connection = iter.next();
try {
connection.callbacks.onConnect(connection.root.getRootId(), token,
@@ -233,7 +240,8 @@
for (Pair<IBinder, Bundle> callback : callbackList) {
if (MediaBrowserCompatUtils.hasDuplicatedItems(
options, callback.second)) {
- performLoadChildren(parentId, connection, callback.second);
+ performLoadChildren(parentId, connection, callback.second,
+ options);
}
}
}
@@ -246,10 +254,33 @@
public Bundle getBrowserRootHints() {
if (mCurConnection == null) {
throw new IllegalStateException("This should be called inside of onLoadChildren,"
- + " onLoadItem or onSearch methods");
+ + " onLoadItem, onSearch, or onCustomAction methods");
}
return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints);
}
+
+ @Override
+ public RemoteUserInfo getCurrentBrowserInfo() {
+ if (mCurConnection == null) {
+ throw new IllegalStateException("This should be called inside of onLoadChildren,"
+ + " onLoadItem, onSearch, or onCustomAction methods");
+ }
+ return mCurConnection.browserInfo;
+ }
+
+ @Override
+ public List<RemoteUserInfo> getSubscribingBrowsers(String parentId) {
+ List<RemoteUserInfo> result = new ArrayList<>();
+ for (IBinder binder : mConnections.keySet()) {
+ ConnectionRecord connection = mConnections.get(binder);
+ List<Pair<IBinder, Bundle>> callbackList =
+ connection.subscriptions.get(parentId);
+ if (callbackList != null) {
+ result.add(connection.browserInfo);
+ }
+ }
+ return result;
+ }
}
@RequiresApi(21)
@@ -298,19 +329,6 @@
}
@Override
- public Bundle getBrowserRootHints() {
- if (mMessenger == null) {
- // TODO: Handle getBrowserRootHints when connected with framework MediaBrowser.
- return null;
- }
- if (mCurConnection == null) {
- throw new IllegalStateException("This should be called inside of onLoadChildren,"
- + " onLoadItem or onSearch methods");
- }
- return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints);
- }
-
- @Override
public MediaBrowserServiceCompatApi21.BrowserRoot onGetRoot(
String clientPackageName, int clientUid, Bundle rootHints) {
Bundle rootExtras = null;
@@ -328,8 +346,13 @@
mRootExtrasList.add(rootExtras);
}
}
+ // We aren't sure whether this connection request would be accepted.
+ // Temporarily set mCurConnection just to make getCurrentBrowserInfo() working.
+ mCurConnection = new ConnectionRecord(clientPackageName, -1, clientUid, rootHints,
+ null);
BrowserRoot root = MediaBrowserServiceCompat.this.onGetRoot(
clientPackageName, clientUid, rootHints);
+ mCurConnection = null;
if (root == null) {
return null;
}
@@ -369,6 +392,20 @@
MediaBrowserServiceCompat.this.onLoadChildren(parentId, result);
}
+ @Override
+ public List<RemoteUserInfo> getSubscribingBrowsers(String parentId) {
+ List<RemoteUserInfo> result = new ArrayList<>();
+ for (IBinder binder : mConnections.keySet()) {
+ ConnectionRecord connection = mConnections.get(binder);
+ List<Pair<IBinder, Bundle>> callbackList =
+ connection.subscriptions.get(parentId);
+ if (callbackList != null) {
+ result.add(connection.browserInfo);
+ }
+ }
+ return result;
+ }
+
void notifyChildrenChangedForFramework(final String parentId, final Bundle options) {
MediaBrowserServiceCompatApi21.notifyChildrenChanged(mServiceObj, parentId);
}
@@ -385,7 +422,8 @@
for (Pair<IBinder, Bundle> callback : callbackList) {
if (MediaBrowserCompatUtils.hasDuplicatedItems(
options, callback.second)) {
- performLoadChildren(parentId, connection, callback.second);
+ performLoadChildren(parentId, connection, callback.second,
+ options);
}
}
}
@@ -393,6 +431,28 @@
}
});
}
+
+ @Override
+ public Bundle getBrowserRootHints() {
+ if (mMessenger == null) {
+ // TODO: Handle getBrowserRootHints when connected with framework MediaBrowser.
+ return null;
+ }
+ if (mCurConnection == null) {
+ throw new IllegalStateException("This should be called inside of onGetRoot,"
+ + " onLoadChildren, onLoadItem, onSearch, or onCustomAction methods");
+ }
+ return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints);
+ }
+
+ @Override
+ public RemoteUserInfo getCurrentBrowserInfo() {
+ if (mCurConnection == null) {
+ throw new IllegalStateException("This should be called inside of onGetRoot,"
+ + " onLoadChildren, onLoadItem, onSearch, or onCustomAction methods");
+ }
+ return mCurConnection.browserInfo;
+ }
}
@RequiresApi(23)
@@ -469,7 +529,7 @@
@Override
public Bundle getBrowserRootHints() {
- // If EXTRA_MESSENGER_BINDER is used, mCurConnection is not null.
+ // mCurConnection is not null when EXTRA_MESSENGER_BINDER is used.
if (mCurConnection != null) {
return mCurConnection.rootHints == null ? null
: new Bundle(mCurConnection.rootHints);
@@ -488,6 +548,21 @@
}
}
+ @RequiresApi(28)
+ class MediaBrowserServiceImplApi28 extends MediaBrowserServiceImplApi26 {
+ @Override
+ public RemoteUserInfo getCurrentBrowserInfo() {
+ // mCurConnection is not null when EXTRA_MESSENGER_BINDER is used.
+ if (mCurConnection != null) {
+ return mCurConnection.browserInfo;
+ }
+ android.media.session.MediaSessionManager.RemoteUserInfo userInfoObj =
+ ((MediaBrowserService) mServiceObj).getCurrentBrowserInfo();
+ return new RemoteUserInfo(
+ userInfoObj.getPackageName(), userInfoObj.getPid(), userInfoObj.getUid());
+ }
+ }
+
private final class ServiceHandler extends Handler {
private final ServiceBinderImpl mServiceBinderImpl = new ServiceBinderImpl();
@@ -500,7 +575,8 @@
switch (msg.what) {
case CLIENT_MSG_CONNECT:
mServiceBinderImpl.connect(data.getString(DATA_PACKAGE_NAME),
- data.getInt(DATA_CALLING_UID), data.getBundle(DATA_ROOT_HINTS),
+ data.getInt(DATA_CALLING_PID), data.getInt(DATA_CALLING_UID),
+ data.getBundle(DATA_ROOT_HINTS),
new ServiceCallbacksCompat(msg.replyTo));
break;
case CLIENT_MSG_DISCONNECT:
@@ -524,7 +600,8 @@
break;
case CLIENT_MSG_REGISTER_CALLBACK_MESSENGER:
mServiceBinderImpl.registerCallbacks(new ServiceCallbacksCompat(msg.replyTo),
- data.getBundle(DATA_ROOT_HINTS));
+ data.getString(DATA_PACKAGE_NAME), data.getInt(DATA_CALLING_PID),
+ data.getInt(DATA_CALLING_UID), data.getBundle(DATA_ROOT_HINTS));
break;
case CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER:
mServiceBinderImpl.unregisterCallbacks(new ServiceCallbacksCompat(msg.replyTo));
@@ -555,6 +632,7 @@
Bundle data = msg.getData();
data.setClassLoader(MediaBrowserCompat.class.getClassLoader());
data.putInt(DATA_CALLING_UID, Binder.getCallingUid());
+ data.putInt(DATA_CALLING_PID, Binder.getCallingPid());
return super.sendMessageAtTime(msg, uptimeMillis);
}
@@ -571,13 +649,23 @@
* All the info about a connection.
*/
private class ConnectionRecord implements IBinder.DeathRecipient {
- String pkg;
- Bundle rootHints;
- ServiceCallbacks callbacks;
- BrowserRoot root;
- HashMap<String, List<Pair<IBinder, Bundle>>> subscriptions = new HashMap<>();
+ public final String pkg;
+ public final int pid;
+ public final int uid;
+ public final RemoteUserInfo browserInfo;
+ public final Bundle rootHints;
+ public final ServiceCallbacks callbacks;
+ public final HashMap<String, List<Pair<IBinder, Bundle>>> subscriptions = new HashMap<>();
+ public BrowserRoot root;
- ConnectionRecord() {
+ ConnectionRecord(String pkg, int pid, int uid, Bundle rootHints,
+ ServiceCallbacks callback) {
+ this.pkg = pkg;
+ this.pid = pid;
+ this.uid = uid;
+ this.browserInfo = new RemoteUserInfo(pkg, pid, uid);
+ this.rootHints = rootHints;
+ this.callbacks = callback;
}
@Override
@@ -740,7 +828,7 @@
ServiceBinderImpl() {
}
- public void connect(final String pkg, final int uid, final Bundle rootHints,
+ public void connect(final String pkg, final int pid, final int uid, final Bundle rootHints,
final ServiceCallbacks callbacks) {
if (!isValidPackage(pkg, uid)) {
@@ -756,13 +844,11 @@
// Clear out the old subscriptions. We are getting new ones.
mConnections.remove(b);
- final ConnectionRecord connection = new ConnectionRecord();
- connection.pkg = pkg;
- connection.rootHints = rootHints;
- connection.callbacks = callbacks;
-
- connection.root =
- MediaBrowserServiceCompat.this.onGetRoot(pkg, uid, rootHints);
+ final ConnectionRecord connection = new ConnectionRecord(pkg, pid, uid,
+ rootHints, callbacks);
+ mCurConnection = connection;
+ connection.root = MediaBrowserServiceCompat.this.onGetRoot(pkg, uid, rootHints);
+ mCurConnection = null;
// If they didn't return something, don't allow this client.
if (connection.root == null) {
@@ -872,7 +958,8 @@
}
// Used when {@link MediaBrowserProtocol#EXTRA_MESSENGER_BINDER} is used.
- public void registerCallbacks(final ServiceCallbacks callbacks, final Bundle rootHints) {
+ public void registerCallbacks(final ServiceCallbacks callbacks, final String pkg,
+ final int pid, final int uid, final Bundle rootHints) {
mHandler.postOrRun(new Runnable() {
@Override
public void run() {
@@ -880,9 +967,8 @@
// Clear out the old subscriptions. We are getting new ones.
mConnections.remove(b);
- final ConnectionRecord connection = new ConnectionRecord();
- connection.callbacks = callbacks;
- connection.rootHints = rootHints;
+ final ConnectionRecord connection = new ConnectionRecord(pkg, pid, uid,
+ rootHints, callbacks);
mConnections.put(b, connection);
try {
b.linkToDeath(connection, 0);
@@ -956,8 +1042,8 @@
void onConnect(String root, MediaSessionCompat.Token session, Bundle extras)
throws RemoteException;
void onConnectFailed() throws RemoteException;
- void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, Bundle options)
- throws RemoteException;
+ void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list, Bundle options,
+ Bundle notifyChildrenChangedOptions) throws RemoteException;
}
private static class ServiceCallbacksCompat implements ServiceCallbacks {
@@ -993,10 +1079,11 @@
@Override
public void onLoadChildren(String mediaId, List<MediaBrowserCompat.MediaItem> list,
- Bundle options) throws RemoteException {
+ Bundle options, Bundle notifyChildrenChangedOptions) throws RemoteException {
Bundle data = new Bundle();
data.putString(DATA_MEDIA_ITEM_ID, mediaId);
data.putBundle(DATA_OPTIONS, options);
+ data.putBundle(DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS, notifyChildrenChangedOptions);
if (list != null) {
data.putParcelableArrayList(DATA_MEDIA_ITEM_LIST,
list instanceof ArrayList ? (ArrayList) list : new ArrayList<>(list));
@@ -1031,7 +1118,9 @@
@Override
public void onCreate() {
super.onCreate();
- if (Build.VERSION.SDK_INT >= 26) {
+ if (Build.VERSION.SDK_INT >= 28) {
+ mImpl = new MediaBrowserServiceImplApi28();
+ } else if (Build.VERSION.SDK_INT >= 26) {
mImpl = new MediaBrowserServiceImplApi26();
} else if (Build.VERSION.SDK_INT >= 23) {
mImpl = new MediaBrowserServiceImplApi23();
@@ -1253,6 +1342,17 @@
}
/**
+ * Gets the browser information who sent the current request.
+ *
+ * @throws IllegalStateException If this method is called outside of {@link #onGetRoot} or
+ * {@link #onLoadChildren} or {@link #onLoadItem}.
+ * @see MediaSessionManager#isTrustedForMediaControl(RemoteUserInfo)
+ */
+ public final @NonNull RemoteUserInfo getCurrentBrowserInfo() {
+ return mImpl.getCurrentBrowserInfo();
+ }
+
+ /**
* Notifies all connected media browsers that the children of
* the specified parent id have changed in some way.
* This will cause browsers to fetch subscribed content again.
@@ -1289,6 +1389,18 @@
}
/**
+ * Gets {@link RemoteUserInfo} of all browsers which are subscribing to the given parentId.
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public @NonNull List<RemoteUserInfo> getSubscribingBrowsers(@NonNull String parentId) {
+ if (parentId == null) {
+ throw new IllegalArgumentException("parentId cannot be null in getSubscribingBrowsers");
+ }
+ return mImpl.getSubscribingBrowsers(parentId);
+ }
+
+ /**
* Return whether the given package is one of the ones that is owned by the uid.
*/
boolean isValidPackage(String pkg, int uid) {
@@ -1325,7 +1437,7 @@
callbackList.add(new Pair<>(token, options));
connection.subscriptions.put(id, callbackList);
// send the results
- performLoadChildren(id, connection, options);
+ performLoadChildren(id, connection, options, null);
}
/**
@@ -1358,7 +1470,7 @@
* Callers must make sure that this connection is still connected.
*/
void performLoadChildren(final String parentId, final ConnectionRecord connection,
- final Bundle options) {
+ final Bundle subscribeOptions, final Bundle notifyChildrenChangedOptions) {
final Result<List<MediaBrowserCompat.MediaItem>> result
= new Result<List<MediaBrowserCompat.MediaItem>>(parentId) {
@Override
@@ -1373,9 +1485,10 @@
List<MediaBrowserCompat.MediaItem> filteredList =
(getFlags() & RESULT_FLAG_OPTION_NOT_HANDLED) != 0
- ? applyOptions(list, options) : list;
+ ? applyOptions(list, subscribeOptions) : list;
try {
- connection.callbacks.onLoadChildren(parentId, filteredList, options);
+ connection.callbacks.onLoadChildren(parentId, filteredList, subscribeOptions,
+ notifyChildrenChangedOptions);
} catch (RemoteException ex) {
// The other side is in the process of crashing.
Log.w(TAG, "Calling onLoadChildren() failed for id=" + parentId
@@ -1385,10 +1498,10 @@
};
mCurConnection = connection;
- if (options == null) {
+ if (subscribeOptions == null) {
onLoadChildren(parentId, result);
} else {
- onLoadChildren(parentId, result, options);
+ onLoadChildren(parentId, result, subscribeOptions);
}
mCurConnection = null;
diff --git a/media/src/main/java/androidx/media/MediaConstants2.java b/media/src/main/java/androidx/media/MediaConstants2.java
index 68a9a19..1fec6db 100644
--- a/media/src/main/java/androidx/media/MediaConstants2.java
+++ b/media/src/main/java/androidx/media/MediaConstants2.java
@@ -33,8 +33,10 @@
"androidx.media.session.event.ON_PLAYBACK_INFO_CHANGED";
static final String SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED =
"androidx.media.session.event.ON_PLAYBACK_SPEED_CHANGED";
- static final String SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED =
+ static final String SESSION_EVENT_ON_BUFFERING_STATE_CHANGED =
"androidx.media.session.event.ON_BUFFERING_STATE_CHANGED";
+ static final String SESSION_EVENT_ON_SEEK_COMPLETED =
+ "androidx.media.session.event.ON_SEEK_COMPLETED";
static final String SESSION_EVENT_ON_REPEAT_MODE_CHANGED =
"androidx.media.session.event.ON_REPEAT_MODE_CHANGED";
static final String SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED =
@@ -45,6 +47,10 @@
"androidx.media.session.event.ON_PLAYLIST_METADATA_CHANGED";
static final String SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED =
"androidx.media.session.event.ON_ALLOWED_COMMANDS_CHANGED";
+ static final String SESSION_EVENT_ON_CHILDREN_CHANGED =
+ "androidx.media.session.event.ON_CHILDREN_CHANGED";
+ static final String SESSION_EVENT_ON_SEARCH_RESULT_CHANGED =
+ "androidx.media.session.event.ON_SEARCH_RESULT_CHANGED";
static final String SESSION_EVENT_SEND_CUSTOM_COMMAND =
"androidx.media.session.event.SEND_CUSTOM_COMMAND";
static final String SESSION_EVENT_SET_CUSTOM_LAYOUT =
@@ -89,6 +95,9 @@
static final String ARGUMENT_COMMAND_BUTTONS = "androidx.media.argument.COMMAND_BUTTONS";
static final String ARGUMENT_ROUTE_BUNDLE = "androidx.media.argument.ROUTE_BUNDLE";
static final String ARGUMENT_PLAYBACK_INFO = "androidx.media.argument.PLAYBACK_INFO";
+ static final String ARGUMENT_ITEM_COUNT = "androidx.media.argument.ITEM_COUNT";
+ static final String ARGUMENT_PAGE = "androidx.media.argument.PAGE";
+ static final String ARGUMENT_PAGE_SIZE = "androidx.media.argument.PAGE_SIZE";
static final String ARGUMENT_ICONTROLLER_CALLBACK =
"androidx.media.argument.ICONTROLLER_CALLBACK";
diff --git a/media/src/main/java/androidx/media/MediaController2.java b/media/src/main/java/androidx/media/MediaController2.java
index 1da552d..5d81cc2 100644
--- a/media/src/main/java/androidx/media/MediaController2.java
+++ b/media/src/main/java/androidx/media/MediaController2.java
@@ -16,93 +16,7 @@
package androidx.media;
-import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DURATION;
-
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-import static androidx.media.MediaConstants2.ARGUMENT_ALLOWED_COMMANDS;
-import static androidx.media.MediaConstants2.ARGUMENT_ARGUMENTS;
-import static androidx.media.MediaConstants2.ARGUMENT_BUFFERING_STATE;
-import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_BUTTONS;
-import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_CODE;
-import static androidx.media.MediaConstants2.ARGUMENT_CUSTOM_COMMAND;
-import static androidx.media.MediaConstants2.ARGUMENT_ERROR_CODE;
-import static androidx.media.MediaConstants2.ARGUMENT_EXTRAS;
-import static androidx.media.MediaConstants2.ARGUMENT_ICONTROLLER_CALLBACK;
-import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ID;
-import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ITEM;
-import static androidx.media.MediaConstants2.ARGUMENT_PACKAGE_NAME;
-import static androidx.media.MediaConstants2.ARGUMENT_PID;
-import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_INFO;
-import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_SPEED;
-import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_STATE_COMPAT;
-import static androidx.media.MediaConstants2.ARGUMENT_PLAYER_STATE;
-import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST;
-import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_INDEX;
-import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_METADATA;
-import static androidx.media.MediaConstants2.ARGUMENT_QUERY;
-import static androidx.media.MediaConstants2.ARGUMENT_RATING;
-import static androidx.media.MediaConstants2.ARGUMENT_REPEAT_MODE;
-import static androidx.media.MediaConstants2.ARGUMENT_RESULT_RECEIVER;
-import static androidx.media.MediaConstants2.ARGUMENT_ROUTE_BUNDLE;
-import static androidx.media.MediaConstants2.ARGUMENT_SEEK_POSITION;
-import static androidx.media.MediaConstants2.ARGUMENT_SHUFFLE_MODE;
-import static androidx.media.MediaConstants2.ARGUMENT_UID;
-import static androidx.media.MediaConstants2.ARGUMENT_URI;
-import static androidx.media.MediaConstants2.ARGUMENT_VOLUME;
-import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_DIRECTION;
-import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_FLAGS;
-import static androidx.media.MediaConstants2.CONNECT_RESULT_CONNECTED;
-import static androidx.media.MediaConstants2.CONNECT_RESULT_DISCONNECTED;
-import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_COMMAND_CODE;
-import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_CUSTOM_COMMAND;
-import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_CONNECT;
-import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_DISCONNECT;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ERROR;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYER_STATE_CHANGED;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_CHANGED;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_REPEAT_MODE_CHANGED;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ROUTES_INFO_CHANGED;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED;
-import static androidx.media.MediaConstants2.SESSION_EVENT_SEND_CUSTOM_COMMAND;
-import static androidx.media.MediaConstants2.SESSION_EVENT_SET_CUSTOM_LAYOUT;
-import static androidx.media.MediaPlayerBase.BUFFERING_STATE_UNKNOWN;
-import static androidx.media.MediaPlayerBase.UNKNOWN_TIME;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_RESET;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SEEK_TO;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SET_SPEED;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_ADD_ITEM;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REMOVE_ITEM;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST_METADATA;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM;
-import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM;
-import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_FAST_FORWARD;
-import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID;
-import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH;
-import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI;
-import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID;
-import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH;
-import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI;
-import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_REWIND;
-import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SELECT_ROUTE;
-import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SET_RATING;
-import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO;
-import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO;
-import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_ADJUST_VOLUME;
-import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_SET_VOLUME;
import android.annotation.TargetApi;
import android.app.PendingIntent;
@@ -111,25 +25,14 @@
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IBinder;
-import android.os.Process;
-import android.os.RemoteException;
import android.os.ResultReceiver;
-import android.os.SystemClock;
import android.support.v4.media.MediaBrowserCompat;
-import android.support.v4.media.MediaMetadataCompat;
-import android.support.v4.media.session.MediaControllerCompat;
-import android.support.v4.media.session.MediaSessionCompat;
-import android.support.v4.media.session.PlaybackStateCompat;
-import android.util.Log;
-import androidx.annotation.GuardedBy;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
import androidx.media.MediaPlaylistAgent.RepeatMode;
import androidx.media.MediaPlaylistAgent.ShuffleMode;
import androidx.media.MediaSession2.CommandButton;
@@ -142,8 +45,9 @@
import java.util.concurrent.Executor;
/**
- * Allows an app to interact with an active {@link MediaSession2} in any status. Media buttons and
- * other commands can be sent to the session.
+ * Allows an app to interact with an active {@link MediaSession2} or a
+ * {@link MediaSessionService2} in any status. Media buttons and other commands can be sent to
+ * the session.
* <p>
* When you're done, use {@link #close()} to clean up resources. This also helps session service
* to be destroyed when there's no controller associated with it.
@@ -151,9 +55,16 @@
* When controlling {@link MediaSession2}, the controller will be available immediately after
* the creation.
* <p>
+ * When controlling {@link MediaSessionService2}, the {@link MediaController2} would be
+ * available only if the session service allows this controller by
+ * {@link MediaSession2.SessionCallback#onConnect(MediaSession2, ControllerInfo)} for the service.
+ * Wait {@link ControllerCallback#onConnected(MediaController2, SessionCommandGroup2)} or
+ * {@link ControllerCallback#onDisconnected(MediaController2)} for the result.
+ * <p>
* MediaController2 objects are thread-safe.
* <p>
* @see MediaSession2
+ * @see MediaSessionService2
*/
@TargetApi(Build.VERSION_CODES.KITKAT)
public class MediaController2 implements AutoCloseable {
@@ -176,6 +87,657 @@
@Retention(RetentionPolicy.SOURCE)
public @interface VolumeFlags {}
+ private final SupportLibraryImpl mImpl;
+ // For testing.
+ Long mTimeDiff;
+
+ /**
+ * Create a {@link MediaController2} from the {@link SessionToken2}.
+ * This connects to the session and may wake up the service if it's not available.
+ *
+ * @param context Context
+ * @param token token to connect to
+ * @param executor executor to run callbacks on.
+ * @param callback controller callback to receive changes in
+ */
+ public MediaController2(@NonNull Context context, @NonNull SessionToken2 token,
+ @NonNull Executor executor, @NonNull ControllerCallback callback) {
+ mImpl = new MediaController2ImplBase(context, token, executor, callback);
+ mImpl.setInstance(this);
+ }
+
+ /**
+ * Release this object, and disconnect from the session. After this, callbacks wouldn't be
+ * received.
+ */
+ @Override
+ public void close() {
+ try {
+ mImpl.close();
+ } catch (Exception e) {
+ // Should not be here.
+ }
+ }
+
+ /**
+ * @return token
+ */
+ public @NonNull SessionToken2 getSessionToken() {
+ return mImpl.getSessionToken();
+ }
+
+ /**
+ * Returns whether this class is connected to active {@link MediaSession2} or not.
+ */
+ public boolean isConnected() {
+ return mImpl.isConnected();
+ }
+
+ /**
+ * Requests that the player starts or resumes playback.
+ */
+ public void play() {
+ mImpl.play();
+ }
+
+ /**
+ * Requests that the player pauses playback.
+ */
+ public void pause() {
+ mImpl.pause();
+ }
+
+ /**
+ * Requests that the player be reset to its uninitialized state.
+ */
+ public void reset() {
+ mImpl.reset();
+ }
+
+ /**
+ * Request that the player prepare its playback. In other words, other sessions can continue
+ * to play during the preparation of this session. This method can be used to speed up the
+ * start of the playback. Once the preparation is done, the session will change its playback
+ * state to {@link MediaPlayerInterface#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be
+ * called to start playback.
+ */
+ public void prepare() {
+ mImpl.prepare();
+ }
+
+ /**
+ * Start fast forwarding. If playback is already fast forwarding this
+ * may increase the rate.
+ */
+ public void fastForward() {
+ mImpl.fastForward();
+ }
+
+ /**
+ * Start rewinding. If playback is already rewinding this may increase
+ * the rate.
+ */
+ public void rewind() {
+ mImpl.rewind();
+ }
+
+ /**
+ * Move to a new location in the media stream.
+ *
+ * @param pos Position to move to, in milliseconds.
+ */
+ public void seekTo(long pos) {
+ mImpl.seekTo(pos);
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void skipForward() {
+ // To match with KEYCODE_MEDIA_SKIP_FORWARD
+ mImpl.skipForward();
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void skipBackward() {
+ // To match with KEYCODE_MEDIA_SKIP_BACKWARD
+ mImpl.skipBackward();
+ }
+
+ /**
+ * Request that the player start playback for a specific media id.
+ *
+ * @param mediaId The id of the requested media.
+ * @param extras Optional extras that can include extra information about the media item
+ * to be played.
+ */
+ public void playFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
+ mImpl.playFromMediaId(mediaId, extras);
+ }
+
+ /**
+ * Request that the player start playback for a specific search query.
+ *
+ * @param query The search query. Should not be an empty string.
+ * @param extras Optional extras that can include extra information about the query.
+ */
+ public void playFromSearch(@NonNull String query, @Nullable Bundle extras) {
+ mImpl.playFromSearch(query, extras);
+ }
+
+ /**
+ * Request that the player start playback for a specific {@link Uri}.
+ *
+ * @param uri The URI of the requested media.
+ * @param extras Optional extras that can include extra information about the media item
+ * to be played.
+ */
+ public void playFromUri(@NonNull Uri uri, @Nullable Bundle extras) {
+ mImpl.playFromUri(uri, extras);
+ }
+
+ /**
+ * Request that the player prepare playback for a specific media id. In other words, other
+ * sessions can continue to play during the preparation of this session. This method can be
+ * used to speed up the start of the playback. Once the preparation is done, the session
+ * will change its playback state to {@link MediaPlayerInterface#PLAYER_STATE_PAUSED}.
+ * Afterwards, {@link #play} can be called to start playback. If the preparation is not needed,
+ * {@link #playFromMediaId} can be directly called without this method.
+ *
+ * @param mediaId The id of the requested media.
+ * @param extras Optional extras that can include extra information about the media item
+ * to be prepared.
+ */
+ public void prepareFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
+ mImpl.prepareFromMediaId(mediaId, extras);
+ }
+
+ /**
+ * Request that the player prepare playback for a specific search query.
+ * In other words, other sessions can continue to play during the preparation of this session.
+ * This method can be used to speed up the start of the playback.
+ * Once the preparation is done, the session will change its playback state to
+ * {@link MediaPlayerInterface#PLAYER_STATE_PAUSED}. Afterwards,
+ * {@link #play} can be called to start playback. If the preparation is not needed,
+ * {@link #playFromSearch} can be directly called without this method.
+ *
+ * @param query The search query. Should not be an empty string.
+ * @param extras Optional extras that can include extra information about the query.
+ */
+ public void prepareFromSearch(@NonNull String query, @Nullable Bundle extras) {
+ mImpl.prepareFromSearch(query, extras);
+ }
+
+ /**
+ * Request that the player prepare playback for a specific {@link Uri}. In other words,
+ * other sessions can continue to play during the preparation of this session. This method
+ * can be used to speed up the start of the playback. Once the preparation is done, the
+ * session will change its playback state to {@link MediaPlayerInterface#PLAYER_STATE_PAUSED}.
+ * Afterwards, {@link #play} can be called to start playback. If the preparation is not needed,
+ * {@link #playFromUri} can be directly called without this method.
+ *
+ * @param uri The URI of the requested media.
+ * @param extras Optional extras that can include extra information about the media item
+ * to be prepared.
+ */
+ public void prepareFromUri(@NonNull Uri uri, @Nullable Bundle extras) {
+ mImpl.prepareFromUri(uri, extras);
+ }
+
+ /**
+ * Set the volume of the output this session is playing on. The command will be ignored if it
+ * does not support {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}.
+ * <p>
+ * If the session is local playback, this changes the device's volume with the stream that
+ * session's player is using. Flags will be specified for the {@link AudioManager}.
+ * <p>
+ * If the session is remote player (i.e. session has set volume provider), its volume provider
+ * will receive this request instead.
+ *
+ * @see #getPlaybackInfo()
+ * @param value The value to set it to, between 0 and the reported max.
+ * @param flags flags from {@link AudioManager} to include with the volume request for local
+ * playback
+ */
+ public void setVolumeTo(int value, @VolumeFlags int flags) {
+ mImpl.setVolumeTo(value, flags);
+ }
+
+ /**
+ * Adjust the volume of the output this session is playing on. The direction
+ * must be one of {@link AudioManager#ADJUST_LOWER},
+ * {@link AudioManager#ADJUST_RAISE}, or {@link AudioManager#ADJUST_SAME}.
+ * <p>
+ * The command will be ignored if the session does not support
+ * {@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE} or
+ * {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}.
+ * <p>
+ * If the session is local playback, this changes the device's volume with the stream that
+ * session's player is using. Flags will be specified for the {@link AudioManager}.
+ * <p>
+ * If the session is remote player (i.e. session has set volume provider), its volume provider
+ * will receive this request instead.
+ *
+ * @see #getPlaybackInfo()
+ * @param direction The direction to adjust the volume in.
+ * @param flags flags from {@link AudioManager} to include with the volume request for local
+ * playback
+ */
+ public void adjustVolume(@VolumeDirection int direction, @VolumeFlags int flags) {
+ mImpl.adjustVolume(direction, flags);
+ }
+
+ /**
+ * Get an intent for launching UI associated with this session if one exists.
+ *
+ * @return A {@link PendingIntent} to launch UI or null.
+ */
+ public @Nullable PendingIntent getSessionActivity() {
+ return mImpl.getSessionActivity();
+ }
+
+ /**
+ * Get the lastly cached player state from
+ * {@link ControllerCallback#onPlayerStateChanged(MediaController2, int)}.
+ *
+ * @return player state
+ */
+ public int getPlayerState() {
+ return mImpl.getPlayerState();
+ }
+
+ /**
+ * Gets the duration of the current media item, or {@link MediaPlayerInterface#UNKNOWN_TIME} if
+ * unknown.
+ * @return the duration in ms, or {@link MediaPlayerInterface#UNKNOWN_TIME}.
+ */
+ public long getDuration() {
+ return mImpl.getDuration();
+ }
+
+ /**
+ * Gets the current playback position.
+ * <p>
+ * This returns the calculated value of the position, based on the difference between the
+ * update time and current time.
+ *
+ * @return position
+ */
+ public long getCurrentPosition() {
+ return mImpl.getCurrentPosition();
+ }
+
+ /**
+ * Get the lastly cached playback speed from
+ * {@link ControllerCallback#onPlaybackSpeedChanged(MediaController2, float)}.
+ *
+ * @return speed the lastly cached playback speed, or 0.0f if unknown.
+ */
+ public float getPlaybackSpeed() {
+ return mImpl.getPlaybackSpeed();
+ }
+
+ /**
+ * Set the playback speed.
+ */
+ public void setPlaybackSpeed(float speed) {
+ mImpl.setPlaybackSpeed(speed);
+ }
+
+ /**
+ * Gets the current buffering state of the player.
+ * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+ * buffered.
+ * @return the buffering state.
+ */
+ public @MediaPlayerInterface.BuffState int getBufferingState() {
+ return mImpl.getBufferingState();
+ }
+
+ /**
+ * Gets the lastly cached buffered position from the session when
+ * {@link ControllerCallback#onBufferingStateChanged(MediaController2, MediaItem2, int)} is
+ * called.
+ *
+ * @return buffering position in millis, or {@link MediaPlayerInterface#UNKNOWN_TIME} if
+ * unknown.
+ */
+ public long getBufferedPosition() {
+ return mImpl.getBufferedPosition();
+ }
+
+ /**
+ * Get the current playback info for this session.
+ *
+ * @return The current playback info or null.
+ */
+ public @Nullable PlaybackInfo getPlaybackInfo() {
+ return mImpl.getPlaybackInfo();
+ }
+
+ /**
+ * Rate the media. This will cause the rating to be set for the current user.
+ * The rating style must follow the user rating style from the session.
+ * You can get the rating style from the session through the
+ * {@link MediaMetadata2#getRating(String)} with the key
+ * {@link MediaMetadata2#METADATA_KEY_USER_RATING}.
+ * <p>
+ * If the user rating was {@code null}, the media item does not accept setting user rating.
+ *
+ * @param mediaId The id of the media
+ * @param rating The rating to set
+ */
+ public void setRating(@NonNull String mediaId, @NonNull Rating2 rating) {
+ mImpl.setRating(mediaId, rating);
+ }
+
+ /**
+ * Send custom command to the session
+ *
+ * @param command custom command
+ * @param args optional argument
+ * @param cb optional result receiver
+ */
+ public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args,
+ @Nullable ResultReceiver cb) {
+ mImpl.sendCustomCommand(command, args, cb);
+ }
+
+ /**
+ * Returns the cached playlist from {@link ControllerCallback#onPlaylistChanged}.
+ * <p>
+ * This list may differ with the list that was specified with
+ * {@link #setPlaylist(List, MediaMetadata2)} depending on the {@link MediaPlaylistAgent}
+ * implementation. Use media items returned here for other playlist agent APIs such as
+ * {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}.
+ *
+ * @return playlist. Can be {@code null} if the playlist hasn't set nor controller doesn't have
+ * enough permission.
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST
+ */
+ public @Nullable List<MediaItem2> getPlaylist() {
+ return mImpl.getPlaylist();
+ }
+
+ /**
+ * Sets the playlist.
+ * <p>
+ * Even when the playlist is successfully set, use the playlist returned from
+ * {@link #getPlaylist()} for playlist APIs such as {@link #skipToPlaylistItem(MediaItem2)}.
+ * Otherwise the session in the remote process can't distinguish between media items.
+ *
+ * @param list playlist
+ * @param metadata metadata of the playlist
+ * @see #getPlaylist()
+ * @see ControllerCallback#onPlaylistChanged
+ */
+ public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
+ mImpl.setPlaylist(list, metadata);
+ }
+
+ /**
+ * Updates the playlist metadata
+ *
+ * @param metadata metadata of the playlist
+ */
+ public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
+ mImpl.updatePlaylistMetadata(metadata);
+ }
+
+ /**
+ * Gets the lastly cached playlist playlist metadata either from
+ * {@link ControllerCallback#onPlaylistMetadataChanged or
+ * {@link ControllerCallback#onPlaylistChanged}.
+ *
+ * @return metadata metadata of the playlist, or null if none is set
+ */
+ public @Nullable MediaMetadata2 getPlaylistMetadata() {
+ return mImpl.getPlaylistMetadata();
+ }
+
+ /**
+ * Adds the media item to the playlist at position index. Index equals or greater than
+ * the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of
+ * the playlist.
+ * <p>
+ * This will not change the currently playing media item.
+ * If index is less than or equal to the current index of the playlist,
+ * the current index of the playlist will be incremented correspondingly.
+ *
+ * @param index the index you want to add
+ * @param item the media item you want to add
+ */
+ public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
+ mImpl.addPlaylistItem(index, item);
+ }
+
+ /**
+ * Removes the media item at index in the playlist.
+ *<p>
+ * If the item is the currently playing item of the playlist, current playback
+ * will be stopped and playback moves to next source in the list.
+ *
+ * @param item the media item you want to add
+ */
+ public void removePlaylistItem(@NonNull MediaItem2 item) {
+ mImpl.removePlaylistItem(item);
+ }
+
+ /**
+ * Replace the media item at index in the playlist. This can be also used to update metadata of
+ * an item.
+ *
+ * @param index the index of the item to replace
+ * @param item the new item
+ */
+ public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
+ mImpl.replacePlaylistItem(index, item);
+ }
+
+ /**
+ * Get the lastly cached current item from
+ * {@link ControllerCallback#onCurrentMediaItemChanged(MediaController2, MediaItem2)}.
+ *
+ * @return the currently playing item, or null if unknown.
+ */
+ public MediaItem2 getCurrentMediaItem() {
+ return mImpl.getCurrentMediaItem();
+ }
+
+ /**
+ * Skips to the previous item in the playlist.
+ * <p>
+ * This calls {@link MediaPlaylistAgent#skipToPreviousItem()}.
+ */
+ public void skipToPreviousItem() {
+ mImpl.skipToPreviousItem();
+ }
+
+ /**
+ * Skips to the next item in the playlist.
+ * <p>
+ * This calls {@link MediaPlaylistAgent#skipToNextItem()}.
+ */
+ public void skipToNextItem() {
+ mImpl.skipToNextItem();
+ }
+
+ /**
+ * Skips to the item in the playlist.
+ * <p>
+ * This calls {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}.
+ *
+ * @param item The item in the playlist you want to play
+ */
+ public void skipToPlaylistItem(@NonNull MediaItem2 item) {
+ mImpl.skipToPlaylistItem(item);
+ }
+
+ /**
+ * Gets the cached repeat mode from the {@link ControllerCallback#onRepeatModeChanged}.
+ *
+ * @return repeat mode
+ * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+ * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+ */
+ public @RepeatMode int getRepeatMode() {
+ return mImpl.getRepeatMode();
+ }
+
+ /**
+ * Sets the repeat mode.
+ *
+ * @param repeatMode repeat mode
+ * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+ * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+ */
+ public void setRepeatMode(@RepeatMode int repeatMode) {
+ mImpl.setRepeatMode(repeatMode);
+ }
+
+ /**
+ * Gets the cached shuffle mode from the {@link ControllerCallback#onShuffleModeChanged}.
+ *
+ * @return The shuffle mode
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+ */
+ public @ShuffleMode int getShuffleMode() {
+ return mImpl.getShuffleMode();
+ }
+
+ /**
+ * Sets the shuffle mode.
+ *
+ * @param shuffleMode The shuffle mode
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+ */
+ public void setShuffleMode(@ShuffleMode int shuffleMode) {
+ mImpl.setShuffleMode(shuffleMode);
+ }
+
+ /**
+ * Queries for information about the routes currently known.
+ */
+ public void subscribeRoutesInfo() {
+ mImpl.subscribeRoutesInfo();
+ }
+
+ /**
+ * Unsubscribes for changes to the routes.
+ * <p>
+ * The {@link ControllerCallback#onRoutesInfoChanged callback} will no longer be invoked for
+ * the routes once this method returns.
+ * </p>
+ */
+ public void unsubscribeRoutesInfo() {
+ mImpl.unsubscribeRoutesInfo();
+ }
+
+ /**
+ * Selects the specified route.
+ *
+ * @param route The route to select.
+ */
+ public void selectRoute(@NonNull Bundle route) {
+ mImpl.selectRoute(route);
+ }
+
+ @NonNull Context getContext() {
+ return mImpl.getContext();
+ }
+
+ @NonNull ControllerCallback getCallback() {
+ return mImpl.getCallback();
+ }
+
+ @NonNull Executor getCallbackExecutor() {
+ return mImpl.getCallbackExecutor();
+ }
+
+ @Nullable MediaBrowserCompat getBrowserCompat() {
+ return mImpl.getBrowserCompat();
+ }
+
+ /**
+ * Sets the time diff forcefully when calculating current position.
+ * @param timeDiff {@code null} for reset.
+ */
+ @VisibleForTesting
+ void setTimeDiff(Long timeDiff) {
+ mTimeDiff = timeDiff;
+ }
+
+ interface SupportLibraryImpl extends AutoCloseable {
+ void setInstance(MediaController2 controller);
+ SessionToken2 getSessionToken();
+ boolean isConnected();
+ void play();
+ void pause();
+ void reset();
+ void prepare();
+ void fastForward();
+ void rewind();
+ void seekTo(long pos);
+ void skipForward();
+ void skipBackward();
+ void playFromMediaId(@NonNull String mediaId, @Nullable Bundle extras);
+ void playFromSearch(@NonNull String query, @Nullable Bundle extras);
+ void playFromUri(@NonNull Uri uri, @Nullable Bundle extras);
+ void prepareFromMediaId(@NonNull String mediaId, @Nullable Bundle extras);
+ void prepareFromSearch(@NonNull String query, @Nullable Bundle extras);
+ void prepareFromUri(@NonNull Uri uri, @Nullable Bundle extras);
+ void setVolumeTo(int value, @VolumeFlags int flags);
+ void adjustVolume(@VolumeDirection int direction, @VolumeFlags int flags);
+ @Nullable PendingIntent getSessionActivity();
+ int getPlayerState();
+ long getDuration();
+ long getCurrentPosition();
+ float getPlaybackSpeed();
+ void setPlaybackSpeed(float speed);
+ @MediaPlayerInterface.BuffState int getBufferingState();
+ long getBufferedPosition();
+ @Nullable PlaybackInfo getPlaybackInfo();
+ void setRating(@NonNull String mediaId, @NonNull Rating2 rating);
+ void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args,
+ @Nullable ResultReceiver cb);
+ @Nullable List<MediaItem2> getPlaylist();
+ void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata);
+ void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata);
+ @Nullable MediaMetadata2 getPlaylistMetadata();
+ void addPlaylistItem(int index, @NonNull MediaItem2 item);
+ void removePlaylistItem(@NonNull MediaItem2 item);
+ void replacePlaylistItem(int index, @NonNull MediaItem2 item);
+ MediaItem2 getCurrentMediaItem();
+ void skipToPreviousItem();
+ void skipToNextItem();
+ void skipToPlaylistItem(@NonNull MediaItem2 item);
+ @RepeatMode int getRepeatMode();
+ void setRepeatMode(@RepeatMode int repeatMode);
+ @ShuffleMode int getShuffleMode();
+ void setShuffleMode(@ShuffleMode int shuffleMode);
+ void subscribeRoutesInfo();
+ void unsubscribeRoutesInfo();
+ void selectRoute(@NonNull Bundle route);
+
+ // For MediaBrowser2
+ @NonNull Context getContext();
+ @NonNull ControllerCallback getCallback();
+ @NonNull Executor getCallbackExecutor();
+ @Nullable MediaBrowserCompat getBrowserCompat();
+ }
+
/**
* Interface for listening to change in activeness of the {@link MediaSession2}. It's
* active if and only if it has set a player.
@@ -273,7 +835,7 @@
* @param state the new buffering state.
*/
public void onBufferingStateChanged(@NonNull MediaController2 controller,
- @NonNull MediaItem2 item, @MediaPlayerBase.BuffState int state) { }
+ @NonNull MediaItem2 item, @MediaPlayerInterface.BuffState int state) { }
/**
* Called to indicate that seeking is completed.
@@ -304,7 +866,7 @@
* @see #onBufferingStateChanged(MediaController2, MediaItem2, int)
*/
public void onCurrentMediaItemChanged(@NonNull MediaController2 controller,
- @NonNull MediaItem2 item) { }
+ @Nullable MediaItem2 item) { }
/**
* Called when a playlist is changed.
@@ -461,8 +1023,7 @@
bundle.putInt(KEY_MAX_VOLUME, mMaxVolume);
bundle.putInt(KEY_CURRENT_VOLUME, mCurrentVolume);
if (mAudioAttrsCompat != null) {
- bundle.putParcelable(KEY_AUDIO_ATTRIBUTES,
- MediaUtils2.toAudioAttributesBundle(mAudioAttrsCompat));
+ bundle.putBundle(KEY_AUDIO_ATTRIBUTES, mAudioAttrsCompat.toBundle());
}
return bundle;
}
@@ -480,1291 +1041,10 @@
final int volumeControl = bundle.getInt(KEY_CONTROL_TYPE);
final int maxVolume = bundle.getInt(KEY_MAX_VOLUME);
final int currentVolume = bundle.getInt(KEY_CURRENT_VOLUME);
- final AudioAttributesCompat attrs = MediaUtils2.fromAudioAttributesBundle(
+ final AudioAttributesCompat attrs = AudioAttributesCompat.fromBundle(
bundle.getBundle(KEY_AUDIO_ATTRIBUTES));
return createPlaybackInfo(volumeType, attrs, volumeControl, maxVolume,
currentVolume);
}
}
-
- private final class ControllerCompatCallback extends MediaControllerCompat.Callback {
- @Override
- public void onSessionReady() {
- sendCommand(CONTROLLER_COMMAND_CONNECT, new ResultReceiver(mHandler) {
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- if (!mHandlerThread.isAlive()) {
- return;
- }
- switch (resultCode) {
- case CONNECT_RESULT_CONNECTED:
- onConnectedNotLocked(resultData);
- break;
- case CONNECT_RESULT_DISCONNECTED:
- mCallback.onDisconnected(MediaController2.this);
- close();
- break;
- }
- }
- });
- }
-
- @Override
- public void onSessionDestroyed() {
- close();
- }
-
- @Override
- public void onPlaybackStateChanged(PlaybackStateCompat state) {
- synchronized (mLock) {
- mPlaybackStateCompat = state;
- }
- }
-
- @Override
- public void onMetadataChanged(MediaMetadataCompat metadata) {
- synchronized (mLock) {
- mMediaMetadataCompat = metadata;
- }
- }
-
- @Override
- public void onSessionEvent(String event, Bundle extras) {
- switch (event) {
- case SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED: {
- SessionCommandGroup2 allowedCommands = SessionCommandGroup2.fromBundle(
- extras.getBundle(ARGUMENT_ALLOWED_COMMANDS));
- synchronized (mLock) {
- mAllowedCommands = allowedCommands;
- }
- mCallback.onAllowedCommandsChanged(MediaController2.this, allowedCommands);
- break;
- }
- case SESSION_EVENT_ON_PLAYER_STATE_CHANGED: {
- int playerState = extras.getInt(ARGUMENT_PLAYER_STATE);
- synchronized (mLock) {
- mPlayerState = playerState;
- }
- mCallback.onPlayerStateChanged(MediaController2.this, playerState);
- break;
- }
- case SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED: {
- MediaItem2 item = MediaItem2.fromBundle(extras.getBundle(ARGUMENT_MEDIA_ITEM));
- if (item == null) {
- return;
- }
- synchronized (mLock) {
- mCurrentMediaItem = item;
- }
- mCallback.onCurrentMediaItemChanged(MediaController2.this, item);
- break;
- }
- case SESSION_EVENT_ON_ERROR: {
- int errorCode = extras.getInt(ARGUMENT_ERROR_CODE);
- Bundle errorExtras = extras.getBundle(ARGUMENT_EXTRAS);
- mCallback.onError(MediaController2.this, errorCode, errorExtras);
- break;
- }
- case SESSION_EVENT_ON_ROUTES_INFO_CHANGED: {
- List<Bundle> routes = MediaUtils2.toBundleList(
- extras.getParcelableArray(ARGUMENT_ROUTE_BUNDLE));
- mCallback.onRoutesInfoChanged(MediaController2.this, routes);
- break;
- }
- case SESSION_EVENT_ON_PLAYLIST_CHANGED: {
- MediaMetadata2 playlistMetadata = MediaMetadata2.fromBundle(
- extras.getBundle(ARGUMENT_PLAYLIST_METADATA));
- List<MediaItem2> playlist = MediaUtils2.fromMediaItem2ParcelableArray(
- extras.getParcelableArray(ARGUMENT_PLAYLIST));
- synchronized (mLock) {
- mPlaylist = playlist;
- mPlaylistMetadata = playlistMetadata;
- }
- mCallback.onPlaylistChanged(MediaController2.this, playlist, playlistMetadata);
- break;
- }
- case SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED: {
- MediaMetadata2 playlistMetadata = MediaMetadata2.fromBundle(
- extras.getBundle(ARGUMENT_PLAYLIST_METADATA));
- synchronized (mLock) {
- mPlaylistMetadata = playlistMetadata;
- }
- mCallback.onPlaylistMetadataChanged(MediaController2.this, playlistMetadata);
- break;
- }
- case SESSION_EVENT_ON_REPEAT_MODE_CHANGED: {
- int repeatMode = extras.getInt(ARGUMENT_REPEAT_MODE);
- synchronized (mLock) {
- mRepeatMode = repeatMode;
- }
- mCallback.onRepeatModeChanged(MediaController2.this, repeatMode);
- break;
- }
- case SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED: {
- int shuffleMode = extras.getInt(ARGUMENT_SHUFFLE_MODE);
- synchronized (mLock) {
- mShuffleMode = shuffleMode;
- }
- mCallback.onShuffleModeChanged(MediaController2.this, shuffleMode);
- break;
- }
- case SESSION_EVENT_SEND_CUSTOM_COMMAND: {
- Bundle commandBundle = extras.getBundle(ARGUMENT_CUSTOM_COMMAND);
- if (commandBundle == null) {
- return;
- }
- SessionCommand2 command = SessionCommand2.fromBundle(commandBundle);
- Bundle args = extras.getBundle(ARGUMENT_ARGUMENTS);
- ResultReceiver receiver = extras.getParcelable(ARGUMENT_RESULT_RECEIVER);
- mCallback.onCustomCommand(MediaController2.this, command, args, receiver);
- break;
- }
- case SESSION_EVENT_SET_CUSTOM_LAYOUT: {
- List<CommandButton> layout = MediaUtils2.fromCommandButtonParcelableArray(
- extras.getParcelableArray(ARGUMENT_COMMAND_BUTTONS));
- if (layout == null) {
- return;
- }
- mCallback.onCustomLayoutChanged(MediaController2.this, layout);
- break;
- }
- case SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED: {
- PlaybackInfo info = PlaybackInfo.fromBundle(
- extras.getBundle(ARGUMENT_PLAYBACK_INFO));
- if (info == null) {
- return;
- }
- synchronized (mLock) {
- mPlaybackInfo = info;
- }
- mCallback.onPlaybackInfoChanged(MediaController2.this, info);
- break;
- }
- case SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED: {
- PlaybackStateCompat state =
- extras.getParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT);
- if (state == null) {
- return;
- }
- synchronized (mLock) {
- mPlaybackStateCompat = state;
- }
- mCallback.onPlaybackSpeedChanged(
- MediaController2.this, state.getPlaybackSpeed());
- break;
- }
- case SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED: {
- MediaItem2 item = MediaItem2.fromBundle(extras.getBundle(ARGUMENT_MEDIA_ITEM));
- int bufferingState = extras.getInt(ARGUMENT_BUFFERING_STATE);
- if (item == null) {
- return;
- }
- synchronized (mLock) {
- mBufferingState = bufferingState;
- }
- mCallback.onBufferingStateChanged(MediaController2.this, item, bufferingState);
- break;
- }
- }
- }
- }
-
- private static final String TAG = "MediaController2";
- private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
-
- // Note: Using {@code null} doesn't helpful here because MediaBrowserServiceCompat always wraps
- // the rootHints so it becomes non-null.
- static final Bundle sDefaultRootExtras = new Bundle();
- static {
- sDefaultRootExtras.putBoolean(MediaConstants2.ROOT_EXTRA_DEFAULT, true);
- }
-
- private final Context mContext;
- private final Object mLock = new Object();
-
- private final SessionToken2 mToken;
- private final ControllerCallback mCallback;
- private final Executor mCallbackExecutor;
- private final IBinder.DeathRecipient mDeathRecipient;
-
- private final HandlerThread mHandlerThread;
- private final Handler mHandler;
-
- @GuardedBy("mLock")
- private MediaBrowserCompat mBrowserCompat;
- @GuardedBy("mLock")
- private boolean mIsReleased;
- @GuardedBy("mLock")
- private List<MediaItem2> mPlaylist;
- @GuardedBy("mLock")
- private MediaMetadata2 mPlaylistMetadata;
- @GuardedBy("mLock")
- private @RepeatMode int mRepeatMode;
- @GuardedBy("mLock")
- private @ShuffleMode int mShuffleMode;
- @GuardedBy("mLock")
- private int mPlayerState;
- @GuardedBy("mLock")
- private MediaItem2 mCurrentMediaItem;
- @GuardedBy("mLock")
- private int mBufferingState;
- @GuardedBy("mLock")
- private PlaybackInfo mPlaybackInfo;
- @GuardedBy("mLock")
- private SessionCommandGroup2 mAllowedCommands;
-
- // Media 1.0 variables
- @GuardedBy("mLock")
- private MediaControllerCompat mControllerCompat;
- @GuardedBy("mLock")
- private ControllerCompatCallback mControllerCompatCallback;
- @GuardedBy("mLock")
- private PlaybackStateCompat mPlaybackStateCompat;
- @GuardedBy("mLock")
- private MediaMetadataCompat mMediaMetadataCompat;
-
- // Assignment should be used with the lock hold, but should be used without a lock to prevent
- // potential deadlock.
- @GuardedBy("mLock")
- private volatile boolean mConnected;
-
- /**
- * Create a {@link MediaController2} from the {@link SessionToken2}.
- * This connects to the session and may wake up the service if it's not available.
- *
- * @param context Context
- * @param token token to connect to
- * @param executor executor to run callbacks on.
- * @param callback controller callback to receive changes in
- */
- public MediaController2(@NonNull Context context, @NonNull SessionToken2 token,
- @NonNull Executor executor, @NonNull ControllerCallback callback) {
- super();
- if (context == null) {
- throw new IllegalArgumentException("context shouldn't be null");
- }
- if (token == null) {
- throw new IllegalArgumentException("token shouldn't be null");
- }
- if (callback == null) {
- throw new IllegalArgumentException("callback shouldn't be null");
- }
- if (executor == null) {
- throw new IllegalArgumentException("executor shouldn't be null");
- }
- mContext = context;
- mHandlerThread = new HandlerThread("MediaController2_Thread");
- mHandlerThread.start();
- mHandler = new Handler(mHandlerThread.getLooper());
- mToken = token;
- mCallback = callback;
- mCallbackExecutor = executor;
- mDeathRecipient = new IBinder.DeathRecipient() {
- @Override
- public void binderDied() {
- MediaController2.this.close();
- }
- };
-
- initialize();
- }
-
- /**
- * Release this object, and disconnect from the session. After this, callbacks wouldn't be
- * received.
- */
- @Override
- public void close() {
- if (DEBUG) {
- //Log.d(TAG, "release from " + mToken, new IllegalStateException());
- }
- synchronized (mLock) {
- if (mIsReleased) {
- // Prevent re-enterance from the ControllerCallback.onDisconnected()
- return;
- }
- mHandler.removeCallbacksAndMessages(null);
- mHandlerThread.quitSafely();
-
- mIsReleased = true;
-
- // Send command before the unregister callback to use mIControllerCallback in the
- // callback.
- sendCommand(CONTROLLER_COMMAND_DISCONNECT);
- if (mControllerCompat != null) {
- mControllerCompat.unregisterCallback(mControllerCompatCallback);
- }
- if (mBrowserCompat != null) {
- mBrowserCompat.disconnect();
- mBrowserCompat = null;
- }
- if (mControllerCompat != null) {
- mControllerCompat.unregisterCallback(mControllerCompatCallback);
- mControllerCompat = null;
- }
- mConnected = false;
- }
- mCallbackExecutor.execute(new Runnable() {
- @Override
- public void run() {
- mCallback.onDisconnected(MediaController2.this);
- }
- });
- }
-
- /**
- * @return token
- */
- public @NonNull SessionToken2 getSessionToken() {
- return mToken;
- }
-
- /**
- * Returns whether this class is connected to active {@link MediaSession2} or not.
- */
- public boolean isConnected() {
- synchronized (mLock) {
- return mConnected;
- }
- }
-
- /**
- * Requests that the player starts or resumes playback.
- */
- public void play() {
- synchronized (mLock) {
- if (!mConnected) {
- Log.w(TAG, "Session isn't active", new IllegalStateException());
- return;
- }
- sendCommand(COMMAND_CODE_PLAYBACK_PLAY);
- }
- }
-
- /**
- * Requests that the player pauses playback.
- */
- public void pause() {
- synchronized (mLock) {
- if (!mConnected) {
- Log.w(TAG, "Session isn't active", new IllegalStateException());
- return;
- }
- sendCommand(COMMAND_CODE_PLAYBACK_PAUSE);
- }
- }
-
- /**
- * Requests that the player be reset to its uninitialized state.
- */
- public void reset() {
- synchronized (mLock) {
- if (!mConnected) {
- Log.w(TAG, "Session isn't active", new IllegalStateException());
- return;
- }
- sendCommand(COMMAND_CODE_PLAYBACK_RESET);
- }
- }
-
- /**
- * Request that the player prepare its playback. In other words, other sessions can continue
- * to play during the preparation of this session. This method can be used to speed up the
- * start of the playback. Once the preparation is done, the session will change its playback
- * state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be called
- * to start playback.
- */
- public void prepare() {
- synchronized (mLock) {
- if (!mConnected) {
- Log.w(TAG, "Session isn't active", new IllegalStateException());
- return;
- }
- sendCommand(COMMAND_CODE_PLAYBACK_PREPARE);
- }
- }
-
- /**
- * Start fast forwarding. If playback is already fast forwarding this
- * may increase the rate.
- */
- public void fastForward() {
- synchronized (mLock) {
- if (!mConnected) {
- Log.w(TAG, "Session isn't active", new IllegalStateException());
- return;
- }
- sendCommand(COMMAND_CODE_SESSION_FAST_FORWARD);
- }
- }
-
- /**
- * Start rewinding. If playback is already rewinding this may increase
- * the rate.
- */
- public void rewind() {
- synchronized (mLock) {
- if (!mConnected) {
- Log.w(TAG, "Session isn't active", new IllegalStateException());
- return;
- }
- sendCommand(COMMAND_CODE_SESSION_REWIND);
- }
- }
-
- /**
- * Move to a new location in the media stream.
- *
- * @param pos Position to move to, in milliseconds.
- */
- public void seekTo(long pos) {
- synchronized (mLock) {
- if (!mConnected) {
- Log.w(TAG, "Session isn't active", new IllegalStateException());
- return;
- }
- Bundle args = new Bundle();
- args.putLong(ARGUMENT_SEEK_POSITION, pos);
- sendCommand(COMMAND_CODE_PLAYBACK_SEEK_TO, args);
- }
- }
-
- /**
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public void skipForward() {
- // To match with KEYCODE_MEDIA_SKIP_FORWARD
- }
-
- /**
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public void skipBackward() {
- // To match with KEYCODE_MEDIA_SKIP_BACKWARD
- }
-
- /**
- * Request that the player start playback for a specific media id.
- *
- * @param mediaId The id of the requested media.
- * @param extras Optional extras that can include extra information about the media item
- * to be played.
- */
- public void playFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
- synchronized (mLock) {
- if (!mConnected) {
- Log.w(TAG, "Session isn't active", new IllegalStateException());
- return;
- }
- Bundle args = new Bundle();
- args.putString(ARGUMENT_MEDIA_ID, mediaId);
- args.putBundle(ARGUMENT_EXTRAS, extras);
- sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID, args);
- }
- }
-
- /**
- * Request that the player start playback for a specific search query.
- *
- * @param query The search query. Should not be an empty string.
- * @param extras Optional extras that can include extra information about the query.
- */
- public void playFromSearch(@NonNull String query, @Nullable Bundle extras) {
- synchronized (mLock) {
- if (!mConnected) {
- Log.w(TAG, "Session isn't active", new IllegalStateException());
- return;
- }
- Bundle args = new Bundle();
- args.putString(ARGUMENT_QUERY, query);
- args.putBundle(ARGUMENT_EXTRAS, extras);
- sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_SEARCH, args);
- }
- }
-
- /**
- * Request that the player start playback for a specific {@link Uri}.
- *
- * @param uri The URI of the requested media.
- * @param extras Optional extras that can include extra information about the media item
- * to be played.
- */
- public void playFromUri(@NonNull Uri uri, @Nullable Bundle extras) {
- synchronized (mLock) {
- if (!mConnected) {
- Log.w(TAG, "Session isn't active", new IllegalStateException());
- return;
- }
- Bundle args = new Bundle();
- args.putParcelable(ARGUMENT_URI, uri);
- args.putBundle(ARGUMENT_EXTRAS, extras);
- sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_URI, args);
- }
- }
-
- /**
- * Request that the player prepare playback for a specific media id. In other words, other
- * sessions can continue to play during the preparation of this session. This method can be
- * used to speed up the start of the playback. Once the preparation is done, the session
- * will change its playback state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards,
- * {@link #play} can be called to start playback. If the preparation is not needed,
- * {@link #playFromMediaId} can be directly called without this method.
- *
- * @param mediaId The id of the requested media.
- * @param extras Optional extras that can include extra information about the media item
- * to be prepared.
- */
- public void prepareFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
- synchronized (mLock) {
- if (!mConnected) {
- Log.w(TAG, "Session isn't active", new IllegalStateException());
- return;
- }
- Bundle args = new Bundle();
- args.putString(ARGUMENT_MEDIA_ID, mediaId);
- args.putBundle(ARGUMENT_EXTRAS, extras);
- sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID, args);
- }
- }
-
- /**
- * Request that the player prepare playback for a specific search query.
- * In other words, other sessions can continue to play during the preparation of this session.
- * This method can be used to speed up the start of the playback.
- * Once the preparation is done, the session will change its playback state to
- * {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards,
- * {@link #play} can be called to start playback. If the preparation is not needed,
- * {@link #playFromSearch} can be directly called without this method.
- *
- * @param query The search query. Should not be an empty string.
- * @param extras Optional extras that can include extra information about the query.
- */
- public void prepareFromSearch(@NonNull String query, @Nullable Bundle extras) {
- synchronized (mLock) {
- if (!mConnected) {
- Log.w(TAG, "Session isn't active", new IllegalStateException());
- return;
- }
- Bundle args = new Bundle();
- args.putString(ARGUMENT_QUERY, query);
- args.putBundle(ARGUMENT_EXTRAS, extras);
- sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH, args);
- }
- }
-
- /**
- * Request that the player prepare playback for a specific {@link Uri}. In other words,
- * other sessions can continue to play during the preparation of this session. This method
- * can be used to speed up the start of the playback. Once the preparation is done, the
- * session will change its playback state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}.
- * Afterwards, {@link #play} can be called to start playback. If the preparation is not needed,
- * {@link #playFromUri} can be directly called without this method.
- *
- * @param uri The URI of the requested media.
- * @param extras Optional extras that can include extra information about the media item
- * to be prepared.
- */
- public void prepareFromUri(@NonNull Uri uri, @Nullable Bundle extras) {
- synchronized (mLock) {
- if (!mConnected) {
- Log.w(TAG, "Session isn't active", new IllegalStateException());
- return;
- }
- Bundle args = new Bundle();
- args.putParcelable(ARGUMENT_URI, uri);
- args.putBundle(ARGUMENT_EXTRAS, extras);
- sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_URI, args);
- }
- }
-
- /**
- * Set the volume of the output this session is playing on. The command will be ignored if it
- * does not support {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}.
- * <p>
- * If the session is local playback, this changes the device's volume with the stream that
- * session's player is using. Flags will be specified for the {@link AudioManager}.
- * <p>
- * If the session is remote player (i.e. session has set volume provider), its volume provider
- * will receive this request instead.
- *
- * @see #getPlaybackInfo()
- * @param value The value to set it to, between 0 and the reported max.
- * @param flags flags from {@link AudioManager} to include with the volume request for local
- * playback
- */
- public void setVolumeTo(int value, @VolumeFlags int flags) {
- synchronized (mLock) {
- if (!mConnected) {
- Log.w(TAG, "Session isn't active", new IllegalStateException());
- return;
- }
- Bundle args = new Bundle();
- args.putInt(ARGUMENT_VOLUME, value);
- args.putInt(ARGUMENT_VOLUME_FLAGS, flags);
- sendCommand(COMMAND_CODE_VOLUME_SET_VOLUME, args);
- }
- }
-
- /**
- * Adjust the volume of the output this session is playing on. The direction
- * must be one of {@link AudioManager#ADJUST_LOWER},
- * {@link AudioManager#ADJUST_RAISE}, or {@link AudioManager#ADJUST_SAME}.
- * <p>
- * The command will be ignored if the session does not support
- * {@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE} or
- * {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}.
- * <p>
- * If the session is local playback, this changes the device's volume with the stream that
- * session's player is using. Flags will be specified for the {@link AudioManager}.
- * <p>
- * If the session is remote player (i.e. session has set volume provider), its volume provider
- * will receive this request instead.
- *
- * @see #getPlaybackInfo()
- * @param direction The direction to adjust the volume in.
- * @param flags flags from {@link AudioManager} to include with the volume request for local
- * playback
- */
- public void adjustVolume(@VolumeDirection int direction, @VolumeFlags int flags) {
- synchronized (mLock) {
- if (!mConnected) {
- Log.w(TAG, "Session isn't active", new IllegalStateException());
- return;
- }
- Bundle args = new Bundle();
- args.putInt(ARGUMENT_VOLUME_DIRECTION, direction);
- args.putInt(ARGUMENT_VOLUME_FLAGS, flags);
- sendCommand(COMMAND_CODE_VOLUME_ADJUST_VOLUME, args);
- }
- }
-
- /**
- * Get an intent for launching UI associated with this session if one exists.
- *
- * @return A {@link PendingIntent} to launch UI or null.
- */
- public @Nullable PendingIntent getSessionActivity() {
- synchronized (mLock) {
- if (!mConnected) {
- Log.w(TAG, "Session isn't active", new IllegalStateException());
- return null;
- }
- return mControllerCompat.getSessionActivity();
- }
- }
-
- /**
- * Get the lastly cached player state from
- * {@link ControllerCallback#onPlayerStateChanged(MediaController2, int)}.
- *
- * @return player state
- */
- public int getPlayerState() {
- synchronized (mLock) {
- return mPlayerState;
- }
- }
-
- /**
- * Gets the duration of the current media item, or {@link MediaPlayerBase#UNKNOWN_TIME} if
- * unknown.
- * @return the duration in ms, or {@link MediaPlayerBase#UNKNOWN_TIME}.
- */
- public long getDuration() {
- synchronized (mLock) {
- if (mMediaMetadataCompat != null
- && mMediaMetadataCompat.containsKey(METADATA_KEY_DURATION)) {
- return mMediaMetadataCompat.getLong(METADATA_KEY_DURATION);
- }
- }
- return MediaPlayerBase.UNKNOWN_TIME;
- }
-
- /**
- * Gets the current playback position.
- * <p>
- * This returns the calculated value of the position, based on the difference between the
- * update time and current time.
- *
- * @return position
- */
- public long getCurrentPosition() {
- synchronized (mLock) {
- if (!mConnected) {
- Log.w(TAG, "Session isn't active", new IllegalStateException());
- return UNKNOWN_TIME;
- }
- if (mPlaybackStateCompat != null) {
- long timeDiff = SystemClock.elapsedRealtime()
- - mPlaybackStateCompat.getLastPositionUpdateTime();
- long expectedPosition = mPlaybackStateCompat.getPosition()
- + (long) (mPlaybackStateCompat.getPlaybackSpeed() * timeDiff);
- return Math.max(0, expectedPosition);
- }
- return UNKNOWN_TIME;
- }
- }
-
- /**
- * Get the lastly cached playback speed from
- * {@link ControllerCallback#onPlaybackSpeedChanged(MediaController2, float)}.
- *
- * @return speed the lastly cached playback speed, or 0.0f if unknown.
- */
- public float getPlaybackSpeed() {
- synchronized (mLock) {
- if (!mConnected) {
- Log.w(TAG, "Session isn't active", new IllegalStateException());
- return 0f;
- }
- return (mPlaybackStateCompat == null) ? 0f : mPlaybackStateCompat.getPlaybackSpeed();
- }
- }
-
- /**
- * Set the playback speed.
- */
- public void setPlaybackSpeed(float speed) {
- synchronized (mLock) {
- if (!mConnected) {
- Log.w(TAG, "Session isn't active", new IllegalStateException());
- return;
- }
- Bundle args = new Bundle();
- args.putFloat(ARGUMENT_PLAYBACK_SPEED, speed);
- sendCommand(COMMAND_CODE_PLAYBACK_SET_SPEED, args);
- }
- }
-
- /**
- * Gets the current buffering state of the player.
- * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
- * buffered.
- * @return the buffering state.
- */
- public @MediaPlayerBase.BuffState int getBufferingState() {
- synchronized (mLock) {
- if (!mConnected) {
- Log.w(TAG, "Session isn't active", new IllegalStateException());
- return BUFFERING_STATE_UNKNOWN;
- }
- return mBufferingState;
- }
- }
-
- /**
- * Gets the lastly cached buffered position from the session when
- * {@link ControllerCallback#onBufferingStateChanged(MediaController2, MediaItem2, int)} is
- * called.
- *
- * @return buffering position in millis, or {@link MediaPlayerBase#UNKNOWN_TIME} if unknown.
- */
- public long getBufferedPosition() {
- synchronized (mLock) {
- if (!mConnected) {
- Log.w(TAG, "Session isn't active", new IllegalStateException());
- return UNKNOWN_TIME;
- }
- return (mPlaybackStateCompat == null) ? UNKNOWN_TIME
- : mPlaybackStateCompat.getBufferedPosition();
- }
- }
-
- /**
- * Get the current playback info for this session.
- *
- * @return The current playback info or null.
- */
- public @Nullable PlaybackInfo getPlaybackInfo() {
- synchronized (mLock) {
- return mPlaybackInfo;
- }
- }
-
- /**
- * Rate the media. This will cause the rating to be set for the current user.
- * The rating style must follow the user rating style from the session.
- * You can get the rating style from the session through the
- * {@link MediaMetadata2#getRating(String)} with the key
- * {@link MediaMetadata2#METADATA_KEY_USER_RATING}.
- * <p>
- * If the user rating was {@code null}, the media item does not accept setting user rating.
- *
- * @param mediaId The id of the media
- * @param rating The rating to set
- */
- public void setRating(@NonNull String mediaId, @NonNull Rating2 rating) {
- synchronized (mLock) {
- if (!mConnected) {
- Log.w(TAG, "Session isn't active", new IllegalStateException());
- return;
- }
- Bundle args = new Bundle();
- args.putString(ARGUMENT_MEDIA_ID, mediaId);
- args.putBundle(ARGUMENT_RATING, rating.toBundle());
- sendCommand(COMMAND_CODE_SESSION_SET_RATING, args);
- }
- }
-
- /**
- * Send custom command to the session
- *
- * @param command custom command
- * @param args optional argument
- * @param cb optional result receiver
- */
- public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args,
- @Nullable ResultReceiver cb) {
- synchronized (mLock) {
- if (!mConnected) {
- Log.w(TAG, "Session isn't active", new IllegalStateException());
- return;
- }
- Bundle bundle = new Bundle();
- bundle.putBundle(ARGUMENT_CUSTOM_COMMAND, command.toBundle());
- bundle.putBundle(ARGUMENT_ARGUMENTS, args);
- sendCommand(CONTROLLER_COMMAND_BY_CUSTOM_COMMAND, bundle, cb);
- }
- }
-
- /**
- * Returns the cached playlist from {@link ControllerCallback#onPlaylistChanged}.
- * <p>
- * This list may differ with the list that was specified with
- * {@link #setPlaylist(List, MediaMetadata2)} depending on the {@link MediaPlaylistAgent}
- * implementation. Use media items returned here for other playlist agent APIs such as
- * {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}.
- *
- * @return playlist. Can be {@code null} if the playlist hasn't set nor controller doesn't have
- * enough permission.
- * @see SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST
- */
- public @Nullable List<MediaItem2> getPlaylist() {
- synchronized (mLock) {
- return mPlaylist;
- }
- }
-
- /**
- * Sets the playlist.
- * <p>
- * Even when the playlist is successfully set, use the playlist returned from
- * {@link #getPlaylist()} for playlist APIs such as {@link #skipToPlaylistItem(MediaItem2)}.
- * Otherwise the session in the remote process can't distinguish between media items.
- *
- * @param list playlist
- * @param metadata metadata of the playlist
- * @see #getPlaylist()
- * @see ControllerCallback#onPlaylistChanged
- */
- public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
- if (list == null) {
- throw new IllegalArgumentException("list shouldn't be null");
- }
- Bundle args = new Bundle();
- args.putParcelableArray(ARGUMENT_PLAYLIST, MediaUtils2.toMediaItem2ParcelableArray(list));
- args.putBundle(ARGUMENT_PLAYLIST_METADATA, metadata == null ? null : metadata.toBundle());
- sendCommand(COMMAND_CODE_PLAYLIST_SET_LIST, args);
- }
-
- /**
- * Updates the playlist metadata
- *
- * @param metadata metadata of the playlist
- */
- public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
- Bundle args = new Bundle();
- args.putBundle(ARGUMENT_PLAYLIST_METADATA, metadata == null ? null : metadata.toBundle());
- sendCommand(COMMAND_CODE_PLAYLIST_SET_LIST_METADATA, args);
- }
-
- /**
- * Gets the lastly cached playlist playlist metadata either from
- * {@link ControllerCallback#onPlaylistMetadataChanged or
- * {@link ControllerCallback#onPlaylistChanged}.
- *
- * @return metadata metadata of the playlist, or null if none is set
- */
- public @Nullable MediaMetadata2 getPlaylistMetadata() {
- synchronized (mLock) {
- return mPlaylistMetadata;
- }
- }
-
- /**
- * Adds the media item to the playlist at position index. Index equals or greater than
- * the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of
- * the playlist.
- * <p>
- * This will not change the currently playing media item.
- * If index is less than or equal to the current index of the playlist,
- * the current index of the playlist will be incremented correspondingly.
- *
- * @param index the index you want to add
- * @param item the media item you want to add
- */
- public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
- Bundle args = new Bundle();
- args.putInt(ARGUMENT_PLAYLIST_INDEX, index);
- args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
- sendCommand(COMMAND_CODE_PLAYLIST_ADD_ITEM, args);
- }
-
- /**
- * Removes the media item at index in the playlist.
- *<p>
- * If the item is the currently playing item of the playlist, current playback
- * will be stopped and playback moves to next source in the list.
- *
- * @param item the media item you want to add
- */
- public void removePlaylistItem(@NonNull MediaItem2 item) {
- Bundle args = new Bundle();
- args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
- sendCommand(COMMAND_CODE_PLAYLIST_REMOVE_ITEM, args);
- }
-
- /**
- * Replace the media item at index in the playlist. This can be also used to update metadata of
- * an item.
- *
- * @param index the index of the item to replace
- * @param item the new item
- */
- public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
- Bundle args = new Bundle();
- args.putInt(ARGUMENT_PLAYLIST_INDEX, index);
- args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
- sendCommand(COMMAND_CODE_PLAYLIST_REPLACE_ITEM, args);
- }
-
- /**
- * Get the lastly cached current item from
- * {@link ControllerCallback#onCurrentMediaItemChanged(MediaController2, MediaItem2)}.
- *
- * @return the currently playing item, or null if unknown.
- */
- public MediaItem2 getCurrentMediaItem() {
- synchronized (mLock) {
- return mCurrentMediaItem;
- }
- }
-
- /**
- * Skips to the previous item in the playlist.
- * <p>
- * This calls {@link MediaPlaylistAgent#skipToPreviousItem()}.
- */
- public void skipToPreviousItem() {
- sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM);
- }
-
- /**
- * Skips to the next item in the playlist.
- * <p>
- * This calls {@link MediaPlaylistAgent#skipToNextItem()}.
- */
- public void skipToNextItem() {
- sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM);
- }
-
- /**
- * Skips to the item in the playlist.
- * <p>
- * This calls {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}.
- *
- * @param item The item in the playlist you want to play
- */
- public void skipToPlaylistItem(@NonNull MediaItem2 item) {
- Bundle args = new Bundle();
- args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
- sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM, args);
- }
-
- /**
- * Gets the cached repeat mode from the {@link ControllerCallback#onRepeatModeChanged}.
- *
- * @return repeat mode
- * @see MediaPlaylistAgent#REPEAT_MODE_NONE
- * @see MediaPlaylistAgent#REPEAT_MODE_ONE
- * @see MediaPlaylistAgent#REPEAT_MODE_ALL
- * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
- */
- public @RepeatMode int getRepeatMode() {
- synchronized (mLock) {
- return mRepeatMode;
- }
- }
-
- /**
- * Sets the repeat mode.
- *
- * @param repeatMode repeat mode
- * @see MediaPlaylistAgent#REPEAT_MODE_NONE
- * @see MediaPlaylistAgent#REPEAT_MODE_ONE
- * @see MediaPlaylistAgent#REPEAT_MODE_ALL
- * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
- */
- public void setRepeatMode(@RepeatMode int repeatMode) {
- Bundle args = new Bundle();
- args.putInt(ARGUMENT_REPEAT_MODE, repeatMode);
- sendCommand(COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE, args);
- }
-
- /**
- * Gets the cached shuffle mode from the {@link ControllerCallback#onShuffleModeChanged}.
- *
- * @return The shuffle mode
- * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
- * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
- * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
- */
- public @ShuffleMode int getShuffleMode() {
- synchronized (mLock) {
- return mShuffleMode;
- }
- }
-
- /**
- * Sets the shuffle mode.
- *
- * @param shuffleMode The shuffle mode
- * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
- * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
- * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
- */
- public void setShuffleMode(@ShuffleMode int shuffleMode) {
- Bundle args = new Bundle();
- args.putInt(ARGUMENT_SHUFFLE_MODE, shuffleMode);
- sendCommand(COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE, args);
- }
-
- /**
- * Queries for information about the routes currently known.
- */
- public void subscribeRoutesInfo() {
- sendCommand(COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO);
- }
-
- /**
- * Unsubscribes for changes to the routes.
- * <p>
- * The {@link ControllerCallback#onRoutesInfoChanged callback} will no longer be invoked for
- * the routes once this method returns.
- * </p>
- */
- public void unsubscribeRoutesInfo() {
- sendCommand(COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO);
- }
-
- /**
- * Selects the specified route.
- *
- * @param route The route to select.
- */
- public void selectRoute(@NonNull Bundle route) {
- if (route == null) {
- throw new IllegalArgumentException("route shouldn't be null");
- }
- Bundle args = new Bundle();
- args.putBundle(ARGUMENT_ROUTE_BUNDLE, route);
- sendCommand(COMMAND_CODE_SESSION_SELECT_ROUTE, args);
- }
-
- // Should be used without a lock to prevent potential deadlock.
- void onConnectedNotLocked(Bundle data) {
- // is enough or should we pass it while connecting?
- final SessionCommandGroup2 allowedCommands = SessionCommandGroup2.fromBundle(
- data.getBundle(ARGUMENT_ALLOWED_COMMANDS));
- final int playerState = data.getInt(ARGUMENT_PLAYER_STATE);
- final int bufferingState = data.getInt(ARGUMENT_BUFFERING_STATE);
- final PlaybackStateCompat playbackStateCompat = data.getParcelable(
- ARGUMENT_PLAYBACK_STATE_COMPAT);
- final int repeatMode = data.getInt(ARGUMENT_REPEAT_MODE);
- final int shuffleMode = data.getInt(ARGUMENT_SHUFFLE_MODE);
- final List<MediaItem2> playlist = MediaUtils2.fromMediaItem2ParcelableArray(
- data.getParcelableArray(ARGUMENT_PLAYLIST));
- final MediaItem2 currentMediaItem = MediaItem2.fromBundle(
- data.getBundle(ARGUMENT_MEDIA_ITEM));
- final PlaybackInfo playbackInfo =
- PlaybackInfo.fromBundle(data.getBundle(ARGUMENT_PLAYBACK_INFO));
- final MediaMetadata2 metadata = MediaMetadata2.fromBundle(
- data.getBundle(ARGUMENT_PLAYLIST_METADATA));
- if (DEBUG) {
- Log.d(TAG, "onConnectedNotLocked sessionCompatToken=" + mToken.getSessionCompatToken()
- + ", allowedCommands=" + allowedCommands);
- }
- boolean close = false;
- try {
- synchronized (mLock) {
- if (mIsReleased) {
- return;
- }
- if (mConnected) {
- Log.e(TAG, "Cannot be notified about the connection result many times."
- + " Probably a bug or malicious app.");
- close = true;
- return;
- }
- mAllowedCommands = allowedCommands;
- mPlayerState = playerState;
- mBufferingState = bufferingState;
- mPlaybackStateCompat = playbackStateCompat;
- mRepeatMode = repeatMode;
- mShuffleMode = shuffleMode;
- mPlaylist = playlist;
- mCurrentMediaItem = currentMediaItem;
- mPlaylistMetadata = metadata;
- mConnected = true;
- mPlaybackInfo = playbackInfo;
- }
- mCallbackExecutor.execute(new Runnable() {
- @Override
- public void run() {
- // Note: We may trigger ControllerCallbacks with the initial values
- // But it's hard to define the order of the controller callbacks
- // Only notify about the
- mCallback.onConnected(MediaController2.this, allowedCommands);
- }
- });
- } finally {
- if (close) {
- // Trick to call release() without holding the lock, to prevent potential deadlock
- // with the developer's custom lock within the ControllerCallback.onDisconnected().
- close();
- }
- }
- }
-
- private void initialize() {
- if (mToken.getType() == SessionToken2.TYPE_SESSION) {
- synchronized (mLock) {
- mBrowserCompat = null;
- }
- connectToSession(mToken.getSessionCompatToken());
- } else {
- connectToService();
- }
- }
-
- private void connectToSession(MediaSessionCompat.Token sessionCompatToken) {
- MediaControllerCompat controllerCompat = null;
- try {
- controllerCompat = new MediaControllerCompat(mContext, sessionCompatToken);
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- synchronized (mLock) {
- mControllerCompat = controllerCompat;
- mControllerCompatCallback = new ControllerCompatCallback();
- mControllerCompat.registerCallback(mControllerCompatCallback, mHandler);
- }
-
- if (controllerCompat.isSessionReady()) {
- sendCommand(CONTROLLER_COMMAND_CONNECT, new ResultReceiver(mHandler) {
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- if (!mHandlerThread.isAlive()) {
- return;
- }
- switch (resultCode) {
- case CONNECT_RESULT_CONNECTED:
- onConnectedNotLocked(resultData);
- break;
- case CONNECT_RESULT_DISCONNECTED:
- mCallback.onDisconnected(MediaController2.this);
- close();
- break;
- }
- }
- });
- }
- }
-
- private void connectToService() {
- synchronized (mLock) {
- mBrowserCompat = new MediaBrowserCompat(mContext, mToken.getComponentName(),
- new ConnectionCallback(), sDefaultRootExtras);
- mBrowserCompat.connect();
- }
- }
-
- private void sendCommand(int commandCode) {
- sendCommand(commandCode, null);
- }
-
- private void sendCommand(int commandCode, Bundle args) {
- if (args == null) {
- args = new Bundle();
- }
- args.putInt(ARGUMENT_COMMAND_CODE, commandCode);
- sendCommand(CONTROLLER_COMMAND_BY_COMMAND_CODE, args, null);
- }
-
- private void sendCommand(String command) {
- sendCommand(command, null, null);
- }
-
- private void sendCommand(String command, ResultReceiver receiver) {
- sendCommand(command, null, receiver);
- }
-
- private void sendCommand(String command, Bundle args, ResultReceiver receiver) {
- if (args == null) {
- args = new Bundle();
- }
- MediaControllerCompat controller;
- ControllerCompatCallback callback;
- synchronized (mLock) {
- controller = mControllerCompat;
- callback = mControllerCompatCallback;
- }
- args.putBinder(ARGUMENT_ICONTROLLER_CALLBACK, callback.getIControllerCallback().asBinder());
- args.putString(ARGUMENT_PACKAGE_NAME, mContext.getPackageName());
- args.putInt(ARGUMENT_UID, Process.myUid());
- args.putInt(ARGUMENT_PID, Process.myPid());
- controller.sendCommand(command, args, receiver);
- }
-
- @NonNull Context getContext() {
- return mContext;
- }
-
- @NonNull ControllerCallback getCallback() {
- return mCallback;
- }
-
- @NonNull Executor getCallbackExecutor() {
- return mCallbackExecutor;
- }
-
- @Nullable MediaBrowserCompat getBrowserCompat() {
- synchronized (mLock) {
- return mBrowserCompat;
- }
- }
-
- private class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
- @Override
- public void onConnected() {
- MediaBrowserCompat browser = getBrowserCompat();
- if (browser != null) {
- connectToSession(browser.getSessionToken());
- } else if (DEBUG) {
- Log.d(TAG, "Controller is closed prematually", new IllegalStateException());
- }
- }
-
- @Override
- public void onConnectionSuspended() {
- close();
- }
-
- @Override
- public void onConnectionFailed() {
- close();
- }
- }
}
diff --git a/media/src/main/java/androidx/media/MediaController2ImplBase.java b/media/src/main/java/androidx/media/MediaController2ImplBase.java
new file mode 100644
index 0000000..1bd0009
--- /dev/null
+++ b/media/src/main/java/androidx/media/MediaController2ImplBase.java
@@ -0,0 +1,1283 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DURATION;
+
+import static androidx.media.MediaConstants2.ARGUMENT_ALLOWED_COMMANDS;
+import static androidx.media.MediaConstants2.ARGUMENT_ARGUMENTS;
+import static androidx.media.MediaConstants2.ARGUMENT_BUFFERING_STATE;
+import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_BUTTONS;
+import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_CODE;
+import static androidx.media.MediaConstants2.ARGUMENT_CUSTOM_COMMAND;
+import static androidx.media.MediaConstants2.ARGUMENT_ERROR_CODE;
+import static androidx.media.MediaConstants2.ARGUMENT_EXTRAS;
+import static androidx.media.MediaConstants2.ARGUMENT_ICONTROLLER_CALLBACK;
+import static androidx.media.MediaConstants2.ARGUMENT_ITEM_COUNT;
+import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ID;
+import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ITEM;
+import static androidx.media.MediaConstants2.ARGUMENT_PACKAGE_NAME;
+import static androidx.media.MediaConstants2.ARGUMENT_PID;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_INFO;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_SPEED;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_STATE_COMPAT;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYER_STATE;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_INDEX;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_METADATA;
+import static androidx.media.MediaConstants2.ARGUMENT_QUERY;
+import static androidx.media.MediaConstants2.ARGUMENT_RATING;
+import static androidx.media.MediaConstants2.ARGUMENT_REPEAT_MODE;
+import static androidx.media.MediaConstants2.ARGUMENT_RESULT_RECEIVER;
+import static androidx.media.MediaConstants2.ARGUMENT_ROUTE_BUNDLE;
+import static androidx.media.MediaConstants2.ARGUMENT_SEEK_POSITION;
+import static androidx.media.MediaConstants2.ARGUMENT_SHUFFLE_MODE;
+import static androidx.media.MediaConstants2.ARGUMENT_UID;
+import static androidx.media.MediaConstants2.ARGUMENT_URI;
+import static androidx.media.MediaConstants2.ARGUMENT_VOLUME;
+import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_DIRECTION;
+import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_FLAGS;
+import static androidx.media.MediaConstants2.CONNECT_RESULT_CONNECTED;
+import static androidx.media.MediaConstants2.CONNECT_RESULT_DISCONNECTED;
+import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_COMMAND_CODE;
+import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_CUSTOM_COMMAND;
+import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_CONNECT;
+import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_DISCONNECT;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_BUFFERING_STATE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CHILDREN_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ERROR;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYER_STATE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_REPEAT_MODE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ROUTES_INFO_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SEARCH_RESULT_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SEEK_COMPLETED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_SEND_CUSTOM_COMMAND;
+import static androidx.media.MediaConstants2.SESSION_EVENT_SET_CUSTOM_LAYOUT;
+import static androidx.media.MediaPlayerInterface.BUFFERING_STATE_UNKNOWN;
+import static androidx.media.MediaPlayerInterface.UNKNOWN_TIME;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_RESET;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SEEK_TO;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SET_SPEED;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_ADD_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REMOVE_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST_METADATA;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_FAST_FORWARD;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_REWIND;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SELECT_ROUTE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SET_RATING;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO;
+import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_ADJUST_VOLUME;
+import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_SET_VOLUME;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.SystemClock;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.app.BundleCompat;
+import androidx.media.MediaController2.ControllerCallback;
+import androidx.media.MediaController2.PlaybackInfo;
+import androidx.media.MediaController2.VolumeDirection;
+import androidx.media.MediaController2.VolumeFlags;
+import androidx.media.MediaPlaylistAgent.RepeatMode;
+import androidx.media.MediaPlaylistAgent.ShuffleMode;
+import androidx.media.MediaSession2.CommandButton;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+class MediaController2ImplBase implements MediaController2.SupportLibraryImpl {
+
+ private static final String TAG = "MC2ImplBase";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ // Note: Using {@code null} doesn't helpful here because MediaBrowserServiceCompat always wraps
+ // the rootHints so it becomes non-null.
+ static final Bundle sDefaultRootExtras = new Bundle();
+ static {
+ sDefaultRootExtras.putBoolean(MediaConstants2.ROOT_EXTRA_DEFAULT, true);
+ }
+
+ private final Context mContext;
+ private final Object mLock = new Object();
+
+ private final SessionToken2 mToken;
+ private final ControllerCallback mCallback;
+ private final Executor mCallbackExecutor;
+ private final IBinder.DeathRecipient mDeathRecipient;
+
+ private final HandlerThread mHandlerThread;
+ private final Handler mHandler;
+
+ private MediaController2 mInstance;
+
+ @GuardedBy("mLock")
+ private MediaBrowserCompat mBrowserCompat;
+ @GuardedBy("mLock")
+ private boolean mIsReleased;
+ @GuardedBy("mLock")
+ private List<MediaItem2> mPlaylist;
+ @GuardedBy("mLock")
+ private MediaMetadata2 mPlaylistMetadata;
+ @GuardedBy("mLock")
+ private @RepeatMode int mRepeatMode;
+ @GuardedBy("mLock")
+ private @ShuffleMode int mShuffleMode;
+ @GuardedBy("mLock")
+ private int mPlayerState;
+ @GuardedBy("mLock")
+ private MediaItem2 mCurrentMediaItem;
+ @GuardedBy("mLock")
+ private int mBufferingState;
+ @GuardedBy("mLock")
+ private PlaybackInfo mPlaybackInfo;
+ @GuardedBy("mLock")
+ private SessionCommandGroup2 mAllowedCommands;
+
+ // Media 1.0 variables
+ @GuardedBy("mLock")
+ private MediaControllerCompat mControllerCompat;
+ @GuardedBy("mLock")
+ private ControllerCompatCallback mControllerCompatCallback;
+ @GuardedBy("mLock")
+ private PlaybackStateCompat mPlaybackStateCompat;
+ @GuardedBy("mLock")
+ private MediaMetadataCompat mMediaMetadataCompat;
+
+ // Assignment should be used with the lock hold, but should be used without a lock to prevent
+ // potential deadlock.
+ @GuardedBy("mLock")
+ private volatile boolean mConnected;
+
+ MediaController2ImplBase(@NonNull Context context, @NonNull SessionToken2 token,
+ @NonNull Executor executor, @NonNull ControllerCallback callback) {
+ super();
+ if (context == null) {
+ throw new IllegalArgumentException("context shouldn't be null");
+ }
+ if (token == null) {
+ throw new IllegalArgumentException("token shouldn't be null");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("callback shouldn't be null");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("executor shouldn't be null");
+ }
+ mContext = context;
+ mHandlerThread = new HandlerThread("MediaController2_Thread");
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+ mToken = token;
+ mCallback = callback;
+ mCallbackExecutor = executor;
+ mDeathRecipient = new IBinder.DeathRecipient() {
+ @Override
+ public void binderDied() {
+ MediaController2ImplBase.this.close();
+ }
+ };
+
+ initialize();
+ }
+
+ @Override
+ public void setInstance(MediaController2 controller) {
+ mInstance = controller;
+ }
+
+ @Override
+ public void close() {
+ if (DEBUG) {
+ //Log.d(TAG, "release from " + mToken, new IllegalStateException());
+ }
+ synchronized (mLock) {
+ if (mIsReleased) {
+ // Prevent re-enterance from the ControllerCallback.onDisconnected()
+ return;
+ }
+ mHandler.removeCallbacksAndMessages(null);
+
+ if (Build.VERSION.SDK_INT >= 18) {
+ mHandlerThread.quitSafely();
+ } else {
+ mHandlerThread.quit();
+ }
+
+ mIsReleased = true;
+
+ // Send command before the unregister callback to use mIControllerCallback in the
+ // callback.
+ sendCommand(CONTROLLER_COMMAND_DISCONNECT);
+ if (mControllerCompat != null) {
+ mControllerCompat.unregisterCallback(mControllerCompatCallback);
+ }
+ if (mBrowserCompat != null) {
+ mBrowserCompat.disconnect();
+ mBrowserCompat = null;
+ }
+ if (mControllerCompat != null) {
+ mControllerCompat.unregisterCallback(mControllerCompatCallback);
+ mControllerCompat = null;
+ }
+ mConnected = false;
+ }
+ mCallbackExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onDisconnected(mInstance);
+ }
+ });
+ }
+
+ @Override
+ public @NonNull SessionToken2 getSessionToken() {
+ return mToken;
+ }
+
+ @Override
+ public boolean isConnected() {
+ synchronized (mLock) {
+ return mConnected;
+ }
+ }
+
+ @Override
+ public void play() {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ sendCommand(COMMAND_CODE_PLAYBACK_PLAY);
+ }
+ }
+
+ @Override
+ public void pause() {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ sendCommand(COMMAND_CODE_PLAYBACK_PAUSE);
+ }
+ }
+
+ @Override
+ public void reset() {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ sendCommand(COMMAND_CODE_PLAYBACK_RESET);
+ }
+ }
+
+ @Override
+ public void prepare() {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ sendCommand(COMMAND_CODE_PLAYBACK_PREPARE);
+ }
+ }
+
+ @Override
+ public void fastForward() {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ sendCommand(COMMAND_CODE_SESSION_FAST_FORWARD);
+ }
+ }
+
+ @Override
+ public void rewind() {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ sendCommand(COMMAND_CODE_SESSION_REWIND);
+ }
+ }
+
+ @Override
+ public void seekTo(long pos) {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putLong(ARGUMENT_SEEK_POSITION, pos);
+ sendCommand(COMMAND_CODE_PLAYBACK_SEEK_TO, args);
+ }
+ }
+
+ @Override
+ public void skipForward() {
+ // To match with KEYCODE_MEDIA_SKIP_FORWARD
+ }
+
+ @Override
+ public void skipBackward() {
+ // To match with KEYCODE_MEDIA_SKIP_BACKWARD
+ }
+
+ @Override
+ public void playFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putString(ARGUMENT_MEDIA_ID, mediaId);
+ args.putBundle(ARGUMENT_EXTRAS, extras);
+ sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID, args);
+ }
+ }
+
+ @Override
+ public void playFromSearch(@NonNull String query, @Nullable Bundle extras) {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putString(ARGUMENT_QUERY, query);
+ args.putBundle(ARGUMENT_EXTRAS, extras);
+ sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_SEARCH, args);
+ }
+ }
+
+ @Override
+ public void playFromUri(@NonNull Uri uri, @Nullable Bundle extras) {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putParcelable(ARGUMENT_URI, uri);
+ args.putBundle(ARGUMENT_EXTRAS, extras);
+ sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_URI, args);
+ }
+ }
+
+ @Override
+ public void prepareFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putString(ARGUMENT_MEDIA_ID, mediaId);
+ args.putBundle(ARGUMENT_EXTRAS, extras);
+ sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID, args);
+ }
+ }
+
+ @Override
+ public void prepareFromSearch(@NonNull String query, @Nullable Bundle extras) {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putString(ARGUMENT_QUERY, query);
+ args.putBundle(ARGUMENT_EXTRAS, extras);
+ sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH, args);
+ }
+ }
+
+ @Override
+ public void prepareFromUri(@NonNull Uri uri, @Nullable Bundle extras) {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putParcelable(ARGUMENT_URI, uri);
+ args.putBundle(ARGUMENT_EXTRAS, extras);
+ sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_URI, args);
+ }
+ }
+
+ @Override
+ public void setVolumeTo(int value, @VolumeFlags int flags) {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putInt(ARGUMENT_VOLUME, value);
+ args.putInt(ARGUMENT_VOLUME_FLAGS, flags);
+ sendCommand(COMMAND_CODE_VOLUME_SET_VOLUME, args);
+ }
+ }
+
+ @Override
+ public void adjustVolume(@VolumeDirection int direction, @VolumeFlags int flags) {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putInt(ARGUMENT_VOLUME_DIRECTION, direction);
+ args.putInt(ARGUMENT_VOLUME_FLAGS, flags);
+ sendCommand(COMMAND_CODE_VOLUME_ADJUST_VOLUME, args);
+ }
+ }
+
+ @Override
+ public @Nullable PendingIntent getSessionActivity() {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return null;
+ }
+ return mControllerCompat.getSessionActivity();
+ }
+ }
+
+ @Override
+ public int getPlayerState() {
+ synchronized (mLock) {
+ return mPlayerState;
+ }
+ }
+
+ @Override
+ public long getDuration() {
+ synchronized (mLock) {
+ if (mMediaMetadataCompat != null
+ && mMediaMetadataCompat.containsKey(METADATA_KEY_DURATION)) {
+ return mMediaMetadataCompat.getLong(METADATA_KEY_DURATION);
+ }
+ }
+ return MediaPlayerInterface.UNKNOWN_TIME;
+ }
+
+ @Override
+ public long getCurrentPosition() {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return UNKNOWN_TIME;
+ }
+ if (mPlaybackStateCompat != null) {
+ long timeDiff = (mInstance.mTimeDiff != null) ? mInstance.mTimeDiff
+ : SystemClock.elapsedRealtime()
+ - mPlaybackStateCompat.getLastPositionUpdateTime();
+ long expectedPosition = mPlaybackStateCompat.getPosition()
+ + (long) (mPlaybackStateCompat.getPlaybackSpeed() * timeDiff);
+ return Math.max(0, expectedPosition);
+ }
+ return UNKNOWN_TIME;
+ }
+ }
+
+ @Override
+ public float getPlaybackSpeed() {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return 0f;
+ }
+ return (mPlaybackStateCompat == null) ? 0f : mPlaybackStateCompat.getPlaybackSpeed();
+ }
+ }
+
+ @Override
+ public void setPlaybackSpeed(float speed) {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putFloat(ARGUMENT_PLAYBACK_SPEED, speed);
+ sendCommand(COMMAND_CODE_PLAYBACK_SET_SPEED, args);
+ }
+ }
+
+ @Override
+ public @MediaPlayerInterface.BuffState int getBufferingState() {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return BUFFERING_STATE_UNKNOWN;
+ }
+ return mBufferingState;
+ }
+ }
+
+ @Override
+ public long getBufferedPosition() {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return UNKNOWN_TIME;
+ }
+ return (mPlaybackStateCompat == null) ? UNKNOWN_TIME
+ : mPlaybackStateCompat.getBufferedPosition();
+ }
+ }
+
+ @Override
+ public @Nullable PlaybackInfo getPlaybackInfo() {
+ synchronized (mLock) {
+ return mPlaybackInfo;
+ }
+ }
+
+ @Override
+ public void setRating(@NonNull String mediaId, @NonNull Rating2 rating) {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putString(ARGUMENT_MEDIA_ID, mediaId);
+ args.putBundle(ARGUMENT_RATING, rating.toBundle());
+ sendCommand(COMMAND_CODE_SESSION_SET_RATING, args);
+ }
+ }
+
+ @Override
+ public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args,
+ @Nullable ResultReceiver cb) {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ Bundle bundle = new Bundle();
+ bundle.putBundle(ARGUMENT_CUSTOM_COMMAND, command.toBundle());
+ bundle.putBundle(ARGUMENT_ARGUMENTS, args);
+ sendCommand(CONTROLLER_COMMAND_BY_CUSTOM_COMMAND, bundle, cb);
+ }
+ }
+
+ @Override
+ public @Nullable List<MediaItem2> getPlaylist() {
+ synchronized (mLock) {
+ return mPlaylist;
+ }
+ }
+
+ @Override
+ public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
+ if (list == null) {
+ throw new IllegalArgumentException("list shouldn't be null");
+ }
+ Bundle args = new Bundle();
+ args.putParcelableArray(ARGUMENT_PLAYLIST, MediaUtils2.toMediaItem2ParcelableArray(list));
+ args.putBundle(ARGUMENT_PLAYLIST_METADATA, metadata == null ? null : metadata.toBundle());
+ sendCommand(COMMAND_CODE_PLAYLIST_SET_LIST, args);
+ }
+
+ @Override
+ public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
+ Bundle args = new Bundle();
+ args.putBundle(ARGUMENT_PLAYLIST_METADATA, metadata == null ? null : metadata.toBundle());
+ sendCommand(COMMAND_CODE_PLAYLIST_SET_LIST_METADATA, args);
+ }
+
+ @Override
+ public @Nullable MediaMetadata2 getPlaylistMetadata() {
+ synchronized (mLock) {
+ return mPlaylistMetadata;
+ }
+ }
+
+ @Override
+ public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
+ Bundle args = new Bundle();
+ args.putInt(ARGUMENT_PLAYLIST_INDEX, index);
+ args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
+ sendCommand(COMMAND_CODE_PLAYLIST_ADD_ITEM, args);
+ }
+
+ @Override
+ public void removePlaylistItem(@NonNull MediaItem2 item) {
+ Bundle args = new Bundle();
+ args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
+ sendCommand(COMMAND_CODE_PLAYLIST_REMOVE_ITEM, args);
+ }
+
+ @Override
+ public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
+ Bundle args = new Bundle();
+ args.putInt(ARGUMENT_PLAYLIST_INDEX, index);
+ args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
+ sendCommand(COMMAND_CODE_PLAYLIST_REPLACE_ITEM, args);
+ }
+
+ @Override
+ public MediaItem2 getCurrentMediaItem() {
+ synchronized (mLock) {
+ return mCurrentMediaItem;
+ }
+ }
+
+ @Override
+ public void skipToPreviousItem() {
+ sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM);
+ }
+
+ @Override
+ public void skipToNextItem() {
+ sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM);
+ }
+
+ @Override
+ public void skipToPlaylistItem(@NonNull MediaItem2 item) {
+ Bundle args = new Bundle();
+ args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
+ sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM, args);
+ }
+
+ @Override
+ public @RepeatMode int getRepeatMode() {
+ synchronized (mLock) {
+ return mRepeatMode;
+ }
+ }
+
+ @Override
+ public void setRepeatMode(@RepeatMode int repeatMode) {
+ Bundle args = new Bundle();
+ args.putInt(ARGUMENT_REPEAT_MODE, repeatMode);
+ sendCommand(COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE, args);
+ }
+
+ @Override
+ public @ShuffleMode int getShuffleMode() {
+ synchronized (mLock) {
+ return mShuffleMode;
+ }
+ }
+
+ @Override
+ public void setShuffleMode(@ShuffleMode int shuffleMode) {
+ Bundle args = new Bundle();
+ args.putInt(ARGUMENT_SHUFFLE_MODE, shuffleMode);
+ sendCommand(COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE, args);
+ }
+
+ @Override
+ public void subscribeRoutesInfo() {
+ sendCommand(COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO);
+ }
+
+ @Override
+ public void unsubscribeRoutesInfo() {
+ sendCommand(COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO);
+ }
+
+ @Override
+ public void selectRoute(@NonNull Bundle route) {
+ if (route == null) {
+ throw new IllegalArgumentException("route shouldn't be null");
+ }
+ Bundle args = new Bundle();
+ args.putBundle(ARGUMENT_ROUTE_BUNDLE, route);
+ sendCommand(COMMAND_CODE_SESSION_SELECT_ROUTE, args);
+ }
+
+ @Override
+ public @NonNull Context getContext() {
+ return mContext;
+ }
+
+ @Override
+ public @NonNull ControllerCallback getCallback() {
+ return mCallback;
+ }
+
+ @Override
+ public @NonNull Executor getCallbackExecutor() {
+ return mCallbackExecutor;
+ }
+
+ @Override
+ public @Nullable MediaBrowserCompat getBrowserCompat() {
+ synchronized (mLock) {
+ return mBrowserCompat;
+ }
+ }
+
+ // Should be used without a lock to prevent potential deadlock.
+ void onConnectedNotLocked(Bundle data) {
+ data.setClassLoader(MediaSession2.class.getClassLoader());
+ // is enough or should we pass it while connecting?
+ final SessionCommandGroup2 allowedCommands = SessionCommandGroup2.fromBundle(
+ data.getBundle(ARGUMENT_ALLOWED_COMMANDS));
+ final int playerState = data.getInt(ARGUMENT_PLAYER_STATE);
+ final int bufferingState = data.getInt(ARGUMENT_BUFFERING_STATE);
+ final PlaybackStateCompat playbackStateCompat = data.getParcelable(
+ ARGUMENT_PLAYBACK_STATE_COMPAT);
+ final int repeatMode = data.getInt(ARGUMENT_REPEAT_MODE);
+ final int shuffleMode = data.getInt(ARGUMENT_SHUFFLE_MODE);
+ final List<MediaItem2> playlist = MediaUtils2.fromMediaItem2ParcelableArray(
+ data.getParcelableArray(ARGUMENT_PLAYLIST));
+ final MediaItem2 currentMediaItem = MediaItem2.fromBundle(
+ data.getBundle(ARGUMENT_MEDIA_ITEM));
+ final PlaybackInfo playbackInfo =
+ PlaybackInfo.fromBundle(data.getBundle(ARGUMENT_PLAYBACK_INFO));
+ final MediaMetadata2 metadata = MediaMetadata2.fromBundle(
+ data.getBundle(ARGUMENT_PLAYLIST_METADATA));
+ if (DEBUG) {
+ Log.d(TAG, "onConnectedNotLocked sessionCompatToken=" + mToken.getSessionCompatToken()
+ + ", allowedCommands=" + allowedCommands);
+ }
+ boolean close = false;
+ try {
+ synchronized (mLock) {
+ if (mIsReleased) {
+ return;
+ }
+ if (mConnected) {
+ Log.e(TAG, "Cannot be notified about the connection result many times."
+ + " Probably a bug or malicious app.");
+ close = true;
+ return;
+ }
+ mAllowedCommands = allowedCommands;
+ mPlayerState = playerState;
+ mBufferingState = bufferingState;
+ mPlaybackStateCompat = playbackStateCompat;
+ mRepeatMode = repeatMode;
+ mShuffleMode = shuffleMode;
+ mPlaylist = playlist;
+ mCurrentMediaItem = currentMediaItem;
+ mPlaylistMetadata = metadata;
+ mConnected = true;
+ mPlaybackInfo = playbackInfo;
+ }
+ mCallbackExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ // Note: We may trigger ControllerCallbacks with the initial values
+ // But it's hard to define the order of the controller callbacks
+ // Only notify about the
+ mCallback.onConnected(mInstance, allowedCommands);
+ }
+ });
+ } finally {
+ if (close) {
+ // Trick to call release() without holding the lock, to prevent potential deadlock
+ // with the developer's custom lock within the ControllerCallback.onDisconnected().
+ close();
+ }
+ }
+ }
+
+ private void initialize() {
+ if (mToken.getType() == SessionToken2.TYPE_SESSION) {
+ synchronized (mLock) {
+ mBrowserCompat = null;
+ }
+ connectToSession(mToken.getSessionCompatToken());
+ } else {
+ connectToService();
+ }
+ }
+
+ private void connectToSession(MediaSessionCompat.Token sessionCompatToken) {
+ MediaControllerCompat controllerCompat = null;
+ try {
+ controllerCompat = new MediaControllerCompat(mContext, sessionCompatToken);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ synchronized (mLock) {
+ mControllerCompat = controllerCompat;
+ mControllerCompatCallback = new ControllerCompatCallback();
+ mControllerCompat.registerCallback(mControllerCompatCallback, mHandler);
+ }
+
+ if (controllerCompat.isSessionReady()) {
+ sendCommand(CONTROLLER_COMMAND_CONNECT, new ResultReceiver(mHandler) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (!mHandlerThread.isAlive()) {
+ return;
+ }
+ switch (resultCode) {
+ case CONNECT_RESULT_CONNECTED:
+ onConnectedNotLocked(resultData);
+ break;
+ case CONNECT_RESULT_DISCONNECTED:
+ mCallbackExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onDisconnected(mInstance);
+ }
+ });
+ close();
+ break;
+ }
+ }
+ });
+ }
+ }
+
+ private void connectToService() {
+ mCallbackExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mLock) {
+ mBrowserCompat = new MediaBrowserCompat(mContext, mToken.getComponentName(),
+ new ConnectionCallback(), sDefaultRootExtras);
+ mBrowserCompat.connect();
+ }
+ }
+ });
+ }
+
+ private void sendCommand(int commandCode) {
+ sendCommand(commandCode, null);
+ }
+
+ private void sendCommand(int commandCode, Bundle args) {
+ if (args == null) {
+ args = new Bundle();
+ }
+ args.putInt(ARGUMENT_COMMAND_CODE, commandCode);
+ sendCommand(CONTROLLER_COMMAND_BY_COMMAND_CODE, args, null);
+ }
+
+ private void sendCommand(String command) {
+ sendCommand(command, null, null);
+ }
+
+ private void sendCommand(String command, ResultReceiver receiver) {
+ sendCommand(command, null, receiver);
+ }
+
+ private void sendCommand(String command, Bundle args, ResultReceiver receiver) {
+ if (args == null) {
+ args = new Bundle();
+ }
+ MediaControllerCompat controller;
+ ControllerCompatCallback callback;
+ synchronized (mLock) {
+ controller = mControllerCompat;
+ callback = mControllerCompatCallback;
+ }
+ BundleCompat.putBinder(args, ARGUMENT_ICONTROLLER_CALLBACK,
+ callback.getIControllerCallback().asBinder());
+ args.putString(ARGUMENT_PACKAGE_NAME, mContext.getPackageName());
+ args.putInt(ARGUMENT_UID, Process.myUid());
+ args.putInt(ARGUMENT_PID, Process.myPid());
+ controller.sendCommand(command, args, receiver);
+ }
+
+ private class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
+ @Override
+ public void onConnected() {
+ MediaBrowserCompat browser = getBrowserCompat();
+ if (browser != null) {
+ connectToSession(browser.getSessionToken());
+ } else if (DEBUG) {
+ Log.d(TAG, "Controller is closed prematually", new IllegalStateException());
+ }
+ }
+
+ @Override
+ public void onConnectionSuspended() {
+ close();
+ }
+
+ @Override
+ public void onConnectionFailed() {
+ close();
+ }
+ }
+
+ private final class ControllerCompatCallback extends MediaControllerCompat.Callback {
+ @Override
+ public void onSessionReady() {
+ sendCommand(CONTROLLER_COMMAND_CONNECT, new ResultReceiver(mHandler) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (!mHandlerThread.isAlive()) {
+ return;
+ }
+ switch (resultCode) {
+ case CONNECT_RESULT_CONNECTED:
+ onConnectedNotLocked(resultData);
+ break;
+ case CONNECT_RESULT_DISCONNECTED:
+ mCallbackExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onDisconnected(mInstance);
+ }
+ });
+ close();
+ break;
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onSessionDestroyed() {
+ close();
+ }
+
+ @Override
+ public void onPlaybackStateChanged(PlaybackStateCompat state) {
+ synchronized (mLock) {
+ mPlaybackStateCompat = state;
+ }
+ }
+
+ @Override
+ public void onMetadataChanged(MediaMetadataCompat metadata) {
+ synchronized (mLock) {
+ mMediaMetadataCompat = metadata;
+ }
+ }
+
+ @Override
+ public void onSessionEvent(String event, Bundle extras) {
+ if (extras != null) {
+ extras.setClassLoader(MediaSession2.class.getClassLoader());
+ }
+ switch (event) {
+ case SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED: {
+ final SessionCommandGroup2 allowedCommands = SessionCommandGroup2.fromBundle(
+ extras.getBundle(ARGUMENT_ALLOWED_COMMANDS));
+ synchronized (mLock) {
+ mAllowedCommands = allowedCommands;
+ }
+ mCallbackExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onAllowedCommandsChanged(mInstance, allowedCommands);
+ }
+ });
+ break;
+ }
+ case SESSION_EVENT_ON_PLAYER_STATE_CHANGED: {
+ final int playerState = extras.getInt(ARGUMENT_PLAYER_STATE);
+ PlaybackStateCompat state =
+ extras.getParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT);
+ if (state == null) {
+ return;
+ }
+ synchronized (mLock) {
+ mPlayerState = playerState;
+ mPlaybackStateCompat = state;
+ }
+ mCallbackExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onPlayerStateChanged(mInstance, playerState);
+ }
+ });
+ break;
+ }
+ case SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED: {
+ final MediaItem2 item = MediaItem2.fromBundle(
+ extras.getBundle(ARGUMENT_MEDIA_ITEM));
+ synchronized (mLock) {
+ mCurrentMediaItem = item;
+ }
+ mCallbackExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onCurrentMediaItemChanged(mInstance, item);
+ }
+ });
+ break;
+ }
+ case SESSION_EVENT_ON_ERROR: {
+ final int errorCode = extras.getInt(ARGUMENT_ERROR_CODE);
+ final Bundle errorExtras = extras.getBundle(ARGUMENT_EXTRAS);
+ mCallbackExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onError(mInstance, errorCode, errorExtras);
+ }
+ });
+ break;
+ }
+ case SESSION_EVENT_ON_ROUTES_INFO_CHANGED: {
+ final List<Bundle> routes = MediaUtils2.toBundleList(
+ extras.getParcelableArray(ARGUMENT_ROUTE_BUNDLE));
+ mCallbackExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onRoutesInfoChanged(mInstance, routes);
+ }
+ });
+ break;
+ }
+ case SESSION_EVENT_ON_PLAYLIST_CHANGED: {
+ final MediaMetadata2 playlistMetadata = MediaMetadata2.fromBundle(
+ extras.getBundle(ARGUMENT_PLAYLIST_METADATA));
+ final List<MediaItem2> playlist = MediaUtils2.fromMediaItem2ParcelableArray(
+ extras.getParcelableArray(ARGUMENT_PLAYLIST));
+ synchronized (mLock) {
+ mPlaylist = playlist;
+ mPlaylistMetadata = playlistMetadata;
+ }
+ mCallbackExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onPlaylistChanged(mInstance, playlist, playlistMetadata);
+ }
+ });
+ break;
+ }
+ case SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED: {
+ final MediaMetadata2 playlistMetadata = MediaMetadata2.fromBundle(
+ extras.getBundle(ARGUMENT_PLAYLIST_METADATA));
+ synchronized (mLock) {
+ mPlaylistMetadata = playlistMetadata;
+ }
+ mCallbackExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onPlaylistMetadataChanged(mInstance, playlistMetadata);
+ }
+ });
+ break;
+ }
+ case SESSION_EVENT_ON_REPEAT_MODE_CHANGED: {
+ final int repeatMode = extras.getInt(ARGUMENT_REPEAT_MODE);
+ synchronized (mLock) {
+ mRepeatMode = repeatMode;
+ }
+ mCallbackExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onRepeatModeChanged(mInstance, repeatMode);
+ }
+ });
+ break;
+ }
+ case SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED: {
+ final int shuffleMode = extras.getInt(ARGUMENT_SHUFFLE_MODE);
+ synchronized (mLock) {
+ mShuffleMode = shuffleMode;
+ }
+ mCallbackExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onShuffleModeChanged(mInstance, shuffleMode);
+ }
+ });
+ break;
+ }
+ case SESSION_EVENT_SEND_CUSTOM_COMMAND: {
+ Bundle commandBundle = extras.getBundle(ARGUMENT_CUSTOM_COMMAND);
+ if (commandBundle == null) {
+ return;
+ }
+ final SessionCommand2 command = SessionCommand2.fromBundle(commandBundle);
+ final Bundle args = extras.getBundle(ARGUMENT_ARGUMENTS);
+ final ResultReceiver receiver = extras.getParcelable(ARGUMENT_RESULT_RECEIVER);
+ mCallbackExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onCustomCommand(mInstance, command, args, receiver);
+ }
+ });
+ break;
+ }
+ case SESSION_EVENT_SET_CUSTOM_LAYOUT: {
+ final List<CommandButton> layout = MediaUtils2.fromCommandButtonParcelableArray(
+ extras.getParcelableArray(ARGUMENT_COMMAND_BUTTONS));
+ if (layout == null) {
+ return;
+ }
+ mCallbackExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onCustomLayoutChanged(mInstance, layout);
+ }
+ });
+ break;
+ }
+ case SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED: {
+ final PlaybackInfo info = PlaybackInfo.fromBundle(
+ extras.getBundle(ARGUMENT_PLAYBACK_INFO));
+ if (info == null) {
+ return;
+ }
+ synchronized (mLock) {
+ mPlaybackInfo = info;
+ }
+ mCallbackExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onPlaybackInfoChanged(mInstance, info);
+ }
+ });
+ break;
+ }
+ case SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED: {
+ final PlaybackStateCompat state =
+ extras.getParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT);
+ if (state == null) {
+ return;
+ }
+ synchronized (mLock) {
+ mPlaybackStateCompat = state;
+ }
+ mCallbackExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onPlaybackSpeedChanged(mInstance, state.getPlaybackSpeed());
+ }
+ });
+ break;
+ }
+ case SESSION_EVENT_ON_BUFFERING_STATE_CHANGED: {
+ final MediaItem2 item = MediaItem2.fromBundle(
+ extras.getBundle(ARGUMENT_MEDIA_ITEM));
+ final int bufferingState = extras.getInt(ARGUMENT_BUFFERING_STATE);
+ PlaybackStateCompat state =
+ extras.getParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT);
+ if (item == null || state == null) {
+ return;
+ }
+ synchronized (mLock) {
+ mBufferingState = bufferingState;
+ mPlaybackStateCompat = state;
+ }
+ mCallbackExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onBufferingStateChanged(mInstance, item, bufferingState);
+ }
+ });
+ break;
+ }
+ case SESSION_EVENT_ON_SEEK_COMPLETED: {
+ final long position = extras.getLong(ARGUMENT_SEEK_POSITION);
+ PlaybackStateCompat state =
+ extras.getParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT);
+ if (state == null) {
+ return;
+ }
+ synchronized (mLock) {
+ mPlaybackStateCompat = state;
+ }
+ mCallbackExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onSeekCompleted(mInstance, position);
+ }
+ });
+ break;
+ }
+ case SESSION_EVENT_ON_CHILDREN_CHANGED: {
+ String parentId = extras.getString(ARGUMENT_MEDIA_ID);
+ if (parentId == null || !(mInstance instanceof MediaBrowser2)) {
+ return;
+ }
+ int itemCount = extras.getInt(ARGUMENT_ITEM_COUNT, -1);
+ Bundle childrenExtras = extras.getBundle(ARGUMENT_EXTRAS);
+ ((MediaBrowser2.BrowserCallback) mCallback).onChildrenChanged(
+ (MediaBrowser2) mInstance, parentId, itemCount, childrenExtras);
+ break;
+ }
+ case SESSION_EVENT_ON_SEARCH_RESULT_CHANGED: {
+ final String query = extras.getString(ARGUMENT_QUERY);
+ if (query == null || !(mInstance instanceof MediaBrowser2)) {
+ return;
+ }
+ final int itemCount = extras.getInt(ARGUMENT_ITEM_COUNT, -1);
+ final Bundle searchExtras = extras.getBundle(ARGUMENT_EXTRAS);
+ mCallbackExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ ((MediaBrowser2.BrowserCallback) mCallback).onSearchResultChanged(
+ (MediaBrowser2) mInstance, query, itemCount, searchExtras);
+ }
+ });
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/media/src/main/java/androidx/media/MediaLibraryService2.java b/media/src/main/java/androidx/media/MediaLibraryService2.java
index edd97c3..9e3bfda 100644
--- a/media/src/main/java/androidx/media/MediaLibraryService2.java
+++ b/media/src/main/java/androidx/media/MediaLibraryService2.java
@@ -19,28 +19,27 @@
import static android.support.v4.media.MediaBrowserCompat.EXTRA_PAGE;
import static android.support.v4.media.MediaBrowserCompat.EXTRA_PAGE_SIZE;
-import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static androidx.media.MediaConstants2.ARGUMENT_EXTRAS;
+import static androidx.media.MediaConstants2.ARGUMENT_PAGE;
+import static androidx.media.MediaConstants2.ARGUMENT_PAGE_SIZE;
import android.app.PendingIntent;
import android.content.Intent;
+import android.os.BadParcelableException;
import android.os.Bundle;
import android.os.IBinder;
import android.support.v4.media.MediaBrowserCompat.MediaItem;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
import androidx.media.MediaLibraryService2.MediaLibrarySession.Builder;
import androidx.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback;
import androidx.media.MediaSession2.ControllerInfo;
-import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
/**
- * @hide
* Base class for media library services.
* <p>
* Media library services enable applications to browse media content provided by an application
@@ -62,7 +61,6 @@
*
* @see MediaSessionService2
*/
-@RestrictTo(LIBRARY_GROUP)
public abstract class MediaLibraryService2 extends MediaSessionService2 {
/**
* This is the interface name that a service implementing a session service should say that it
@@ -70,8 +68,6 @@
*/
public static final String SERVICE_INTERFACE = "android.media.MediaLibraryService2";
- // TODO: Revisit this value.
-
/**
* Session for the {@link MediaLibraryService2}. Build this object with
* {@link Builder} and return in {@link #onCreateSession(String)}.
@@ -170,7 +166,6 @@
* @param parentId parent id
* @see SessionCommand2#COMMAND_CODE_LIBRARY_UNSUBSCRIBE
*/
- // TODO: Make this to be called.
public void onUnsubscribe(@NonNull MediaLibrarySession session,
@NonNull ControllerInfo controller, @NonNull String parentId) {
}
@@ -217,6 +212,8 @@
*/
// Override all methods just to show them with the type instead of generics in Javadoc.
// This workarounds javadoc issue described in the MediaSession2.BuilderBase.
+ // Note: Don't override #setSessionCallback() because the callback can be set by the
+ // constructor.
public static final class Builder extends MediaSession2.BuilderBase<MediaLibrarySession,
Builder, MediaLibrarySessionCallback> {
private MediaLibrarySessionImplBase.Builder mImpl;
@@ -235,7 +232,7 @@
}
@Override
- public @NonNull Builder setPlayer(@NonNull MediaPlayerBase player) {
+ public @NonNull Builder setPlayer(@NonNull MediaPlayerInterface player) {
return super.setPlayer(player);
}
@@ -261,12 +258,6 @@
}
@Override
- public @NonNull Builder setSessionCallback(@NonNull Executor executor,
- @NonNull MediaLibrarySessionCallback callback) {
- return super.setSessionCallback(executor, callback);
- }
-
- @Override
public @NonNull MediaLibrarySession build() {
return super.build();
}
@@ -291,9 +282,10 @@
*/
public void notifyChildrenChanged(@NonNull ControllerInfo controller,
@NonNull String parentId, int itemCount, @Nullable Bundle extras) {
- Bundle options = new Bundle(extras);
- options.putInt(MediaBrowser2.EXTRA_ITEM_COUNT, itemCount);
- options.putBundle(MediaBrowser2.EXTRA_TARGET, controller.toBundle());
+ List<MediaSessionManager.RemoteUserInfo> subscribingBrowsers =
+ getServiceCompat().getSubscribingBrowsers(parentId);
+ getImpl().notifyChildrenChanged(controller, parentId, itemCount, extras,
+ subscribingBrowsers);
}
/**
@@ -308,9 +300,11 @@
// This is for the backward compatibility.
public void notifyChildrenChanged(@NonNull String parentId, int itemCount,
@Nullable Bundle extras) {
- Bundle options = new Bundle(extras);
- options.putInt(MediaBrowser2.EXTRA_ITEM_COUNT, itemCount);
- getServiceCompat().notifyChildrenChanged(parentId, options);
+ if (extras == null) {
+ getServiceCompat().notifyChildrenChanged(parentId);
+ } else {
+ getServiceCompat().notifyChildrenChanged(parentId, extras);
+ }
}
/**
@@ -322,8 +316,8 @@
* @param extras extra bundle
*/
public void notifySearchResultChanged(@NonNull ControllerInfo controller,
- @NonNull String query, int itemCount, @NonNull Bundle extras) {
- // TODO: Implement
+ @NonNull String query, int itemCount, @Nullable Bundle extras) {
+ getImpl().notifySearchResultChanged(controller, query, itemCount, extras);
}
private MediaLibraryService2 getService() {
@@ -494,10 +488,7 @@
// controller.
return sDefaultBrowserRoot;
}
- final CountDownLatch latch = new CountDownLatch(1);
- // TODO: Revisit this when we support caller information.
- final ControllerInfo info = new ControllerInfo(MediaLibraryService2.this, clientUid, -1,
- clientPackageName, null);
+ final ControllerInfo controller = getController();
MediaLibrarySession session = getLibrarySession();
// Call onGetLibraryRoot() directly instead of execute on the executor. Here's the
// reason.
@@ -511,7 +502,7 @@
// Because of the reason, just call onGetLibraryRoot directly here. onGetLibraryRoot()
// has documentation that it may be called on the main thread.
LibraryRoot libraryRoot = session.getCallback().onGetLibraryRoot(
- session, info, extras);
+ session, controller, extras);
if (libraryRoot == null) {
return null;
}
@@ -526,36 +517,47 @@
@Override
public void onLoadChildren(final String parentId, final Result<List<MediaItem>> result,
final Bundle options) {
+ result.detach();
final ControllerInfo controller = getController();
getLibrarySession().getCallbackExecutor().execute(new Runnable() {
@Override
public void run() {
- int page = options.getInt(EXTRA_PAGE, -1);
- int pageSize = options.getInt(EXTRA_PAGE_SIZE, -1);
- if (page >= 0 && pageSize >= 0) {
- // Requesting the list of children through the pagenation.
- List<MediaItem2> children = getLibrarySession().getCallback().onGetChildren(
- getLibrarySession(), controller, parentId, page, pageSize, options);
- if (children == null) {
- result.sendError(null);
- } else {
- List<MediaItem> list = new ArrayList<>();
- for (int i = 0; i < children.size(); i++) {
- list.add(MediaUtils2.createMediaItem(children.get(i)));
+ if (options != null) {
+ options.setClassLoader(MediaLibraryService2.this.getClassLoader());
+ try {
+ int page = options.getInt(EXTRA_PAGE);
+ int pageSize = options.getInt(EXTRA_PAGE_SIZE);
+ if (page > 0 && pageSize > 0) {
+ // Requesting the list of children through pagination.
+ List<MediaItem2> children = getLibrarySession().getCallback()
+ .onGetChildren(getLibrarySession(), controller, parentId,
+ page, pageSize, options);
+ result.sendResult(MediaUtils2.fromMediaItem2List(children));
+ return;
+ } else if (options.containsKey(
+ MediaBrowser2.MEDIA_BROWSER2_SUBSCRIBE)) {
+ // This onLoadChildren() was triggered by MediaBrowser2.subscribe().
+ options.remove(MediaBrowser2.MEDIA_BROWSER2_SUBSCRIBE);
+ getLibrarySession().getCallback().onSubscribe(getLibrarySession(),
+ controller, parentId, options.getBundle(ARGUMENT_EXTRAS));
+ return;
}
- result.sendResult(list);
+ } catch (BadParcelableException e) {
+ // pass-through.
}
- } else {
- // Only wants to register callbacks
- getLibrarySession().getCallback().onSubscribe(getLibrarySession(),
- controller, parentId, options);
}
+ List<MediaItem2> children = getLibrarySession().getCallback()
+ .onGetChildren(getLibrarySession(), controller, parentId,
+ 1 /* page */, Integer.MAX_VALUE /* pageSize*/,
+ null /* extras */);
+ result.sendResult(MediaUtils2.fromMediaItem2List(children));
}
});
}
@Override
public void onLoadItem(final String itemId, final Result<MediaItem> result) {
+ result.detach();
final ControllerInfo controller = getController();
getLibrarySession().getCallbackExecutor().execute(new Runnable() {
@Override
@@ -563,7 +565,7 @@
MediaItem2 item = getLibrarySession().getCallback().onGetItem(
getLibrarySession(), controller, itemId);
if (item == null) {
- result.sendError(null);
+ result.sendResult(null);
} else {
result.sendResult(MediaUtils2.createMediaItem(item));
}
@@ -572,17 +574,65 @@
}
@Override
- public void onSearch(String query, Bundle extras, Result<List<MediaItem>> result) {
- // TODO: Implement
+ public void onSearch(final String query, final Bundle extras,
+ final Result<List<MediaItem>> result) {
+ result.detach();
+ final ControllerInfo controller = getController();
+ extras.setClassLoader(MediaLibraryService2.this.getClassLoader());
+ try {
+ final int page = extras.getInt(ARGUMENT_PAGE);
+ final int pageSize = extras.getInt(ARGUMENT_PAGE_SIZE);
+ if (!(page > 0 && pageSize > 0)) {
+ getLibrarySession().getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ getLibrarySession().getCallback().onSearch(
+ getLibrarySession(), controller, query, extras);
+ }
+ });
+ } else {
+ getLibrarySession().getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ List<MediaItem2> searchResult = getLibrarySession().getCallback()
+ .onGetSearchResult(getLibrarySession(), controller, query,
+ page, pageSize, extras);
+ if (searchResult == null) {
+ result.sendResult(null);
+ return;
+ }
+ result.sendResult(MediaUtils2.fromMediaItem2List(searchResult));
+ }
+ });
+ }
+ } catch (BadParcelableException e) {
+ // Do nothing.
+ }
}
@Override
public void onCustomAction(String action, Bundle extras, Result<Bundle> result) {
- // TODO: Implement
+ // No-op. Library session will handle the custom action.
}
private ControllerInfo getController() {
- // TODO: Implement, by using getBrowserRootHints() / getCurrentBrowserInfo() / ...
+ MediaLibrarySession session = getLibrarySession();
+ List<ControllerInfo> controllers = session.getConnectedControllers();
+
+ MediaSessionManager.RemoteUserInfo info = getCurrentBrowserInfo();
+ if (info == null) {
+ return null;
+ }
+
+ for (int i = 0; i < controllers.size(); i++) {
+ // Note: This cannot pick the right controller between two controllers in same
+ // process.
+ ControllerInfo controller = controllers.get(i);
+ if (controller.getPackageName().equals(info.getPackageName())
+ && controller.getUid() == info.getUid()) {
+ return controller;
+ }
+ }
return null;
}
}
diff --git a/media/src/main/java/androidx/media/MediaLibrarySessionImplBase.java b/media/src/main/java/androidx/media/MediaLibrarySessionImplBase.java
index c21edd3..dfaa98c 100644
--- a/media/src/main/java/androidx/media/MediaLibrarySessionImplBase.java
+++ b/media/src/main/java/androidx/media/MediaLibrarySessionImplBase.java
@@ -31,7 +31,7 @@
class MediaLibrarySessionImplBase extends MediaSession2ImplBase {
MediaLibrarySessionImplBase(Context context,
MediaSessionCompat sessionCompat, String id,
- MediaPlayerBase player, MediaPlaylistAgent playlistAgent,
+ MediaPlayerInterface player, MediaPlaylistAgent playlistAgent,
VolumeProviderCompat volumeProvider, PendingIntent sessionActivity,
Executor callbackExecutor,
MediaSession2.SessionCallback callback) {
@@ -39,6 +39,11 @@
callbackExecutor, callback);
}
+ @Override
+ MediaSession2 createInstance() {
+ return new MediaLibrarySession(this);
+ }
+
static final class Builder extends MediaSession2ImplBase.BuilderBase<
MediaLibrarySession, MediaLibrarySession.MediaLibrarySessionCallback> {
Builder(Context context) {
diff --git a/media/src/main/java/androidx/media/MediaPlayer2.java b/media/src/main/java/androidx/media/MediaPlayer2.java
index 1864d72..4edb946 100644
--- a/media/src/main/java/androidx/media/MediaPlayer2.java
+++ b/media/src/main/java/androidx/media/MediaPlayer2.java
@@ -28,6 +28,7 @@
import android.media.MediaTimestamp;
import android.media.PlaybackParams;
import android.media.ResourceBusyException;
+import android.media.SubtitleData;
import android.media.SyncParams;
import android.media.TimedMetaData;
import android.media.UnsupportedSchemeException;
@@ -47,27 +48,16 @@
import java.util.UUID;
import java.util.concurrent.Executor;
-
/**
- * @hide
- * MediaPlayer2 class can be used to control playback
- * of audio/video files and streams. An example on how to use the methods in
- * this class can be found in {@link android.widget.VideoView}.
+ * MediaPlayer2 class can be used to control playback of audio/video files and streams.
*
* <p>Topics covered here are:
* <ol>
* <li><a href="#StateDiagram">State Diagram</a>
- * <li><a href="#Valid_and_Invalid_States">Valid and Invalid States</a>
* <li><a href="#Permissions">Permissions</a>
* <li><a href="#Callbacks">Register informational and error callbacks</a>
* </ol>
*
- * <div class="special reference">
- * <h3>Developer Guides</h3>
- * <p>For more information about how to use MediaPlayer2, read the
- * <a href="{@docRoot}guide/topics/media/mediaplayer.html">Media Playback</a> developer guide.</p>
- * </div>
- *
* <a name="StateDiagram"></a>
* <h3>State Diagram</h3>
*
@@ -87,18 +77,18 @@
* <p>From this state diagram, one can see that a MediaPlayer2 object has the
* following states:</p>
* <ul>
- * <li>When a MediaPlayer2 object is just created using <code>create</code> or
- * after {@link #reset()} is called, it is in the <em>Idle</em> state; and after
- * {@link #close()} is called, it is in the <em>End</em> state. Between these
- * two states is the life cycle of the MediaPlayer2 object.
+ * <li>When a MediaPlayer2 object is just created using {@link #create()} or
+ * after {@link #reset()} is called, it is in the <strong>Idle</strong> state; and Once
+ * {@link #close()} is called, it can no longer be used and there is no way to bring it
+ * back to any other state.
* <ul>
- * <li> It is a programming error to invoke methods such
- * as {@link #getCurrentPosition()},
- * {@link #getDuration()}, {@link #getVideoHeight()},
- * {@link #getVideoWidth()}, {@link #setAudioAttributes(AudioAttributes)},
- * {@link #setPlayerVolume(float)}, {@link #pause()}, {@link #play()},
- * {@link #seekTo(long, int)} or
- * {@link #prepare()} in the <em>Idle</em> state.
+ * <li>Calling {@link #setDataSource(DataSourceDesc)} and {@link #prepare()} transfers a
+ * MediaPlayer2 object in the <strong>Idle</strong> state to the <strong>Paused</strong>
+ * state. It is good programming practice to register a event callback for
+ * {@link MediaPlayer2EventCallback#onCallCompleted} and
+ * look out for {@link #CALL_STATUS_BAD_VALUE} and {@link #CALL_STATUS_ERROR_IO} that may be
+ * caused from {@link #setDataSource}.
+ * </li>
* <li>It is also recommended that once
* a MediaPlayer2 object is no longer being used, call {@link #close()} immediately
* so that resources used by the internal player engine associated with the
@@ -106,13 +96,7 @@
* singleton resources such as hardware acceleration components and
* failure to call {@link #close()} may cause subsequent instances of
* MediaPlayer2 objects to fallback to software implementations or fail
- * altogether. Once the MediaPlayer2
- * object is in the <em>End</em> state, it can no longer be used and
- * there is no way to bring it back to any other state. </li>
- * <li>Furthermore,
- * the MediaPlayer2 objects created using <code>new</code> is in the
- * <em>Idle</em> state.
- * </li>
+ * altogether. </li>
* </ul>
* </li>
* <li>In general, some playback control operation may fail due to various
@@ -121,71 +105,55 @@
* Thus, error reporting and recovery is an important concern under
* these circumstances. Sometimes, due to programming errors, invoking a playback
* control operation in an invalid state may also occur. Under all these
- * error conditions, the internal player engine invokes a user supplied
- * MediaPlayer2EventCallback.onError() method if an MediaPlayer2EventCallback has been
- * registered beforehand via
- * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.
+ * error conditions, the player goes to <strong>Error</strong> state and invokes a user
+ * supplied {@link MediaPlayer2EventCallback#onError}} method if an event callback has been
+ * registered beforehand via {@link #setMediaPlayer2EventCallback}.
* <ul>
* <li>It is important to note that once an error occurs, the
- * MediaPlayer2 object enters the <em>Error</em> state (except as noted
- * above), even if an error listener has not been registered by the application.</li>
- * <li>In order to reuse a MediaPlayer2 object that is in the <em>
- * Error</em> state and recover from the error,
- * {@link #reset()} can be called to restore the object to its <em>Idle</em>
+ * MediaPlayer2 object enters the <strong>Error</strong> state (except as noted
+ * above), even if a callback has not been registered by the application.</li>
+ * <li>In order to reuse a MediaPlayer2 object that is in the <strong>
+ * Error</strong> state and recover from the error,
+ * {@link #reset()} can be called to restore the object to its <strong>Idle</strong>
* state.</li>
* <li>It is good programming practice to have your application
- * register a OnErrorListener to look out for error notifications from
+ * register a {@link MediaPlayer2EventCallback} to look out for error callbacks from
* the internal player engine.</li>
- * <li>IllegalStateException is
- * thrown to prevent programming errors such as calling
- * {@link #prepare()}, {@link #setDataSource(DataSourceDesc)}
- * methods in an invalid state. </li>
+ * <li> {@link MediaPlayer2EventCallback#onCallCompleted} is called with
+ * {@link #CALL_STATUS_INVALID_OPERATION} on programming errors such as calling
+ * {@link #prepare()} and {@link #setDataSource(DataSourceDesc)} methods in an invalid
+ * state. </li>
* </ul>
* </li>
- * <li>Calling
- * {@link #setDataSource(DataSourceDesc)} transfers a
- * MediaPlayer2 object in the <em>Idle</em> state to the
- * <em>Initialized</em> state.
- * <ul>
- * <li>An IllegalStateException is thrown if
- * setDataSource() is called in any other state.</li>
- * <li>It is good programming
- * practice to always look out for <code>IllegalArgumentException</code>
- * and <code>IOException</code> that may be thrown from
- * <code>setDataSource</code>.</li>
- * </ul>
- * </li>
- * <li>A MediaPlayer2 object must first enter the <em>Prepared</em> state
+ * <li>A MediaPlayer2 object must first enter the <strong>Paused</strong> state
* before playback can be started.
* <ul>
- * <li>There are an asynchronous way that the <em>Prepared</em> state can be reached:
- * a call to {@link #prepare()} (asynchronous) which
- * first transfers the object to the <em>Preparing</em> state after the
- * call returns (which occurs almost right way) while the internal
- * player engine continues working on the rest of preparation work
- * until the preparation work completes. When the preparation completes,
+ * <li>The <strong>Paused</strong> state can be reached by calling {@link #prepare()}. Note
+ * that {@link #prepare()} is asynchronous. When the preparation completes,
* the internal player engine then calls a user supplied callback method,
- * onInfo() of the MediaPlayer2EventCallback interface with {@link #MEDIA_INFO_PREPARED},
- * if an MediaPlayer2EventCallback is registered beforehand via
+ * {@link MediaPlayer2EventCallback#onInfo} interface with {@link #MEDIA_INFO_PREPARED},
+ * if a MediaPlayer2EventCallback is registered beforehand via
* {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.</li>
- * <li>It is important to note that
- * the <em>Preparing</em> state is a transient state, and the behavior
- * of calling any method with side effect while a MediaPlayer2 object is
- * in the <em>Preparing</em> state is undefined.</li>
- * <li>An IllegalStateException is
- * thrown if {@link #prepare()} is called in
- * any other state.</li>
- * <li>While in the <em>Prepared</em> state, properties
- * such as audio/sound volume, screenOnWhilePlaying, looping can be
- * adjusted by invoking the corresponding set methods.</li>
+ * <li>The player also goes to <strong>Paused</strong> state when {@link #pause()} is called
+ * to pause the ongoing playback. Note that {@link #pause()} is asynchronous. Once
+ * {@link #pause()} is processed successfully by the internal media engine,
+ * <strong>Paused</strong> state will be notified with
+ * {@link MediaPlayerInterface.PlayerEventCallback#onPlayerStateChanged} callback.
+ * In addition to the callback, {@link #getMediaPlayer2State()} can also be used to test
+ * whether the MediaPlayer2 object is in the <strong>Paused</strong> state.
+ * </li>
+ * <li>While in the <em>Paused</em> state, properties such as audio/sound volume, looping
+ * can be adjusted by invoking the corresponding set methods.</li>
* </ul>
* </li>
- * <li>To start the playback, {@link #play()} must be called. After
- * {@link #play()} returns successfully, the MediaPlayer2 object is in the
- * <em>Started</em> state. {@link #getPlayerState()} can be called to test
- * whether the MediaPlayer2 object is in the <em>Started</em> state.
+ * <li>To start the playback, {@link #play()} must be called. Once {@link #play()} is processed
+ * successfully by the internal media engine, <strong>Playing</strong> state will be
+ * notified with {@link MediaPlayerInterface.PlayerEventCallback#onPlayerStateChanged}
+ * callback.
+ * In addition to the callback, {@link #getMediaPlayer2State()} can be called to test
+ * whether the MediaPlayer2 object is in the <strong>Started</strong> state.
* <ul>
- * <li>While in the <em>Started</em> state, the internal player engine calls
+ * <li>While in the <strong>Playing</strong> state, the internal player engine calls
* a user supplied callback method MediaPlayer2EventCallback.onInfo() with
* {@link #MEDIA_INFO_BUFFERING_UPDATE} if an MediaPlayer2EventCallback has been
* registered beforehand via
@@ -193,42 +161,21 @@
* This callback allows applications to keep track of the buffering status
* while streaming audio/video.</li>
* <li>Calling {@link #play()} has not effect
- * on a MediaPlayer2 object that is already in the <em>Started</em> state.</li>
+ * on a MediaPlayer2 object that is already in the <strong>Playing</strong> state.</li>
* </ul>
* </li>
- * <li>Playback can be paused and stopped, and the current playback position
- * can be adjusted. Playback can be paused via {@link #pause()}. When the call to
- * {@link #pause()} returns, the MediaPlayer2 object enters the
- * <em>Paused</em> state. Note that the transition from the <em>Started</em>
- * state to the <em>Paused</em> state and vice versa happens
- * asynchronously in the player engine. It may take some time before
- * the state is updated in calls to {@link #getPlayerState()}, and it can be
- * a number of seconds in the case of streamed content.
+ * <li>The playback position can be adjusted with a call to {@link #seekTo}.
* <ul>
- * <li>Calling {@link #play()} to resume playback for a paused
- * MediaPlayer2 object, and the resumed playback
- * position is the same as where it was paused. When the call to
- * {@link #play()} returns, the paused MediaPlayer2 object goes back to
- * the <em>Started</em> state.</li>
- * <li>Calling {@link #pause()} has no effect on
- * a MediaPlayer2 object that is already in the <em>Paused</em> state.</li>
- * </ul>
- * </li>
- * <li>The playback position can be adjusted with a call to
- * {@link #seekTo(long, int)}.
- * <ul>
- * <li>Although the asynchronuous {@link #seekTo(long, int)}
- * call returns right away, the actual seek operation may take a while to
+ * <li>Although the asynchronous {@link #seekTo} call returns right away,
+ * the actual seek operation may take a while to
* finish, especially for audio/video being streamed. When the actual
* seek operation completes, the internal player engine calls a user
* supplied MediaPlayer2EventCallback.onCallCompleted() with
* {@link #CALL_COMPLETED_SEEK_TO}
* if an MediaPlayer2EventCallback has been registered beforehand via
* {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.</li>
- * <li>Please
- * note that {@link #seekTo(long, int)} can also be called in the other states,
- * such as <em>Prepared</em>, <em>Paused</em> and <em>PlaybackCompleted
- * </em> state. When {@link #seekTo(long, int)} is called in those states,
+ * <li>Please note that {@link #seekTo(long, int)} can also be called in
+ * <strong>Paused</strong> state. When {@link #seekTo(long, int)} is called in those states,
* one video frame will be displayed if the stream has video and the requested
* position is valid.
* </li>
@@ -241,206 +188,20 @@
* <li>When the playback reaches the end of stream, the playback completes.
* <ul>
* <li>If current source is set to loop by {@link #loopCurrent(boolean)},
- * the MediaPlayer2 object shall remain in the <em>Started</em> state.</li>
+ * the MediaPlayer2 object shall remain in the <strong>Playing</strong> state.</li>
* <li>If the looping mode was set to <var>false
* </var>, the player engine calls a user supplied callback method,
- * MediaPlayer2EventCallback.onCompletion(), if an MediaPlayer2EventCallback is
- * registered beforehand via
+ * {@link MediaPlayer2EventCallback#onInfo} with {@link #MEDIA_INFO_PLAYBACK_COMPLETE},
+ * if an MediaPlayer2EventCallback is registered beforehand via
* {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.
- * The invoke of the callback signals that the object is now in the <em>
- * PlaybackCompleted</em> state.</li>
- * <li>While in the <em>PlaybackCompleted</em>
- * state, calling {@link #play()} can restart the playback from the
- * beginning of the audio/video source.</li>
+ * The invoke of the callback signals that the object is now in the <strong>Paused</strong>
+ * state.</li>
+ * <li>While in the <strong>Paused</strong> state, calling {@link #play()} can restart the
+ * playback from the beginning of the audio/video source.</li>
* </ul>
*
- *
- * <a name="Valid_and_Invalid_States"></a>
- * <h3>Valid and invalid states</h3>
- *
- * <table border="0" cellspacing="0" cellpadding="0">
- * <tr><td>Method Name </p></td>
- * <td>Valid Sates </p></td>
- * <td>Invalid States </p></td>
- * <td>Comments </p></td></tr>
- * <tr><td>attachAuxEffect </p></td>
- * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td>
- * <td>{Idle, Error} </p></td>
- * <td>This method must be called after setDataSource.
- * Calling it does not change the object state. </p></td></tr>
- * <tr><td>getAudioSessionId </p></td>
- * <td>any </p></td>
- * <td>{} </p></td>
- * <td>This method can be called in any state and calling it does not change
- * the object state. </p></td></tr>
- * <tr><td>getCurrentPosition </p></td>
- * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
- * PlaybackCompleted} </p></td>
- * <td>{Error}</p></td>
- * <td>Successful invoke of this method in a valid state does not change the
- * state. Calling this method in an invalid state transfers the object
- * to the <em>Error</em> state. </p></td></tr>
- * <tr><td>getDuration </p></td>
- * <td>{Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td>
- * <td>{Idle, Initialized, Error} </p></td>
- * <td>Successful invoke of this method in a valid state does not change the
- * state. Calling this method in an invalid state transfers the object
- * to the <em>Error</em> state. </p></td></tr>
- * <tr><td>getVideoHeight </p></td>
- * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
- * PlaybackCompleted}</p></td>
- * <td>{Error}</p></td>
- * <td>Successful invoke of this method in a valid state does not change the
- * state. Calling this method in an invalid state transfers the object
- * to the <em>Error</em> state. </p></td></tr>
- * <tr><td>getVideoWidth </p></td>
- * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
- * PlaybackCompleted}</p></td>
- * <td>{Error}</p></td>
- * <td>Successful invoke of this method in a valid state does not change
- * the state. Calling this method in an invalid state transfers the
- * object to the <em>Error</em> state. </p></td></tr>
- * <tr><td>getPlayerState </p></td>
- * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
- * PlaybackCompleted}</p></td>
- * <td>{Error}</p></td>
- * <td>Successful invoke of this method in a valid state does not change
- * the state. Calling this method in an invalid state transfers the
- * object to the <em>Error</em> state. </p></td></tr>
- * <tr><td>pause </p></td>
- * <td>{Started, Paused, PlaybackCompleted}</p></td>
- * <td>{Idle, Initialized, Prepared, Stopped, Error}</p></td>
- * <td>Successful invoke of this method in a valid state transfers the
- * object to the <em>Paused</em> state. Calling this method in an
- * invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
- * <tr><td>prepare </p></td>
- * <td>{Initialized, Stopped} </p></td>
- * <td>{Idle, Prepared, Started, Paused, PlaybackCompleted, Error} </p></td>
- * <td>Successful invoke of this method in a valid state transfers the
- * object to the <em>Preparing</em> state. Calling this method in an
- * invalid state throws an IllegalStateException.</p></td></tr>
- * <tr><td>release </p></td>
- * <td>any </p></td>
- * <td>{} </p></td>
- * <td>After {@link #close()}, the object is no longer available. </p></td></tr>
- * <tr><td>reset </p></td>
- * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
- * PlaybackCompleted, Error}</p></td>
- * <td>{}</p></td>
- * <td>After {@link #reset()}, the object is like being just created.</p></td></tr>
- * <tr><td>seekTo </p></td>
- * <td>{Prepared, Started, Paused, PlaybackCompleted} </p></td>
- * <td>{Idle, Initialized, Stopped, Error}</p></td>
- * <td>Successful invoke of this method in a valid state does not change
- * the state. Calling this method in an invalid state transfers the
- * object to the <em>Error</em> state. </p></td></tr>
- * <tr><td>setAudioAttributes </p></td>
- * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
- * PlaybackCompleted}</p></td>
- * <td>{Error}</p></td>
- * <td>Successful invoke of this method does not change the state. In order for the
- * target audio attributes type to become effective, this method must be called before
- * prepare().</p></td></tr>
- * <tr><td>setAudioSessionId </p></td>
- * <td>{Idle} </p></td>
- * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
- * Error} </p></td>
- * <td>This method must be called in idle state as the audio session ID must be known before
- * calling setDataSource. Calling it does not change the object
- * state. </p></td></tr>
- * <tr><td>setAudioStreamType (deprecated)</p></td>
- * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
- * PlaybackCompleted}</p></td>
- * <td>{Error}</p></td>
- * <td>Successful invoke of this method does not change the state. In order for the
- * target audio stream type to become effective, this method must be called before
- * prepare().</p></td></tr>
- * <tr><td>setAuxEffectSendLevel </p></td>
- * <td>any</p></td>
- * <td>{} </p></td>
- * <td>Calling this method does not change the object state. </p></td></tr>
- * <tr><td>setDataSource </p></td>
- * <td>{Idle} </p></td>
- * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
- * Error} </p></td>
- * <td>Successful invoke of this method in a valid state transfers the
- * object to the <em>Initialized</em> state. Calling this method in an
- * invalid state throws an IllegalStateException.</p></td></tr>
- * <tr><td>setDisplay </p></td>
- * <td>any </p></td>
- * <td>{} </p></td>
- * <td>This method can be called in any state and calling it does not change
- * the object state. </p></td></tr>
- * <tr><td>setSurface </p></td>
- * <td>any </p></td>
- * <td>{} </p></td>
- * <td>This method can be called in any state and calling it does not change
- * the object state. </p></td></tr>
- * <tr><td>loopCurrent </p></td>
- * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
- * PlaybackCompleted}</p></td>
- * <td>{Error}</p></td>
- * <td>Successful invoke of this method in a valid state does not change
- * the state. Calling this method in an
- * invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
- * <tr><td>isLooping </p></td>
- * <td>any </p></td>
- * <td>{} </p></td>
- * <td>This method can be called in any state and calling it does not change
- * the object state. </p></td></tr>
- * <tr><td>setDrmEventCallback </p></td>
- * <td>any </p></td>
- * <td>{} </p></td>
- * <td>This method can be called in any state and calling it does not change
- * the object state. </p></td></tr>
- * <tr><td>setMediaPlayer2EventCallback </p></td>
- * <td>any </p></td>
- * <td>{} </p></td>
- * <td>This method can be called in any state and calling it does not change
- * the object state. </p></td></tr>
- * <tr><td>setPlaybackParams</p></td>
- * <td>{Initialized, Prepared, Started, Paused, PlaybackCompleted, Error}</p></td>
- * <td>{Idle, Stopped} </p></td>
- * <td>This method will change state in some cases, depending on when it's called.
- * </p></td></tr>
- * <tr><td>setPlayerVolume </p></td>
- * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
- * PlaybackCompleted}</p></td>
- * <td>{Error}</p></td>
- * <td>Successful invoke of this method does not change the state.
- * <tr><td>play </p></td>
- * <td>{Prepared, Started, Paused, PlaybackCompleted}</p></td>
- * <td>{Idle, Initialized, Stopped, Error}</p></td>
- * <td>Successful invoke of this method in a valid state transfers the
- * object to the <em>Started</em> state. Calling this method in an
- * invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
- * <tr><td>stop </p></td>
- * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
- * <td>{Idle, Initialized, Error}</p></td>
- * <td>Successful invoke of this method in a valid state transfers the
- * object to the <em>Stopped</em> state. Calling this method in an
- * invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
- * <tr><td>getTrackInfo </p></td>
- * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
- * <td>{Idle, Initialized, Error}</p></td>
- * <td>Successful invoke of this method does not change the state.</p></td></tr>
- * <tr><td>selectTrack </p></td>
- * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
- * <td>{Idle, Initialized, Error}</p></td>
- * <td>Successful invoke of this method does not change the state.</p></td></tr>
- * <tr><td>deselectTrack </p></td>
- * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
- * <td>{Idle, Initialized, Error}</p></td>
- * <td>Successful invoke of this method does not change the state.</p></td></tr>
- *
- * </table>
- *
* <a name="Permissions"></a>
* <h3>Permissions</h3>
- * <p>One may need to declare a corresponding WAKE_LOCK permission {@link
- * android.R.styleable#AndroidManifestUsesPermission <uses-permission>}
- * element.
- *
* <p>This class requires the {@link android.Manifest.permission#INTERNET} permission
* when used with network-based content.
*
@@ -460,8 +221,7 @@
*
*/
@TargetApi(Build.VERSION_CODES.P)
-@RestrictTo(LIBRARY_GROUP)
-public abstract class MediaPlayer2 extends MediaPlayerBase {
+public abstract class MediaPlayer2 {
/**
* Create a MediaPlayer2 object.
*
@@ -478,6 +238,12 @@
public MediaPlayer2() { }
/**
+ * Returns a {@link MediaPlayerInterface} implementation which runs based on
+ * this MediaPlayer2 instance.
+ */
+ public abstract MediaPlayerInterface getMediaPlayerInterface();
+
+ /**
* Releases the resources held by this {@code MediaPlayer2} object.
*
* It is considered good practice to call this method when you're
@@ -495,13 +261,8 @@
* of the same codec are supported, some performance degradation
* may be expected when unnecessary multiple instances are used
* at the same time.
- *
- * {@code close()} may be safely called after a prior {@code close()}.
- * This class implements the Java {@code AutoCloseable} interface and
- * may be used with try-with-resources.
*/
// This is a synchronous call.
- @Override
public abstract void close();
/**
@@ -513,7 +274,6 @@
*
*/
// This is an asynchronous call.
- @Override
public abstract void play();
/**
@@ -524,21 +284,18 @@
*
*/
// This is an asynchronous call.
- @Override
public abstract void prepare();
/**
* Pauses playback. Call play() to resume.
*/
// This is an asynchronous call.
- @Override
public abstract void pause();
/**
* Tries to play next data source if applicable.
*/
// This is an asynchronous call.
- @Override
public abstract void skipToNext();
/**
@@ -548,7 +305,6 @@
* @param msec the offset in milliseconds from the start to seek to
*/
// This is an asynchronous call.
- @Override
public void seekTo(long msec) {
seekTo(msec, SEEK_PREVIOUS_SYNC /* mode */);
}
@@ -558,7 +314,6 @@
*
* @return the current position in milliseconds
*/
- @Override
public abstract long getCurrentPosition();
/**
@@ -567,7 +322,6 @@
* @return the duration in milliseconds, if no duration is available
* (for example, if streaming live content), -1 is returned.
*/
- @Override
public abstract long getDuration();
/**
@@ -579,25 +333,14 @@
*
* @return the current buffered media source position in milliseconds
*/
- @Override
public abstract long getBufferedPosition();
/**
- * Gets the current player state.
+ * Gets the current MediaPlayer2 state.
*
- * @return the current player state.
+ * @return the current MediaPlayer2 state.
*/
- @Override
- public abstract @PlayerState int getPlayerState();
-
- /**
- * Gets the current buffering state of the player.
- * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
- * buffered.
- * @return the buffering state, one of the following:
- */
- @Override
- public abstract @BuffState int getBufferingState();
+ public abstract @MediaPlayer2State int getMediaPlayer2State();
/**
* Sets the audio attributes for this MediaPlayer2.
@@ -607,14 +350,12 @@
* @param attributes a non-null set of audio attributes
*/
// This is an asynchronous call.
- @Override
public abstract void setAudioAttributes(@NonNull AudioAttributesCompat attributes);
/**
* Gets the audio attributes for this MediaPlayer2.
* @return attributes a set of audio attributes
*/
- @Override
public abstract @Nullable AudioAttributesCompat getAudioAttributes();
/**
@@ -623,7 +364,6 @@
* @param dsd the descriptor of data source you want to play
*/
// This is an asynchronous call.
- @Override
public abstract void setDataSource(@NonNull DataSourceDesc dsd);
/**
@@ -633,7 +373,6 @@
* @param dsd the descriptor of data source you want to play after current one
*/
// This is an asynchronous call.
- @Override
public abstract void setNextDataSource(@NonNull DataSourceDesc dsd);
/**
@@ -642,7 +381,6 @@
* @param dsds the list of data sources you want to play after current one
*/
// This is an asynchronous call.
- @Override
public abstract void setNextDataSources(@NonNull List<DataSourceDesc> dsds);
/**
@@ -650,7 +388,6 @@
*
* @return the current DataSourceDesc
*/
- @Override
public abstract @NonNull DataSourceDesc getCurrentDataSource();
/**
@@ -658,7 +395,6 @@
* @param loop true if the current data source is meant to loop.
*/
// This is an asynchronous call.
- @Override
public abstract void loopCurrent(boolean loop);
/**
@@ -671,7 +407,6 @@
* @param speed the desired playback speed
*/
// This is an asynchronous call.
- @Override
public abstract void setPlaybackSpeed(float speed);
/**
@@ -679,7 +414,6 @@
* Note that it may differ from the speed set in {@link #setPlaybackSpeed(float)}.
* @return the actual playback speed
*/
- @Override
public float getPlaybackSpeed() {
return 1.0f;
}
@@ -690,7 +424,6 @@
* {@link #setPlaybackSpeed(float)}.
* @return true if reverse playback is supported.
*/
- @Override
public boolean isReversePlaybackSupported() {
return false;
}
@@ -705,7 +438,6 @@
* @param volume a value between 0.0f and {@link #getMaxPlayerVolume()}.
*/
// This is an asynchronous call.
- @Override
public abstract void setPlayerVolume(float volume);
/**
@@ -713,36 +445,16 @@
* Note that it does not take into account the associated stream volume.
* @return the player volume.
*/
- @Override
public abstract float getPlayerVolume();
/**
* @return the maximum volume that can be used in {@link #setPlayerVolume(float)}.
*/
- @Override
public float getMaxPlayerVolume() {
return 1.0f;
}
/**
- * Adds a callback to be notified of events for this player.
- * @param e the {@link Executor} to be used for the events.
- * @param cb the callback to receive the events.
- */
- // This is a synchronous call.
- @Override
- public abstract void registerPlayerEventCallback(@NonNull Executor e,
- @NonNull PlayerEventCallback cb);
-
- /**
- * Removes a previously registered callback for player events
- * @param cb the callback to remove
- */
- // This is a synchronous call.
- @Override
- public abstract void unregisterPlayerEventCallback(@NonNull PlayerEventCallback cb);
-
- /**
* Insert a task in the command queue to help the client to identify whether a batch
* of commands has been finished. When this command is processed, a notification
* {@code MediaPlayer2EventCallback.onCommandLabelReached} will be fired with the
@@ -832,10 +544,7 @@
*
* Additional vendor-specific fields may also be present in
* the return value.
- * @hide
- * TODO: This method is not ready for public. Currently returns metrics data in MediaPlayer1.
*/
- @RestrictTo(LIBRARY_GROUP)
public abstract PersistableBundle getMetrics();
/**
@@ -930,7 +639,8 @@
/**
* Moves the media to specified time position by considering the given mode.
* <p>
- * When seekTo is finished, the user will be notified via OnSeekComplete supplied by the user.
+ * When seekTo is finished, the user will be notified via
+ * {@link MediaPlayer2EventCallback#onInfo} with {@link #CALL_COMPLETED_SEEK_TO}.
* There is at most one active seekTo processed at any time. If there is a to-be-completed
* seekTo, new seekTo requests will be queued in such a way that only the last request
* is kept. When current seekTo is completed, the queued request will be processed if
@@ -948,7 +658,7 @@
public abstract void seekTo(long msec, @SeekMode int mode);
/**
- * Get current playback position as a {@link MediaTimestamp}.
+ * Gets current playback position as a {@link MediaTimestamp}.
* <p>
* The MediaTimestamp represents how the media time correlates to the system time in
* a linear fashion using an anchor and a clock rate. During regular playback, the media
@@ -974,7 +684,6 @@
* data source and calling prepare().
*/
// This is a synchronous call.
- @Override
public abstract void reset();
/**
@@ -1109,8 +818,9 @@
/**
* Selects a track.
* <p>
- * If a MediaPlayer2 is in invalid state, it throws an IllegalStateException exception.
- * If a MediaPlayer2 is in <em>Started</em> state, the selected track is presented immediately.
+ * If a MediaPlayer2 is in invalid state, {@link #CALL_STATUS_INVALID_OPERATION} will be
+ * reported with {@link MediaPlayer2EventCallback#onCallCompleted}.
+ * If a MediaPlayer2 is in <em>Playing</em> state, the selected track is presented immediately.
* If a MediaPlayer2 is not in Started state, it just marks the track to be played.
* </p>
* <p>
@@ -1124,13 +834,10 @@
* </p>
* <p>
* Currently, only timed text tracks or audio tracks can be selected via this method.
- * In addition, the support for selecting an audio track at runtime is pretty limited
- * in that an audio track can only be selected in the <em>Prepared</em> state.
* </p>
* @param index the index of the track to be selected. The valid range of the index
* is 0..total number of track - 1. The total number of tracks as well as the type of
* each individual track can be found by calling {@link #getTrackInfo()} method.
- * @throws IllegalStateException if called in an invalid state.
*
* @see MediaPlayer2#getTrackInfo
*/
@@ -1138,7 +845,7 @@
public abstract void selectTrack(int index);
/**
- * Deselect a track.
+ * Deselects a track.
* <p>
* Currently, the track must be a timed text track and no audio or video tracks can be
* deselected. If the timed text track identified by index has not been
@@ -1147,7 +854,6 @@
* @param index the index of the track to be deselected. The valid range of the index
* is 0..total number of tracks - 1. The total number of tracks as well as the type of
* each individual track can be found by calling {@link #getTrackInfo()} method.
- * @throws IllegalStateException if called in an invalid state.
*
* @see MediaPlayer2#getTrackInfo
*/
@@ -1229,13 +935,26 @@
@CallStatus int status) { }
/**
- * Called to indicate media clock has changed.
+ * Called when a discontinuity in the normal progression of the media time is detected.
+ * The "normal progression" of media time is defined as the expected increase of the
+ * playback position when playing media, relative to the playback speed (for instance every
+ * second, media time increases by two seconds when playing at 2x).<br>
+ * Discontinuities are encountered in the following cases:
+ * <ul>
+ * <li>when the player is starved for data and cannot play anymore</li>
+ * <li>when the player encounters a playback error</li>
+ * <li>when the a seek operation starts, and when it's completed</li>
+ * <li>when the playback speed changes</li>
+ * <li>when the playback state changes</li>
+ * <li>when the player is reset</li>
+ * </ul>
*
* @param mp the MediaPlayer2 the media time pertains to.
* @param dsd the DataSourceDesc of this data source
- * @param timestamp the new media clock.
+ * @param timestamp the timestamp that correlates media time, system time and clock rate,
+ * or {@link MediaTimestamp#TIMESTAMP_UNKNOWN} in an error case.
*/
- public void onMediaTimeChanged(
+ public void onMediaTimeDiscontinuity(
MediaPlayer2 mp, DataSourceDesc dsd, MediaTimestamp timestamp) { }
/**
@@ -1247,12 +966,14 @@
*/
public void onCommandLabelReached(MediaPlayer2 mp, @NonNull Object label) { }
- /* TODO : uncomment below once API is available in supportlib.
+ /**
* Called when when a player subtitle track has new subtitle data available.
* @param mp the player that reports the new subtitle data
+ * @param dsd the DataSourceDesc of this data source
* @param data the subtitle data
*/
- // public void onSubtitleData(MediaPlayer2 mp, @NonNull SubtitleData data) { }
+ public void onSubtitleData(
+ MediaPlayer2 mp, DataSourceDesc dsd, @NonNull SubtitleData data) { }
}
/**
@@ -1271,6 +992,46 @@
// This is a synchronous call.
public abstract void clearMediaPlayer2EventCallback();
+ /**
+ * MediaPlayer2 has not been prepared or just has been reset.
+ * In this state, MediaPlayer2 doesn't fetch data.
+ */
+ public static final int MEDIAPLAYER2_STATE_IDLE = 1001;
+
+ /**
+ * MediaPlayer2 has been just prepared.
+ * In this state, MediaPlayer2 just fetches data from media source,
+ * but doesn't actively render data.
+ */
+ public static final int MEDIAPLAYER2_STATE_PREPARED = 1002;
+
+ /**
+ * MediaPlayer2 is paused.
+ * In this state, MediaPlayer2 doesn't actively render data.
+ */
+ public static final int MEDIAPLAYER2_STATE_PAUSED = 1003;
+
+ /**
+ * MediaPlayer2 is actively playing back data.
+ */
+ public static final int MEDIAPLAYER2_STATE_PLAYING = 1004;
+
+ /**
+ * MediaPlayer2 has hit some fatal error and cannot continue playback.
+ */
+ public static final int MEDIAPLAYER2_STATE_ERROR = 1005;
+
+ /** @hide */
+ @IntDef(flag = false, value = {
+ MEDIAPLAYER2_STATE_IDLE,
+ MEDIAPLAYER2_STATE_PREPARED,
+ MEDIAPLAYER2_STATE_PAUSED,
+ MEDIAPLAYER2_STATE_PLAYING,
+ MEDIAPLAYER2_STATE_ERROR })
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(LIBRARY_GROUP)
+ public @interface MediaPlayer2State {}
+
/* Do not change these values without updating their counterparts
* in include/media/mediaplayer2.h!
*/
@@ -1331,7 +1092,9 @@
/** The player switched to this datas source because it is the
* next-to-be-played in the playlist.
* @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+ * @hide
*/
+ @RestrictTo(LIBRARY_GROUP)
public static final int MEDIA_INFO_STARTED_AS_NEXT = 2;
/** The player just pushed the very first video frame for rendering.
@@ -1512,16 +1275,6 @@
*/
public static final int CALL_COMPLETED_PREPARE = 6;
- /** The player just completed a call {@link #releaseDrm}.
- * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
- */
- public static final int CALL_COMPLETED_RELEASE_DRM = 12;
-
- /** The player just completed a call {@link #restoreDrmKeys}.
- * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
- */
- public static final int CALL_COMPLETED_RESTORE_DRM_KEYS = 13;
-
/** The player just completed a call {@link #seekTo}.
* @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
*/
@@ -1609,8 +1362,6 @@
CALL_COMPLETED_PAUSE,
CALL_COMPLETED_PLAY,
CALL_COMPLETED_PREPARE,
- CALL_COMPLETED_RELEASE_DRM,
- CALL_COMPLETED_RESTORE_DRM_KEYS,
CALL_COMPLETED_SEEK_TO,
CALL_COMPLETED_SELECT_TRACK,
CALL_COMPLETED_SET_AUDIO_ATTRIBUTES,
@@ -1661,12 +1412,6 @@
*/
public static final int CALL_STATUS_ERROR_IO = 4;
- /** Status code represents that DRM operation is called before preparing a DRM scheme through
- * {@link #prepareDrm}.
- * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
- */
- public static final int CALL_STATUS_NO_DRM_SCHEME = 5;
-
/**
* @hide
*/
@@ -1676,8 +1421,7 @@
CALL_STATUS_INVALID_OPERATION,
CALL_STATUS_BAD_VALUE,
CALL_STATUS_PERMISSION_DENIED,
- CALL_STATUS_ERROR_IO,
- CALL_STATUS_NO_DRM_SCHEME})
+ CALL_STATUS_ERROR_IO})
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(LIBRARY_GROUP)
public @interface CallStatus {}
@@ -2008,4 +1752,100 @@
super(detailMessage);
}
}
+
+ /**
+ * Definitions for the metrics that are reported via the {@link #getMetrics} call.
+ */
+ public static final class MetricsConstants {
+ private MetricsConstants() {}
+
+ /**
+ * Key to extract the MIME type of the video track
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is a String.
+ */
+ public static final String MIME_TYPE_VIDEO = "android.media.mediaplayer.video.mime";
+
+ /**
+ * Key to extract the codec being used to decode the video track
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is a String.
+ */
+ public static final String CODEC_VIDEO = "android.media.mediaplayer.video.codec";
+
+ /**
+ * Key to extract the width (in pixels) of the video track
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is an integer.
+ */
+ public static final String WIDTH = "android.media.mediaplayer.width";
+
+ /**
+ * Key to extract the height (in pixels) of the video track
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is an integer.
+ */
+ public static final String HEIGHT = "android.media.mediaplayer.height";
+
+ /**
+ * Key to extract the count of video frames played
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is an integer.
+ */
+ public static final String FRAMES = "android.media.mediaplayer.frames";
+
+ /**
+ * Key to extract the count of video frames dropped
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is an integer.
+ */
+ public static final String FRAMES_DROPPED = "android.media.mediaplayer.dropped";
+
+ /**
+ * Key to extract the MIME type of the audio track
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is a String.
+ */
+ public static final String MIME_TYPE_AUDIO = "android.media.mediaplayer.audio.mime";
+
+ /**
+ * Key to extract the codec being used to decode the audio track
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is a String.
+ */
+ public static final String CODEC_AUDIO = "android.media.mediaplayer.audio.codec";
+
+ /**
+ * Key to extract the duration (in milliseconds) of the
+ * media being played
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is a long.
+ */
+ public static final String DURATION = "android.media.mediaplayer.durationMs";
+
+ /**
+ * Key to extract the playing time (in milliseconds) of the
+ * media being played
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is a long.
+ */
+ public static final String PLAYING = "android.media.mediaplayer.playingMs";
+
+ /**
+ * Key to extract the count of errors encountered while
+ * playing the media
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is an integer.
+ */
+ public static final String ERRORS = "android.media.mediaplayer.err";
+
+ /**
+ * Key to extract an (optional) error code detected while
+ * playing the media
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is an integer.
+ */
+ public static final String ERROR_CODE = "android.media.mediaplayer.errcode";
+
+ }
}
diff --git a/media/src/main/java/androidx/media/MediaPlayer2Impl.java b/media/src/main/java/androidx/media/MediaPlayer2Impl.java
index 3b3e119..3504876 100644
--- a/media/src/main/java/androidx/media/MediaPlayer2Impl.java
+++ b/media/src/main/java/androidx/media/MediaPlayer2Impl.java
@@ -28,6 +28,7 @@
import android.media.MediaTimestamp;
import android.media.PlaybackParams;
import android.media.ResourceBusyException;
+import android.media.SubtitleData;
import android.media.SyncParams;
import android.media.TimedMetaData;
import android.media.UnsupportedSchemeException;
@@ -36,9 +37,7 @@
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Parcel;
-import android.os.Parcelable;
import android.os.PersistableBundle;
-import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
import android.view.Surface;
@@ -47,7 +46,11 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import androidx.collection.ArrayMap;
import androidx.core.util.Preconditions;
+import androidx.media.MediaPlayerInterface.BuffState;
+import androidx.media.MediaPlayerInterface.PlayerEventCallback;
+import androidx.media.MediaPlayerInterface.PlayerState;
import java.io.IOException;
import java.nio.ByteOrder;
@@ -70,13 +73,15 @@
private static final String TAG = "MediaPlayer2Impl";
- private static final int NEXT_SOURCE_STATE_ERROR = -1;
- private static final int NEXT_SOURCE_STATE_INIT = 0;
- private static final int NEXT_SOURCE_STATE_PREPARING = 1;
- private static final int NEXT_SOURCE_STATE_PREPARED = 2;
+ private static final int SOURCE_STATE_ERROR = -1;
+ private static final int SOURCE_STATE_INIT = 0;
+ private static final int SOURCE_STATE_PREPARING = 1;
+ private static final int SOURCE_STATE_PREPARED = 2;
private static ArrayMap<Integer, Integer> sInfoEventMap;
private static ArrayMap<Integer, Integer> sErrorEventMap;
+ private static ArrayMap<Integer, Integer> sPrepareDrmStatusMap;
+ private static ArrayMap<Integer, Integer> sStateMap;
static {
sInfoEventMap = new ArrayMap<>();
@@ -106,24 +111,29 @@
sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_MALFORMED, MEDIA_ERROR_MALFORMED);
sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_UNSUPPORTED, MEDIA_ERROR_UNSUPPORTED);
sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_TIMED_OUT, MEDIA_ERROR_TIMED_OUT);
+
+ sPrepareDrmStatusMap = new ArrayMap<>();
+ sPrepareDrmStatusMap.put(
+ MediaPlayer.PREPARE_DRM_STATUS_SUCCESS, PREPARE_DRM_STATUS_SUCCESS);
+ sPrepareDrmStatusMap.put(
+ MediaPlayer.PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR,
+ PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR);
+ sPrepareDrmStatusMap.put(
+ MediaPlayer.PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR,
+ PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR);
+ sPrepareDrmStatusMap.put(
+ MediaPlayer.PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR,
+ PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR);
+
+ sStateMap = new ArrayMap<>();
+ sStateMap.put(MEDIAPLAYER2_STATE_IDLE, MediaPlayerInterface.PLAYER_STATE_IDLE);
+ sStateMap.put(MEDIAPLAYER2_STATE_PREPARED, MediaPlayerInterface.PLAYER_STATE_PAUSED);
+ sStateMap.put(MEDIAPLAYER2_STATE_PAUSED, MediaPlayerInterface.PLAYER_STATE_PAUSED);
+ sStateMap.put(MEDIAPLAYER2_STATE_PLAYING, MediaPlayerInterface.PLAYER_STATE_PLAYING);
+ sStateMap.put(MEDIAPLAYER2_STATE_ERROR, MediaPlayerInterface.PLAYER_STATE_ERROR);
}
- private MediaPlayer mPlayer; // MediaPlayer is thread-safe.
-
- private final Object mSrcLock = new Object();
- //--- guarded by |mSrcLock| start
- private long mSrcIdGenerator = 0;
- private DataSourceDesc mCurrentDSD;
- private long mCurrentSrcId = mSrcIdGenerator++;
- private List<DataSourceDesc> mNextDSDs;
- private long mNextSrcId = mSrcIdGenerator++;
- private int mNextSourceState = NEXT_SOURCE_STATE_INIT;
- private boolean mNextSourcePlayPending = false;
- //--- guarded by |mSrcLock| end
-
- private AtomicInteger mBufferedPercentageCurrent = new AtomicInteger(0);
- private AtomicInteger mBufferedPercentageNext = new AtomicInteger(0);
- private volatile float mVolume = 1.0f;
+ private MediaPlayerSourceQueue mPlayer;
private HandlerThread mHandlerThread;
private final Handler mTaskHandler;
@@ -135,8 +145,6 @@
private final Object mLock = new Object();
//--- guarded by |mLock| start
- @PlayerState private int mPlayerState;
- @BuffState private int mBufferingState;
private AudioAttributesCompat mAudioAttributes;
private ArrayList<Pair<Executor, MediaPlayer2EventCallback>> mMp2EventCallbackRecords =
new ArrayList<>();
@@ -144,8 +152,21 @@
new ArrayMap<>();
private ArrayList<Pair<Executor, DrmEventCallback>> mDrmEventCallbackRecords =
new ArrayList<>();
+ private MediaPlayerInterfaceImpl mMediaPlayerInterfaceImpl;
//--- guarded by |mLock| end
+ private void handleDataSourceError(final DataSourceError err) {
+ if (err == null) {
+ return;
+ }
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback callback) {
+ callback.onError(MediaPlayer2Impl.this, err.mDSD, err.mWhat, err.mExtra);
+ }
+ });
+ }
+
/**
* Default constructor.
* <p>When done with the MediaPlayer2Impl, you should call {@link #close()},
@@ -157,10 +178,24 @@
mHandlerThread.start();
Looper looper = mHandlerThread.getLooper();
mTaskHandler = new Handler(looper);
- mPlayer = new MediaPlayer();
- mPlayerState = PLAYER_STATE_IDLE;
- mBufferingState = BUFFERING_STATE_UNKNOWN;
- setUpListeners();
+
+ // TODO: To make sure MediaPlayer1 listeners work, the caller thread should have a looper.
+ // Fix the framework or document this behavior.
+ mPlayer = new MediaPlayerSourceQueue();
+ }
+
+ /**
+ * Returns a {@link MediaPlayerInterface} implementation which runs based on
+ * this MediaPlayer2 instance.
+ */
+ @Override
+ public MediaPlayerInterface getMediaPlayerInterface() {
+ synchronized (mLock) {
+ if (mMediaPlayerInterfaceImpl == null) {
+ mMediaPlayerInterfaceImpl = new MediaPlayerInterfaceImpl();
+ }
+ return mMediaPlayerInterfaceImpl;
+ }
}
/**
@@ -204,8 +239,7 @@
addTask(new Task(CALL_COMPLETED_PLAY, false) {
@Override
void process() {
- mPlayer.start();
- setPlayerState(PLAYER_STATE_PLAYING);
+ mPlayer.play();
}
});
}
@@ -226,7 +260,6 @@
@Override
void process() throws IOException {
mPlayer.prepareAsync();
- setBufferingState(BUFFERING_STATE_BUFFERING_AND_STARVED);
}
});
}
@@ -242,7 +275,6 @@
@Override
void process() {
mPlayer.pause();
- setPlayerState(PLAYER_STATE_PAUSED);
}
});
}
@@ -257,7 +289,7 @@
addTask(new Task(CALL_COMPLETED_SKIP_TO_NEXT, false) {
@Override
void process() {
- // TODO: switch to next data source and play
+ mPlayer.skipToNext();
}
});
}
@@ -295,14 +327,21 @@
@Override
public long getBufferedPosition() {
// Use cached buffered percent for now.
- return getDuration() * mBufferedPercentageCurrent.get() / 100;
+ return mPlayer.getBufferedPosition();
}
+ /**
+ * Gets the current MediaPlayer2 state.
+ *
+ * @return the current MediaPlayer2 state.
+ */
@Override
- public @PlayerState int getPlayerState() {
- synchronized (mLock) {
- return mPlayerState;
- }
+ public @MediaPlayer2State int getMediaPlayer2State() {
+ return mPlayer.getMediaPlayer2State();
+ }
+
+ private @MediaPlayerInterface.PlayerState int getPlayerState() {
+ return mPlayer.getPlayerState();
}
/**
@@ -310,11 +349,8 @@
* During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
* buffered.
*/
- @Override
- public @BuffState int getBufferingState() {
- synchronized (mLock) {
- return mBufferingState;
- }
+ private @MediaPlayerInterface.BuffState int getBufferingState() {
+ return mPlayer.getBufferingState();
}
/**
@@ -361,13 +397,10 @@
void process() {
Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
// TODO: setDataSource could update exist data source
- synchronized (mSrcLock) {
- mCurrentDSD = dsd;
- mCurrentSrcId = mSrcIdGenerator++;
- try {
- handleDataSource(true /* isCurrent */, dsd, mCurrentSrcId);
- } catch (IOException e) {
- }
+ try {
+ mPlayer.setFirst(dsd);
+ } catch (IOException e) {
+ Log.e(TAG, "process: setDataSource", e);
}
}
});
@@ -387,21 +420,7 @@
@Override
void process() {
Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
- synchronized (mSrcLock) {
- mNextDSDs = new ArrayList<DataSourceDesc>(1);
- mNextDSDs.add(dsd);
- mNextSrcId = mSrcIdGenerator++;
- mNextSourceState = NEXT_SOURCE_STATE_INIT;
- mNextSourcePlayPending = false;
- }
- /* FIXME : define and handle state.
- int state = getMediaPlayer2State();
- if (state != MEDIAPLAYER2_STATE_IDLE) {
- synchronized (mSrcLock) {
- prepareNextDataSource_l();
- }
- }
- */
+ handleDataSourceError(mPlayer.setNext(dsd));
}
});
}
@@ -427,30 +446,14 @@
"DataSourceDesc in the source list cannot be null.");
}
}
-
- synchronized (mSrcLock) {
- mNextDSDs = new ArrayList(dsds);
- mNextSrcId = mSrcIdGenerator++;
- mNextSourceState = NEXT_SOURCE_STATE_INIT;
- mNextSourcePlayPending = false;
- }
- /* FIXME : define and handle state.
- int state = getMediaPlayer2State();
- if (state != MEDIAPLAYER2_STATE_IDLE) {
- synchronized (mSrcLock) {
- prepareNextDataSource_l();
- }
- }
- */
+ handleDataSourceError(mPlayer.setNextMultiple(dsds));
}
});
}
@Override
public @NonNull DataSourceDesc getCurrentDataSource() {
- synchronized (mSrcLock) {
- return mCurrentDSD;
- }
+ return mPlayer.getFirst().getDSD();
}
/**
@@ -521,8 +524,7 @@
addTask(new Task(CALL_COMPLETED_SET_PLAYER_VOLUME, false) {
@Override
void process() {
- mVolume = volume;
- mPlayer.setVolume(volume, volume);
+ mPlayer.setVolume(volume);
}
});
}
@@ -534,7 +536,7 @@
*/
@Override
public float getPlayerVolume() {
- return mVolume;
+ return mPlayer.getVolume();
}
/**
@@ -550,8 +552,7 @@
* @param e the {@link Executor} to be used for the events.
* @param cb the callback to receive the events.
*/
- @Override
- public void registerPlayerEventCallback(@NonNull Executor e,
+ private void registerPlayerEventCallback(@NonNull Executor e,
@NonNull PlayerEventCallback cb) {
if (cb == null) {
throw new IllegalArgumentException("Illegal null PlayerEventCallback");
@@ -569,8 +570,7 @@
* Removes a previously registered callback for player events
* @param cb the callback to remove
*/
- @Override
- public void unregisterPlayerEventCallback(@NonNull PlayerEventCallback cb) {
+ private void unregisterPlayerEventCallback(@NonNull PlayerEventCallback cb) {
if (cb == null) {
throw new IllegalArgumentException("Illegal null PlayerEventCallback");
}
@@ -628,7 +628,9 @@
*/
@Override
public void clearPendingCommands() {
- // TODO: implement this.
+ synchronized (mTaskLock) {
+ mPendingTasks.clear();
+ }
}
private void addTask(Task task) {
@@ -650,14 +652,15 @@
}
}
- private void handleDataSource(boolean isCurrent, @NonNull final DataSourceDesc dsd, long srcId)
+ private static void handleDataSource(MediaPlayerSource src)
throws IOException {
+ final DataSourceDesc dsd = src.getDSD();
Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
- // TODO: handle the case isCurrent is false.
+ MediaPlayer player = src.mPlayer;
switch (dsd.getType()) {
case DataSourceDesc.TYPE_CALLBACK:
- mPlayer.setDataSource(new MediaDataSource() {
+ player.setDataSource(new MediaDataSource() {
Media2DataSource mDataSource = dsd.getMedia2DataSource();
@Override
public int readAt(long position, byte[] buffer, int offset, int size)
@@ -678,14 +681,14 @@
break;
case DataSourceDesc.TYPE_FD:
- mPlayer.setDataSource(
+ player.setDataSource(
dsd.getFileDescriptor(),
dsd.getFileDescriptorOffset(),
dsd.getFileDescriptorLength());
break;
case DataSourceDesc.TYPE_URI:
- mPlayer.setDataSource(
+ player.setDataSource(
dsd.getUriContext(),
dsd.getUri(),
dsd.getUriHeaders(),
@@ -871,9 +874,12 @@
@Override
public void reset() {
mPlayer.reset();
- setPlayerState(PLAYER_STATE_IDLE);
- setBufferingState(BUFFERING_STATE_UNKNOWN);
- /* FIXME: reset other internal variables. */
+ synchronized (mLock) {
+ mAudioAttributes = null;
+ mMp2EventCallbackRecords.clear();
+ mPlayerEventCallbackMap.clear();
+ mDrmEventCallbackRecords.clear();
+ }
}
/**
@@ -997,45 +1003,11 @@
return null;
}
- TrackInfoImpl(Parcel in) {
- mTrackType = in.readInt();
- // TODO: parcel in the full MediaFormat; currently we are using createSubtitleFormat
- // even for audio/video tracks, meaning we only set the mime and language.
- String mime = in.readString();
- String language = in.readString();
- mFormat = MediaFormat.createSubtitleFormat(mime, language);
-
- if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
- mFormat.setInteger(MediaFormat.KEY_IS_AUTOSELECT, in.readInt());
- mFormat.setInteger(MediaFormat.KEY_IS_DEFAULT, in.readInt());
- mFormat.setInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE, in.readInt());
- }
- }
-
TrackInfoImpl(int type, MediaFormat format) {
mTrackType = type;
mFormat = format;
}
- /**
- * Flatten this object in to a Parcel.
- *
- * @param dest The Parcel in which the object should be written.
- * @param flags Additional flags about how the object should be written.
- * May be 0 or {@link android.os.Parcelable#PARCELABLE_WRITE_RETURN_VALUE}.
- */
- /* package private */ void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(mTrackType);
- dest.writeString(getLanguage());
-
- if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
- dest.writeString(mFormat.getString(MediaFormat.KEY_MIME));
- dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_AUTOSELECT));
- dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_DEFAULT));
- dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE));
- }
- }
-
@Override
public String toString() {
StringBuilder out = new StringBuilder(128);
@@ -1062,23 +1034,6 @@
out.append("}");
return out.toString();
}
-
- /**
- * Used to read a TrackInfoImpl from a Parcel.
- */
- /* package private */ static final Parcelable.Creator<TrackInfoImpl> CREATOR =
- new Parcelable.Creator<TrackInfoImpl>() {
- @Override
- public TrackInfoImpl createFromParcel(Parcel in) {
- return new TrackInfoImpl(in);
- }
-
- @Override
- public TrackInfoImpl[] newArray(int size) {
- return new TrackInfoImpl[size];
- }
- };
-
};
/**
@@ -1230,8 +1185,9 @@
mPlayer.setOnDrmConfigHelper(new MediaPlayer.OnDrmConfigHelper() {
@Override
public void onDrmConfig(MediaPlayer mp) {
- /** FIXME: pass the right DSD. */
- listener.onDrmConfig(MediaPlayer2Impl.this, null);
+ MediaPlayerSource src = mPlayer.getSourceForPlayer(mp);
+ DataSourceDesc dsd = src == null ? null : src.getDSD();
+ listener.onDrmConfig(MediaPlayer2Impl.this, dsd);
}
});
}
@@ -1340,16 +1296,11 @@
*/
@Override
public void releaseDrm() throws NoDrmSchemeException {
- addTask(new Task(CALL_COMPLETED_RELEASE_DRM, false) {
- @Override
- void process() throws NoDrmSchemeException {
- try {
- mPlayer.releaseDrm();
- } catch (MediaPlayer.NoDrmSchemeException e) {
- throw new NoDrmSchemeException(e.getMessage());
- }
- }
- });
+ try {
+ mPlayer.releaseDrm();
+ } catch (MediaPlayer.NoDrmSchemeException e) {
+ throw new NoDrmSchemeException(e.getMessage());
+ }
}
@@ -1443,16 +1394,11 @@
@Override
public void restoreDrmKeys(@NonNull final byte[] keySetId)
throws NoDrmSchemeException {
- addTask(new Task(CALL_COMPLETED_RESTORE_DRM_KEYS, false) {
- @Override
- void process() throws NoDrmSchemeException {
- try {
- mPlayer.restoreKeys(keySetId);
- } catch (MediaPlayer.NoDrmSchemeException e) {
- throw new NoDrmSchemeException(e.getMessage());
- }
- }
- });
+ try {
+ mPlayer.restoreKeys(keySetId);
+ } catch (MediaPlayer.NoDrmSchemeException e) {
+ throw new NoDrmSchemeException(e.getMessage());
+ }
}
@@ -1504,46 +1450,16 @@
private void setPlaybackParamsInternal(final PlaybackParams params) {
PlaybackParams current = mPlayer.getPlaybackParams();
mPlayer.setPlaybackParams(params);
- if (Math.abs(current.getSpeed() - params.getSpeed()) > 0.0001f) {
+ if (current.getSpeed() != params.getSpeed()) {
notifyPlayerEvent(new PlayerEventNotifier() {
@Override
public void notify(PlayerEventCallback cb) {
- cb.onPlaybackSpeedChanged(MediaPlayer2Impl.this, params.getSpeed());
+ cb.onPlaybackSpeedChanged(mMediaPlayerInterfaceImpl, params.getSpeed());
}
});
}
}
- private void setPlayerState(@PlayerState final int state) {
- synchronized (mLock) {
- if (mPlayerState == state) {
- return;
- }
- mPlayerState = state;
- }
- notifyPlayerEvent(new PlayerEventNotifier() {
- @Override
- public void notify(PlayerEventCallback cb) {
- cb.onPlayerStateChanged(MediaPlayer2Impl.this, state);
- }
- });
- }
-
- private void setBufferingState(@BuffState final int state) {
- synchronized (mLock) {
- if (mBufferingState == state) {
- return;
- }
- mBufferingState = state;
- }
- notifyPlayerEvent(new PlayerEventNotifier() {
- @Override
- public void notify(PlayerEventCallback cb) {
- cb.onBufferingStateChanged(MediaPlayer2Impl.this, mCurrentDSD, state);
- }
- });
- }
-
private void notifyMediaPlayer2Event(final Mp2EventNotifier notifier) {
List<Pair<Executor, MediaPlayer2EventCallback>> records;
synchronized (mLock) {
@@ -1577,6 +1493,21 @@
}
}
+ private void notifyDrmEvent(final DrmEventNotifier notifier) {
+ List<Pair<Executor, DrmEventCallback>> records;
+ synchronized (mLock) {
+ records = new ArrayList<>(mDrmEventCallbackRecords);
+ }
+ for (final Pair<Executor, DrmEventCallback> record : records) {
+ record.first.execute(new Runnable() {
+ @Override
+ public void run() {
+ notifier.notify(record.second);
+ }
+ });
+ }
+ }
+
private interface Mp2EventNotifier {
void notify(MediaPlayer2EventCallback callback);
}
@@ -1585,28 +1516,34 @@
void notify(PlayerEventCallback callback);
}
- private void setUpListeners() {
- mPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
+ private interface DrmEventNotifier {
+ void notify(DrmEventCallback callback);
+ }
+
+ private void setUpListeners(final MediaPlayerSource src) {
+ MediaPlayer p = src.mPlayer;
+ p.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
- setPlayerState(PLAYER_STATE_PAUSED);
- setBufferingState(BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
+ handleDataSourceError(mPlayer.onPrepared(mp));
notifyMediaPlayer2Event(new Mp2EventNotifier() {
@Override
public void notify(MediaPlayer2EventCallback callback) {
- callback.onInfo(MediaPlayer2Impl.this, mCurrentDSD, MEDIA_INFO_PREPARED, 0);
+ MediaPlayer2Impl mp2 = MediaPlayer2Impl.this;
+ DataSourceDesc dsd = src.getDSD();
+ callback.onInfo(mp2, dsd, MEDIA_INFO_PREPARED, 0);
}
});
notifyPlayerEvent(new PlayerEventNotifier() {
@Override
public void notify(PlayerEventCallback cb) {
- cb.onMediaPrepared(MediaPlayer2Impl.this, mCurrentDSD);
+ cb.onMediaPrepared(mMediaPlayerInterfaceImpl, src.getDSD());
}
});
synchronized (mTaskLock) {
if (mCurrentTask != null
&& mCurrentTask.mMediaCallType == CALL_COMPLETED_PREPARE
- && mCurrentTask.mDSD == mCurrentDSD
+ && mCurrentTask.mDSD == src.getDSD()
&& mCurrentTask.mNeedToWaitForEventToComplete) {
mCurrentTask.sendCompleteNotification(CALL_STATUS_NO_ERROR);
mCurrentTask = null;
@@ -1615,18 +1552,18 @@
}
}
});
- mPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
+ p.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
@Override
public void onVideoSizeChanged(MediaPlayer mp, final int width, final int height) {
notifyMediaPlayer2Event(new Mp2EventNotifier() {
@Override
public void notify(MediaPlayer2EventCallback cb) {
- cb.onVideoSizeChanged(MediaPlayer2Impl.this, mCurrentDSD, width, height);
+ cb.onVideoSizeChanged(MediaPlayer2Impl.this, src.getDSD(), width, height);
}
});
}
});
- mPlayer.setOnInfoListener(new MediaPlayer.OnInfoListener() {
+ p.setOnInfoListener(new MediaPlayer.OnInfoListener() {
@Override
public boolean onInfo(MediaPlayer mp, int what, int extra) {
switch (what) {
@@ -1634,50 +1571,52 @@
notifyMediaPlayer2Event(new Mp2EventNotifier() {
@Override
public void notify(MediaPlayer2EventCallback cb) {
- cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD,
+ cb.onInfo(MediaPlayer2Impl.this, src.getDSD(),
MEDIA_INFO_VIDEO_RENDERING_START, 0);
}
});
break;
case MediaPlayer.MEDIA_INFO_BUFFERING_START:
- setBufferingState(BUFFERING_STATE_BUFFERING_AND_STARVED);
+ mPlayer.setBufferingState(
+ mp, MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_STARVED);
break;
case MediaPlayer.MEDIA_INFO_BUFFERING_END:
- setBufferingState(BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
+ mPlayer.setBufferingState(
+ mp, MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
break;
}
return false;
}
});
- mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
+ p.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
- setPlayerState(PLAYER_STATE_PAUSED);
+ handleDataSourceError(mPlayer.onCompletion(mp));
notifyMediaPlayer2Event(new Mp2EventNotifier() {
@Override
public void notify(MediaPlayer2EventCallback cb) {
- cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD, MEDIA_INFO_PLAYBACK_COMPLETE,
- 0);
+ MediaPlayer2Impl mp2 = MediaPlayer2Impl.this;
+ DataSourceDesc dsd = src.getDSD();
+ cb.onInfo(mp2, dsd, MEDIA_INFO_PLAYBACK_COMPLETE, 0);
}
});
}
});
- mPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
+ p.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, final int what, final int extra) {
- setPlayerState(PLAYER_STATE_ERROR);
- setBufferingState(BUFFERING_STATE_UNKNOWN);
+ mPlayer.onError(mp);
notifyMediaPlayer2Event(new Mp2EventNotifier() {
@Override
public void notify(MediaPlayer2EventCallback cb) {
int w = sErrorEventMap.getOrDefault(what, MEDIA_ERROR_UNKNOWN);
- cb.onError(MediaPlayer2Impl.this, mCurrentDSD, w, extra);
+ cb.onError(MediaPlayer2Impl.this, src.getDSD(), w, extra);
}
});
return true;
}
});
- mPlayer.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() {
+ p.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() {
@Override
public void onSeekComplete(MediaPlayer mp) {
synchronized (mTaskLock) {
@@ -1695,12 +1634,12 @@
public void notify(PlayerEventCallback cb) {
// TODO: The actual seeked position might be different from the
// requested position. Clarify which one is expected here.
- cb.onSeekCompleted(MediaPlayer2Impl.this, seekPos);
+ cb.onSeekCompleted(mMediaPlayerInterfaceImpl, seekPos);
}
});
}
});
- mPlayer.setOnTimedMetaDataAvailableListener(
+ p.setOnTimedMetaDataAvailableListener(
new MediaPlayer.OnTimedMetaDataAvailableListener() {
@Override
public void onTimedMetaDataAvailable(MediaPlayer mp, final TimedMetaData data) {
@@ -1708,40 +1647,91 @@
@Override
public void notify(MediaPlayer2EventCallback cb) {
cb.onTimedMetaDataAvailable(
- MediaPlayer2Impl.this, mCurrentDSD, data);
+ MediaPlayer2Impl.this, src.getDSD(), data);
}
});
}
});
- mPlayer.setOnInfoListener(new MediaPlayer.OnInfoListener() {
+ p.setOnInfoListener(new MediaPlayer.OnInfoListener() {
@Override
public boolean onInfo(MediaPlayer mp, final int what, final int extra) {
notifyMediaPlayer2Event(new Mp2EventNotifier() {
@Override
public void notify(MediaPlayer2EventCallback cb) {
int w = sInfoEventMap.getOrDefault(what, MEDIA_INFO_UNKNOWN);
- cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD, w, extra);
+ cb.onInfo(MediaPlayer2Impl.this, src.getDSD(), w, extra);
}
});
return true;
}
});
- mPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
+ p.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
@Override
public void onBufferingUpdate(MediaPlayer mp, final int percent) {
if (percent >= 100) {
- setBufferingState(BUFFERING_STATE_BUFFERING_COMPLETE);
+ mPlayer.setBufferingState(
+ mp, MediaPlayerInterface.BUFFERING_STATE_BUFFERING_COMPLETE);
}
- mBufferedPercentageCurrent.set(percent);
+ src.mBufferedPercentage.set(percent);
notifyMediaPlayer2Event(new Mp2EventNotifier() {
@Override
public void notify(MediaPlayer2EventCallback cb) {
- cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD,
+ cb.onInfo(MediaPlayer2Impl.this, src.getDSD(),
MEDIA_INFO_BUFFERING_UPDATE, percent);
}
});
}
});
+ p.setOnMediaTimeDiscontinuityListener(
+ new MediaPlayer.OnMediaTimeDiscontinuityListener() {
+ @Override
+ public void onMediaTimeDiscontinuity(
+ MediaPlayer mp, final MediaTimestamp timestamp) {
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onMediaTimeDiscontinuity(
+ MediaPlayer2Impl.this, src.getDSD(), timestamp);
+ }
+ });
+ }
+ });
+ p.setOnSubtitleDataListener(new MediaPlayer.OnSubtitleDataListener() {
+ @Override
+ public void onSubtitleData(MediaPlayer mp, final SubtitleData data) {
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onSubtitleData(MediaPlayer2Impl.this, src.getDSD(), data);
+ }
+ });
+ }
+ });
+ p.setOnDrmInfoListener(new MediaPlayer.OnDrmInfoListener() {
+ @Override
+ public void onDrmInfo(MediaPlayer mp, final MediaPlayer.DrmInfo drmInfo) {
+ notifyDrmEvent(new DrmEventNotifier() {
+ @Override
+ public void notify(DrmEventCallback cb) {
+ cb.onDrmInfo(MediaPlayer2Impl.this, src.getDSD(),
+ new DrmInfoImpl(drmInfo.getPssh(), drmInfo.getSupportedSchemes()));
+ }
+ });
+ }
+ });
+ p.setOnDrmPreparedListener(new MediaPlayer.OnDrmPreparedListener() {
+ @Override
+ public void onDrmPrepared(MediaPlayer mp, final int status) {
+ notifyDrmEvent(new DrmEventNotifier() {
+ @Override
+ public void notify(DrmEventCallback cb) {
+ int s = sPrepareDrmStatusMap.getOrDefault(
+ status, PREPARE_DRM_STATUS_PREPARATION_ERROR);
+ cb.onDrmPrepared(MediaPlayer2Impl.this, src.getDSD(), s);
+ }
+ });
+ }
+ });
}
/**
@@ -1944,14 +1934,10 @@
status = CALL_STATUS_PERMISSION_DENIED;
} catch (IOException e) {
status = CALL_STATUS_ERROR_IO;
- } catch (NoDrmSchemeException e) {
- status = CALL_STATUS_NO_DRM_SCHEME;
} catch (Exception e) {
status = CALL_STATUS_ERROR_UNKNOWN;
}
- synchronized (mSrcLock) {
- mDSD = mCurrentDSD;
- }
+ mDSD = getCurrentDataSource();
if (!mNeedToWaitForEventToComplete || status != CALL_STATUS_NO_ERROR) {
@@ -1979,4 +1965,590 @@
});
}
};
+
+ private static class DataSourceError {
+ final DataSourceDesc mDSD;
+ final int mWhat;
+
+ final int mExtra;
+ DataSourceError(DataSourceDesc dsd, int what, int extra) {
+ mDSD = dsd;
+ mWhat = what;
+ mExtra = extra;
+ }
+
+ }
+
+ private class MediaPlayerSource {
+
+ volatile DataSourceDesc mDSD;
+ final MediaPlayer mPlayer = new MediaPlayer();
+ final AtomicInteger mBufferedPercentage = new AtomicInteger(0);
+ int mSourceState = SOURCE_STATE_INIT;
+ @MediaPlayer2State int mMp2State = MEDIAPLAYER2_STATE_IDLE;
+ @BuffState int mBufferingState = MediaPlayerInterface.BUFFERING_STATE_UNKNOWN;
+ @PlayerState int mPlayerState = MediaPlayerInterface.PLAYER_STATE_IDLE;
+ boolean mPlayPending;
+
+ MediaPlayerSource(final DataSourceDesc dsd) {
+ mDSD = dsd;
+ setUpListeners(this);
+ }
+
+ DataSourceDesc getDSD() {
+ return mDSD;
+ }
+
+ }
+
+ private class MediaPlayerSourceQueue {
+
+ List<MediaPlayerSource> mQueue = new ArrayList<>();
+ float mVolume = 1.0f;
+ Surface mSurface;
+
+ MediaPlayerSourceQueue() {
+ mQueue.add(new MediaPlayerSource(null));
+ }
+
+ synchronized MediaPlayer getCurrentPlayer() {
+ return mQueue.get(0).mPlayer;
+ }
+
+ synchronized MediaPlayerSource getFirst() {
+ return mQueue.get(0);
+ }
+
+ synchronized void setFirst(DataSourceDesc dsd) throws IOException {
+ if (mQueue.isEmpty()) {
+ mQueue.add(0, new MediaPlayerSource(dsd));
+ } else {
+ mQueue.get(0).mDSD = dsd;
+ setUpListeners(mQueue.get(0));
+ }
+ handleDataSource(mQueue.get(0));
+ }
+
+ synchronized DataSourceError setNext(DataSourceDesc dsd) {
+ MediaPlayerSource src = new MediaPlayerSource(dsd);
+ if (mQueue.isEmpty()) {
+ mQueue.add(src);
+ return prepareAt(0);
+ } else {
+ mQueue.add(1, src);
+ return prepareAt(1);
+ }
+ }
+
+ synchronized DataSourceError setNextMultiple(List<DataSourceDesc> descs) {
+ List<MediaPlayerSource> sources = new ArrayList<>();
+ for (DataSourceDesc dsd: descs) {
+ sources.add(new MediaPlayerSource(dsd));
+ }
+ if (mQueue.isEmpty()) {
+ mQueue.addAll(sources);
+ return prepareAt(0);
+ } else {
+ mQueue.addAll(1, sources);
+ return prepareAt(1);
+ }
+ }
+
+ synchronized void play() {
+ MediaPlayerSource src = mQueue.get(0);
+ if (src.mSourceState == SOURCE_STATE_PREPARED) {
+ src.mPlayer.start();
+ setMp2State(src.mPlayer, MEDIAPLAYER2_STATE_PLAYING);
+ }
+ }
+
+ synchronized void prepare() {
+ getCurrentPlayer().prepareAsync();
+ }
+
+ synchronized void release() {
+ getCurrentPlayer().release();
+ }
+
+ synchronized void prepareAsync() {
+ MediaPlayer mp = getCurrentPlayer();
+ mp.prepareAsync();
+ setBufferingState(mp, MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_STARVED);
+ }
+
+ synchronized void pause() {
+ MediaPlayer mp = getCurrentPlayer();
+ mp.pause();
+ setMp2State(mp, MEDIAPLAYER2_STATE_PAUSED);
+ }
+
+ synchronized long getCurrentPosition() {
+ return getCurrentPlayer().getCurrentPosition();
+ }
+
+ synchronized long getDuration() {
+ return getCurrentPlayer().getDuration();
+ }
+
+ synchronized long getBufferedPosition() {
+ MediaPlayerSource src = mQueue.get(0);
+ return (long) src.mPlayer.getDuration() * src.mBufferedPercentage.get() / 100;
+ }
+
+ synchronized void setAudioAttributes(AudioAttributes attributes) {
+ getCurrentPlayer().setAudioAttributes(attributes);
+ }
+
+ synchronized DataSourceError onPrepared(MediaPlayer mp) {
+ for (int i = 0; i < mQueue.size(); i++) {
+ MediaPlayerSource src = mQueue.get(i);
+ if (mp == src.mPlayer) {
+ if (i == 0) {
+ if (src.mPlayPending) {
+ src.mPlayPending = false;
+ src.mPlayer.start();
+ setMp2State(src.mPlayer, MEDIAPLAYER2_STATE_PLAYING);
+ } else {
+ setMp2State(src.mPlayer, MEDIAPLAYER2_STATE_PREPARED);
+ }
+ }
+ src.mSourceState = SOURCE_STATE_PREPARED;
+ setBufferingState(src.mPlayer,
+ MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
+ return prepareAt(i + 1);
+ }
+ }
+ return null;
+ }
+
+ synchronized DataSourceError onCompletion(MediaPlayer mp) {
+ if (!mQueue.isEmpty() && mp == getCurrentPlayer()) {
+ if (mQueue.size() == 1) {
+ setMp2State(mp, MEDIAPLAYER2_STATE_PAUSED);
+ return null;
+ }
+ moveToNext();
+ }
+ return playCurrent();
+ }
+
+ synchronized void moveToNext() {
+ final MediaPlayerSource src1 = mQueue.remove(0);
+ src1.mPlayer.release();
+ if (mQueue.isEmpty()) {
+ throw new IllegalStateException("player/source queue emptied");
+ }
+ final MediaPlayerSource src2 = mQueue.get(0);
+ if (src1.mPlayerState != src2.mPlayerState) {
+ notifyPlayerEvent(new PlayerEventNotifier() {
+ @Override
+ public void notify(PlayerEventCallback cb) {
+ cb.onPlayerStateChanged(mMediaPlayerInterfaceImpl, src2.mPlayerState);
+ }
+ });
+ }
+ notifyPlayerEvent(new PlayerEventNotifier() {
+ @Override
+ public void notify(PlayerEventCallback cb) {
+ cb.onCurrentDataSourceChanged(mMediaPlayerInterfaceImpl, src2.mDSD);
+ }
+ });
+ }
+
+ synchronized DataSourceError playCurrent() {
+ DataSourceError err = null;
+ final MediaPlayerSource src = mQueue.get(0);
+ src.mPlayer.setSurface(mSurface);
+ src.mPlayer.setVolume(mVolume, mVolume);
+ if (src.mSourceState == SOURCE_STATE_PREPARED) {
+ // start next source only when it's in prepared state.
+ src.mPlayer.start();
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback callback) {
+ callback.onInfo(MediaPlayer2Impl.this, src.getDSD(),
+ MEDIA_INFO_STARTED_AS_NEXT, 0);
+ }
+ });
+
+ } else {
+ if (src.mSourceState == SOURCE_STATE_INIT) {
+ err = prepareAt(0);
+ }
+ src.mPlayPending = true;
+ }
+ return err;
+ }
+
+ synchronized void onError(MediaPlayer mp) {
+ setMp2State(mp, MEDIAPLAYER2_STATE_ERROR);
+ setBufferingState(mp, MediaPlayerInterface.BUFFERING_STATE_UNKNOWN);
+ }
+
+ synchronized DataSourceError prepareAt(int n) {
+ if (n >= mQueue.size()
+ || mQueue.get(n).mSourceState != SOURCE_STATE_INIT
+ || getPlayerState() == MediaPlayerInterface.PLAYER_STATE_IDLE) {
+ // There is no next source or it's in preparing or prepared state.
+ return null;
+ }
+
+ MediaPlayerSource src = mQueue.get(n);
+ try {
+ src.mSourceState = SOURCE_STATE_PREPARING;
+ handleDataSource(src);
+ src.mPlayer.prepareAsync();
+ return null;
+ } catch (Exception e) {
+ DataSourceDesc dsd = src.getDSD();
+ setMp2State(src.mPlayer, MEDIAPLAYER2_STATE_ERROR);
+ return new DataSourceError(dsd, MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNSUPPORTED);
+ }
+
+ }
+
+ synchronized void skipToNext() {
+ if (mQueue.size() <= 1) {
+ throw new IllegalStateException("No next source available");
+ }
+ final MediaPlayerSource src = mQueue.get(0);
+ moveToNext();
+ if (src.mPlayerState == MediaPlayerInterface.PLAYER_STATE_PLAYING) {
+ playCurrent();
+ }
+ }
+
+ synchronized void setLooping(boolean loop) {
+ getCurrentPlayer().setLooping(loop);
+ }
+
+ synchronized void setPlaybackParams(PlaybackParams playbackParams) {
+ getCurrentPlayer().setPlaybackParams(playbackParams);
+ }
+
+ synchronized float getVolume() {
+ return mVolume;
+ }
+
+ synchronized void setVolume(float volume) {
+ mVolume = volume;
+ getCurrentPlayer().setVolume(volume, volume);
+ }
+
+ synchronized void setSurface(Surface surface) {
+ mSurface = surface;
+ getCurrentPlayer().setSurface(surface);
+ }
+
+ synchronized int getVideoWidth() {
+ return getCurrentPlayer().getVideoWidth();
+ }
+
+ synchronized int getVideoHeight() {
+ return getCurrentPlayer().getVideoHeight();
+ }
+
+ synchronized PersistableBundle getMetrics() {
+ return getCurrentPlayer().getMetrics();
+ }
+
+ synchronized PlaybackParams getPlaybackParams() {
+ return getCurrentPlayer().getPlaybackParams();
+ }
+
+ synchronized void setSyncParams(SyncParams params) {
+ getCurrentPlayer().setSyncParams(params);
+ }
+
+ synchronized SyncParams getSyncParams() {
+ return getCurrentPlayer().getSyncParams();
+ }
+
+ synchronized void seekTo(long msec, int mode) {
+ getCurrentPlayer().seekTo(msec, mode);
+ }
+
+ synchronized void reset() {
+ MediaPlayerSource src = mQueue.get(0);
+ src.mPlayer.reset();
+ src.mBufferedPercentage.set(0);
+ mVolume = 1.0f;
+ setMp2State(src.mPlayer, MEDIAPLAYER2_STATE_IDLE);
+ setBufferingState(src.mPlayer, MediaPlayerInterface.BUFFERING_STATE_UNKNOWN);
+ }
+
+ synchronized MediaTimestamp getTimestamp() {
+ return getCurrentPlayer().getTimestamp();
+ }
+
+ synchronized void setAudioSessionId(int sessionId) {
+ getCurrentPlayer().setAudioSessionId(sessionId);
+ }
+
+ synchronized int getAudioSessionId() {
+ return getCurrentPlayer().getAudioSessionId();
+ }
+
+ synchronized void attachAuxEffect(int effectId) {
+ getCurrentPlayer().attachAuxEffect(effectId);
+ }
+
+ synchronized void setAuxEffectSendLevel(float level) {
+ getCurrentPlayer().setAuxEffectSendLevel(level);
+ }
+
+ synchronized MediaPlayer.TrackInfo[] getTrackInfo() {
+ return getCurrentPlayer().getTrackInfo();
+ }
+
+ synchronized int getSelectedTrack(int trackType) {
+ return getCurrentPlayer().getSelectedTrack(trackType);
+ }
+
+ synchronized void selectTrack(int index) {
+ getCurrentPlayer().selectTrack(index);
+ }
+
+ synchronized void deselectTrack(int index) {
+ getCurrentPlayer().deselectTrack(index);
+ }
+
+ synchronized MediaPlayer.DrmInfo getDrmInfo() {
+ return getCurrentPlayer().getDrmInfo();
+ }
+
+ synchronized void prepareDrm(UUID uuid)
+ throws ResourceBusyException, MediaPlayer.ProvisioningServerErrorException,
+ MediaPlayer.ProvisioningNetworkErrorException, UnsupportedSchemeException {
+ getCurrentPlayer().prepareDrm(uuid);
+ }
+
+ synchronized void releaseDrm() throws MediaPlayer.NoDrmSchemeException {
+ getCurrentPlayer().releaseDrm();
+ }
+
+ synchronized byte[] provideKeyResponse(byte[] keySetId, byte[] response)
+ throws DeniedByServerException, MediaPlayer.NoDrmSchemeException {
+ return getCurrentPlayer().provideKeyResponse(keySetId, response);
+ }
+
+ synchronized void restoreKeys(byte[] keySetId) throws MediaPlayer.NoDrmSchemeException {
+ getCurrentPlayer().restoreKeys(keySetId);
+ }
+
+ synchronized String getDrmPropertyString(String propertyName)
+ throws MediaPlayer.NoDrmSchemeException {
+ return getCurrentPlayer().getDrmPropertyString(propertyName);
+ }
+
+ synchronized void setDrmPropertyString(String propertyName, String value)
+ throws MediaPlayer.NoDrmSchemeException {
+ getCurrentPlayer().setDrmPropertyString(propertyName, value);
+ }
+
+ synchronized void setOnDrmConfigHelper(MediaPlayer.OnDrmConfigHelper onDrmConfigHelper) {
+ getCurrentPlayer().setOnDrmConfigHelper(onDrmConfigHelper);
+ }
+
+ synchronized MediaDrm.KeyRequest getKeyRequest(byte[] keySetId, byte[] initData,
+ String mimeType,
+ int keyType, Map<String, String> optionalParameters)
+ throws MediaPlayer.NoDrmSchemeException {
+ return getCurrentPlayer().getKeyRequest(keySetId, initData, mimeType, keyType,
+ optionalParameters);
+ }
+
+ synchronized void setMp2State(MediaPlayer mp, @MediaPlayer2State int mp2State) {
+ for (final MediaPlayerSource src: mQueue) {
+ if (src.mPlayer != mp) {
+ continue;
+ }
+ if (src.mMp2State == mp2State) {
+ return;
+ }
+ src.mMp2State = mp2State;
+
+ final int playerState = sStateMap.get(mp2State);
+ if (src.mPlayerState == playerState) {
+ return;
+ }
+ src.mPlayerState = playerState;
+ notifyPlayerEvent(new PlayerEventNotifier() {
+ @Override
+ public void notify(PlayerEventCallback cb) {
+ cb.onPlayerStateChanged(mMediaPlayerInterfaceImpl, playerState);
+ }
+ });
+ return;
+ }
+ }
+
+ synchronized void setBufferingState(MediaPlayer mp, @BuffState final int state) {
+ for (final MediaPlayerSource src: mQueue) {
+ if (src.mPlayer != mp) {
+ continue;
+ }
+ if (src.mBufferingState == state) {
+ return;
+ }
+ src.mBufferingState = state;
+ notifyPlayerEvent(new PlayerEventNotifier() {
+ @Override
+ public void notify(PlayerEventCallback cb) {
+ DataSourceDesc dsd = src.getDSD();
+ cb.onBufferingStateChanged(mMediaPlayerInterfaceImpl, dsd, state);
+ }
+ });
+ return;
+ }
+ }
+
+ synchronized @MediaPlayer2State int getMediaPlayer2State() {
+ return mQueue.get(0).mMp2State;
+ }
+
+ synchronized @BuffState int getBufferingState() {
+ return mQueue.get(0).mBufferingState;
+ }
+
+ synchronized @PlayerState int getPlayerState() {
+ return mQueue.get(0).mPlayerState;
+ }
+
+ synchronized MediaPlayerSource getSourceForPlayer(MediaPlayer mp) {
+ for (MediaPlayerSource src: mQueue) {
+ if (src.mPlayer == mp) {
+ return src;
+ }
+ }
+ return null;
+ }
+ }
+
+ private class MediaPlayerInterfaceImpl extends MediaPlayerInterface {
+ @Override
+ public void play() {
+ MediaPlayer2Impl.this.play();
+ }
+
+ @Override
+ public void prepare() {
+ MediaPlayer2Impl.this.prepare();
+ }
+
+ @Override
+ public void pause() {
+ MediaPlayer2Impl.this.pause();
+ }
+
+ @Override
+ public void reset() {
+ MediaPlayer2Impl.this.reset();
+ }
+
+ @Override
+ public void skipToNext() {
+ MediaPlayer2Impl.this.skipToNext();
+ }
+
+ @Override
+ public void seekTo(long pos) {
+ MediaPlayer2Impl.this.seekTo(pos);
+ }
+
+ @Override
+ public long getCurrentPosition() {
+ return MediaPlayer2Impl.this.getCurrentPosition();
+ }
+
+ @Override
+ public long getDuration() {
+ return MediaPlayer2Impl.this.getDuration();
+ }
+
+ @Override
+ public long getBufferedPosition() {
+ return MediaPlayer2Impl.this.getBufferedPosition();
+ }
+
+ @Override
+ public int getPlayerState() {
+ return MediaPlayer2Impl.this.getPlayerState();
+ }
+
+ @Override
+ public int getBufferingState() {
+ return MediaPlayer2Impl.this.getBufferingState();
+ }
+
+ @Override
+ public void setAudioAttributes(AudioAttributesCompat attributes) {
+ MediaPlayer2Impl.this.setAudioAttributes(attributes);
+ }
+
+ @Override
+ public AudioAttributesCompat getAudioAttributes() {
+ return MediaPlayer2Impl.this.getAudioAttributes();
+ }
+
+ @Override
+ public void setDataSource(DataSourceDesc dsd) {
+ MediaPlayer2Impl.this.setDataSource(dsd);
+ }
+
+ @Override
+ public void setNextDataSource(DataSourceDesc dsd) {
+ MediaPlayer2Impl.this.setNextDataSource(dsd);
+ }
+
+ @Override
+ public void setNextDataSources(List<DataSourceDesc> dsds) {
+ MediaPlayer2Impl.this.setNextDataSources(dsds);
+ }
+
+ @Override
+ public DataSourceDesc getCurrentDataSource() {
+ return MediaPlayer2Impl.this.getCurrentDataSource();
+ }
+
+ @Override
+ public void loopCurrent(boolean loop) {
+ MediaPlayer2Impl.this.loopCurrent(loop);
+ }
+
+ @Override
+ public void setPlaybackSpeed(float speed) {
+ MediaPlayer2Impl.this.setPlaybackSpeed(speed);
+ }
+
+ @Override
+ public float getPlaybackSpeed() {
+ return MediaPlayer2Impl.this.getPlaybackSpeed();
+ }
+
+ @Override
+ public void setPlayerVolume(float volume) {
+ MediaPlayer2Impl.this.setPlayerVolume(volume);
+ }
+
+ @Override
+ public float getPlayerVolume() {
+ return MediaPlayer2Impl.this.getPlayerVolume();
+ }
+
+ @Override
+ public void registerPlayerEventCallback(Executor e, final PlayerEventCallback cb) {
+ MediaPlayer2Impl.this.registerPlayerEventCallback(e, cb);
+ }
+
+ @Override
+ public void unregisterPlayerEventCallback(PlayerEventCallback cb) {
+ MediaPlayer2Impl.this.unregisterPlayerEventCallback(cb);
+ }
+
+ @Override
+ public void close() throws Exception {
+ MediaPlayer2Impl.this.close();
+ }
+ }
}
diff --git a/media/src/main/java/androidx/media/MediaPlayerBase.java b/media/src/main/java/androidx/media/MediaPlayerInterface.java
similarity index 90%
rename from media/src/main/java/androidx/media/MediaPlayerBase.java
rename to media/src/main/java/androidx/media/MediaPlayerInterface.java
index de0e128..9d51609 100644
--- a/media/src/main/java/androidx/media/MediaPlayerBase.java
+++ b/media/src/main/java/androidx/media/MediaPlayerInterface.java
@@ -32,10 +32,10 @@
import java.util.concurrent.Executor;
/**
- * Base class for all media players that want media session.
+ * Base interface for all media players that want media session.
*/
@TargetApi(Build.VERSION_CODES.KITKAT)
-public abstract class MediaPlayerBase implements AutoCloseable {
+public abstract class MediaPlayerInterface implements AutoCloseable {
/**
* @hide
*/
@@ -111,9 +111,9 @@
/**
* Prepares the player for playback.
- * See {@link PlayerEventCallback#onMediaPrepared(MediaPlayerBase, DataSourceDesc)} for being
- * notified when the preparation phase completed. During this time, the player may allocate
- * resources required to play, such as audio and video decoders.
+ * See {@link PlayerEventCallback#onMediaPrepared(MediaPlayerInterface, DataSourceDesc)} for
+ * being notified when the preparation phase completed. During this time, the player may
+ * allocate resources required to play, such as audio and video decoders.
*/
public abstract void prepare();
@@ -123,7 +123,7 @@
public abstract void pause();
/**
- * Resets the MediaPlayerBase to its uninitialized state.
+ * Resets the MediaPlayerInterface to its uninitialized state.
*/
public abstract void reset();
@@ -166,7 +166,7 @@
/**
* Returns the current player state.
- * See also {@link PlayerEventCallback#onPlayerStateChanged(MediaPlayerBase, int)} for
+ * See also {@link PlayerEventCallback#onPlayerStateChanged(MediaPlayerInterface, int)} for
* notification of changes.
* @return the current player state
*/
@@ -294,8 +294,8 @@
/**
* A callback class to receive notifications for events on the media player.
- * See {@link MediaPlayerBase#registerPlayerEventCallback(Executor, PlayerEventCallback)} to
- * register this callback.
+ * See {@link MediaPlayerInterface#registerPlayerEventCallback(Executor, PlayerEventCallback)}
+ * to register this callback.
*/
public abstract static class PlayerEventCallback {
/**
@@ -304,7 +304,7 @@
* @param mpb the player whose data source changed.
* @param dsd the new current data source. null, if no more data sources available.
*/
- public void onCurrentDataSourceChanged(@NonNull MediaPlayerBase mpb,
+ public void onCurrentDataSourceChanged(@NonNull MediaPlayerInterface mpb,
@Nullable DataSourceDesc dsd) { }
/**
@@ -313,16 +313,17 @@
* @param mpb the player that is prepared.
* @param dsd the data source that the player is prepared to play.
*/
- public void onMediaPrepared(@NonNull MediaPlayerBase mpb,
+ public void onMediaPrepared(@NonNull MediaPlayerInterface mpb,
@NonNull DataSourceDesc dsd) { }
/**
* Called to indicate that the state of the player has changed.
- * See {@link MediaPlayerBase#getPlayerState()} for polling the player state.
+ * See {@link MediaPlayerInterface#getPlayerState()} for polling the player state.
* @param mpb the player whose state has changed.
* @param state the new state of the player.
*/
- public void onPlayerStateChanged(@NonNull MediaPlayerBase mpb, @PlayerState int state) { }
+ public void onPlayerStateChanged(@NonNull MediaPlayerInterface mpb,
+ @PlayerState int state) { }
/**
* Called to report buffering events for a data source.
@@ -330,7 +331,7 @@
* @param dsd the data source for which buffering is happening.
* @param state the new buffering state.
*/
- public void onBufferingStateChanged(@NonNull MediaPlayerBase mpb,
+ public void onBufferingStateChanged(@NonNull MediaPlayerInterface mpb,
@NonNull DataSourceDesc dsd, @BuffState int state) { }
/**
@@ -338,7 +339,7 @@
* @param mpb the player that has changed the playback speed.
* @param speed the new playback speed.
*/
- public void onPlaybackSpeedChanged(@NonNull MediaPlayerBase mpb, float speed) { }
+ public void onPlaybackSpeedChanged(@NonNull MediaPlayerInterface mpb, float speed) { }
/**
* Called to indicate that {@link #seekTo(long)} is completed.
@@ -347,6 +348,6 @@
* @param position the previous seeking request.
* @see #seekTo(long)
*/
- public void onSeekCompleted(@NonNull MediaPlayerBase mpb, long position) { }
+ public void onSeekCompleted(@NonNull MediaPlayerInterface mpb, long position) { }
}
}
diff --git a/media/src/main/java/androidx/media/MediaPlaylistAgent.java b/media/src/main/java/androidx/media/MediaPlaylistAgent.java
index 07838e8..4e1eee4 100644
--- a/media/src/main/java/androidx/media/MediaPlaylistAgent.java
+++ b/media/src/main/java/androidx/media/MediaPlaylistAgent.java
@@ -377,12 +377,12 @@
/**
* Called by {@link MediaSession2} when it wants to translate {@link DataSourceDesc} from the
- * {@link MediaPlayerBase.PlayerEventCallback} to the {@link MediaItem2}. Override this method
- * if you want to create {@link DataSourceDesc}s dynamically, instead of specifying them with
- * {@link #setPlaylist(List, MediaMetadata2)}.
+ * {@link MediaPlayerInterface.PlayerEventCallback} to the {@link MediaItem2}. Override this
+ * method if you want to create {@link DataSourceDesc}s dynamically, instead of specifying them
+ * with {@link #setPlaylist(List, MediaMetadata2)}.
* <p>
* Session would throw an exception if this returns {@code null} for the dsd from the
- * {@link MediaPlayerBase.PlayerEventCallback}.
+ * {@link MediaPlayerInterface.PlayerEventCallback}.
* <p>
* Default implementation calls the {@link #getPlaylist()} and searches the {@link MediaItem2}
* with the {@param dsd}.
diff --git a/media/src/main/java/androidx/media/MediaSession2.java b/media/src/main/java/androidx/media/MediaSession2.java
index 909e979..ce95078 100644
--- a/media/src/main/java/androidx/media/MediaSession2.java
+++ b/media/src/main/java/androidx/media/MediaSession2.java
@@ -27,8 +27,9 @@
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
+import android.os.RemoteException;
import android.os.ResultReceiver;
-import android.support.v4.media.session.IMediaControllerCallback;
+import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import androidx.annotation.IntDef;
@@ -36,8 +37,8 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.media.MediaController2.PlaybackInfo;
-import androidx.media.MediaPlayerBase.BuffState;
-import androidx.media.MediaPlayerBase.PlayerState;
+import androidx.media.MediaPlayerInterface.BuffState;
+import androidx.media.MediaPlayerInterface.PlayerState;
import androidx.media.MediaPlaylistAgent.PlaylistEventCallback;
import androidx.media.MediaPlaylistAgent.RepeatMode;
import androidx.media.MediaPlaylistAgent.ShuffleMode;
@@ -60,6 +61,10 @@
* handle media keys. In general an app only needs one session for all playback, though multiple
* sessions can be created to provide finer grain controls of media.
* <p>
+ * If you want to support background playback, {@link MediaSessionService2} is preferred
+ * instead. With it, your playback can be revived even after playback is finished. See
+ * {@link MediaSessionService2} for details.
+ * <p>
* A session can be obtained by {@link Builder}. The owner of the session may pass its session token
* to other processes to allow them to create a {@link MediaController2} to interact with the
* session.
@@ -72,6 +77,8 @@
* and notify any controllers.
* <p>
* {@link MediaSession2} objects should be used on the thread on the looper.
+ *
+ * @see MediaSessionService2
*/
@TargetApi(Build.VERSION_CODES.KITKAT)
public class MediaSession2 extends MediaInterface2.SessionPlayer implements AutoCloseable {
@@ -154,6 +161,594 @@
*/
public static final int ERROR_CODE_SETUP_REQUIRED = 12;
+ static final String TAG = "MediaSession2";
+
+ private final SupportLibraryImpl mImpl;
+
+ MediaSession2(SupportLibraryImpl impl) {
+ mImpl = impl;
+ }
+
+ SupportLibraryImpl getImpl() {
+ return mImpl;
+ }
+
+ /**
+ * Sets the underlying {@link MediaPlayerInterface} and {@link MediaPlaylistAgent} for this
+ * session to dispatch incoming event to.
+ * <p>
+ * When a {@link MediaPlaylistAgent} is specified here, the playlist agent should manage
+ * {@link MediaPlayerInterface} for calling
+ * {@link MediaPlayerInterface#setNextDataSources(List)}.
+ * <p>
+ * If the {@link MediaPlaylistAgent} isn't set, session will recreate the default playlist
+ * agent.
+ *
+ * @param player a {@link MediaPlayerInterface} that handles actual media playback in your app
+ * @param playlistAgent a {@link MediaPlaylistAgent} that manages playlist of the {@code player}
+ * @param volumeProvider a {@link VolumeProviderCompat}. If {@code null}, system will adjust the
+ * appropriate stream volume for this session's player.
+ */
+ public void updatePlayer(@NonNull MediaPlayerInterface player,
+ @Nullable MediaPlaylistAgent playlistAgent,
+ @Nullable VolumeProviderCompat volumeProvider) {
+ mImpl.updatePlayer(player, playlistAgent, volumeProvider);
+ }
+
+ @Override
+ public void close() {
+ try {
+ mImpl.close();
+ } catch (Exception e) {
+ // Should not be here.
+ }
+ }
+
+ /**
+ * @return player
+ */
+ public @NonNull MediaPlayerInterface getPlayer() {
+ return mImpl.getPlayer();
+ }
+
+ /**
+ * @return playlist agent
+ */
+ public @NonNull MediaPlaylistAgent getPlaylistAgent() {
+ return mImpl.getPlaylistAgent();
+ }
+
+ /**
+ * @return volume provider
+ */
+ public @Nullable VolumeProviderCompat getVolumeProvider() {
+ return mImpl.getVolumeProvider();
+ }
+
+ /**
+ * Returns the {@link SessionToken2} for creating {@link MediaController2}.
+ */
+ public @NonNull SessionToken2 getToken() {
+ return mImpl.getToken();
+ }
+
+ @NonNull Context getContext() {
+ return mImpl.getContext();
+ }
+
+ @NonNull Executor getCallbackExecutor() {
+ return mImpl.getCallbackExecutor();
+ }
+
+ @NonNull SessionCallback getCallback() {
+ return mImpl.getCallback();
+ }
+
+ /**
+ * Returns the list of connected controller.
+ *
+ * @return list of {@link ControllerInfo}
+ */
+ public @NonNull List<ControllerInfo> getConnectedControllers() {
+ return mImpl.getConnectedControllers();
+ }
+
+ /**
+ * Set the {@link AudioFocusRequest} to obtain the audio focus
+ *
+ * @param afr the full request parameters
+ */
+ public void setAudioFocusRequest(@Nullable AudioFocusRequest afr) {
+ mImpl.setAudioFocusRequest(afr);
+ }
+
+ /**
+ * Sets ordered list of {@link CommandButton} for controllers to build UI with it.
+ * <p>
+ * It's up to controller's decision how to represent the layout in its own UI.
+ * Here's the same way
+ * (layout[i] means a CommandButton at index i in the given list)
+ * For 5 icons row
+ * layout[3] layout[1] layout[0] layout[2] layout[4]
+ * For 3 icons row
+ * layout[1] layout[0] layout[2]
+ * For 5 icons row with overflow icon (can show +5 extra buttons with overflow button)
+ * expanded row: layout[5] layout[6] layout[7] layout[8] layout[9]
+ * main row: layout[3] layout[1] layout[0] layout[2] layout[4]
+ * <p>
+ * This API can be called in the
+ * {@link SessionCallback#onConnect(MediaSession2, ControllerInfo)}.
+ *
+ * @param controller controller to specify layout.
+ * @param layout ordered list of layout.
+ */
+ public void setCustomLayout(@NonNull ControllerInfo controller,
+ @NonNull List<CommandButton> layout) {
+ mImpl.setCustomLayout(controller, layout);
+ }
+
+ /**
+ * Set the new allowed command group for the controller
+ *
+ * @param controller controller to change allowed commands
+ * @param commands new allowed commands
+ */
+ public void setAllowedCommands(@NonNull ControllerInfo controller,
+ @NonNull SessionCommandGroup2 commands) {
+ mImpl.setAllowedCommands(controller, commands);
+ }
+
+ /**
+ * Send custom command to all connected controllers.
+ *
+ * @param command a command
+ * @param args optional argument
+ */
+ public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args) {
+ mImpl.sendCustomCommand(command, args);
+ }
+
+ /**
+ * Send custom command to a specific controller.
+ *
+ * @param command a command
+ * @param args optional argument
+ * @param receiver result receiver for the session
+ */
+ public void sendCustomCommand(@NonNull ControllerInfo controller,
+ @NonNull SessionCommand2 command, @Nullable Bundle args,
+ @Nullable ResultReceiver receiver) {
+ mImpl.sendCustomCommand(controller, command, args, receiver);
+ }
+
+ /**
+ * Play playback.
+ * <p>
+ * This calls {@link MediaPlayerInterface#play()}.
+ */
+ @Override
+ public void play() {
+ mImpl.play();
+ }
+
+ /**
+ * Pause playback.
+ * <p>
+ * This calls {@link MediaPlayerInterface#pause()}.
+ */
+ @Override
+ public void pause() {
+ mImpl.pause();
+ }
+
+ /**
+ * Stop playback, and reset the player to the initial state.
+ * <p>
+ * This calls {@link MediaPlayerInterface#reset()}.
+ */
+ @Override
+ public void reset() {
+ mImpl.reset();
+ }
+
+ /**
+ * Request that the player prepare its playback. In other words, other sessions can continue
+ * to play during the preparation of this session. This method can be used to speed up the
+ * start of the playback. Once the preparation is done, the session will change its playback
+ * state to {@link MediaPlayerInterface#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be
+ * called to start playback.
+ * <p>
+ * This calls {@link MediaPlayerInterface#reset()}.
+ */
+ @Override
+ public void prepare() {
+ mImpl.prepare();
+ }
+
+ /**
+ * Move to a new location in the media stream.
+ *
+ * @param pos Position to move to, in milliseconds.
+ */
+ @Override
+ public void seekTo(long pos) {
+ mImpl.seekTo(pos);
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void skipForward() {
+ mImpl.skipForward();
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void skipBackward() {
+ mImpl.skipBackward();
+ }
+
+ /**
+ * Notify errors to the connected controllers
+ *
+ * @param errorCode error code
+ * @param extras extras
+ */
+ @Override
+ public void notifyError(@ErrorCode int errorCode, @Nullable Bundle extras) {
+ mImpl.notifyError(errorCode, extras);
+ }
+
+ /**
+ * Notify routes information to a connected controller
+ *
+ * @param controller controller information
+ * @param routes The routes information. Each bundle should be from
+ * MediaRouteDescritor.asBundle().
+ */
+ public void notifyRoutesInfoChanged(@NonNull ControllerInfo controller,
+ @Nullable List<Bundle> routes) {
+ mImpl.notifyRoutesInfoChanged(controller, routes);
+ }
+
+ /**
+ * Gets the current player state.
+ *
+ * @return the current player state
+ */
+ @Override
+ public @PlayerState int getPlayerState() {
+ return mImpl.getPlayerState();
+ }
+
+ /**
+ * Gets the current position.
+ *
+ * @return the current playback position in ms, or {@link MediaPlayerInterface#UNKNOWN_TIME} if
+ * unknown.
+ */
+ @Override
+ public long getCurrentPosition() {
+ return mImpl.getCurrentPosition();
+ }
+
+ /**
+ * Gets the duration of the currently playing media item.
+ *
+ * @return the duration of the current item from {@link MediaPlayerInterface#getDuration()}.
+ */
+ @Override
+ public long getDuration() {
+ return mImpl.getDuration();
+ }
+
+ /**
+ * Gets the buffered position, or {@link MediaPlayerInterface#UNKNOWN_TIME} if unknown.
+ *
+ * @return the buffered position in ms, or {@link MediaPlayerInterface#UNKNOWN_TIME}.
+ */
+ @Override
+ public long getBufferedPosition() {
+ return mImpl.getBufferedPosition();
+ }
+
+ /**
+ * Gets the current buffering state of the player.
+ * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+ * buffered.
+ *
+ * @return the buffering state.
+ */
+ @Override
+ public @BuffState int getBufferingState() {
+ return mImpl.getBufferingState();
+ }
+
+ /**
+ * Get the playback speed.
+ *
+ * @return speed
+ */
+ @Override
+ public float getPlaybackSpeed() {
+ return mImpl.getPlaybackSpeed();
+ }
+
+ /**
+ * Set the playback speed.
+ */
+ @Override
+ public void setPlaybackSpeed(float speed) {
+ mImpl.setPlaybackSpeed(speed);
+ }
+
+ /**
+ * Sets the data source missing helper. Helper will be used to provide default implementation of
+ * {@link MediaPlaylistAgent} when it isn't set by developer.
+ * <p>
+ * Default implementation of the {@link MediaPlaylistAgent} will call helper when a
+ * {@link MediaItem2} in the playlist doesn't have a {@link DataSourceDesc}. This may happen
+ * when
+ * <ul>
+ * <li>{@link MediaItem2} specified by {@link #setPlaylist(List, MediaMetadata2)} doesn't
+ * have {@link DataSourceDesc}</li>
+ * <li>{@link MediaController2#addPlaylistItem(int, MediaItem2)} is called and accepted
+ * by {@link SessionCallback#onCommandRequest(
+ * MediaSession2, ControllerInfo, SessionCommand2)}.
+ * In that case, an item would be added automatically without the data source.</li>
+ * </ul>
+ * <p>
+ * If it's not set, playback wouldn't happen for the item without data source descriptor.
+ * <p>
+ * The helper will be run on the executor that was specified by
+ * {@link Builder#setSessionCallback(Executor, SessionCallback)}.
+ *
+ * @param helper a data source missing helper.
+ * @throws IllegalStateException when the helper is set when the playlist agent is set
+ * @see #setPlaylist(List, MediaMetadata2)
+ * @see SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_ADD_ITEM
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_REPLACE_ITEM
+ */
+ @Override
+ public void setOnDataSourceMissingHelper(@NonNull OnDataSourceMissingHelper helper) {
+ mImpl.setOnDataSourceMissingHelper(helper);
+ }
+
+ /**
+ * Clears the data source missing helper.
+ *
+ * @see #setOnDataSourceMissingHelper(OnDataSourceMissingHelper)
+ */
+ @Override
+ public void clearOnDataSourceMissingHelper() {
+ mImpl.clearOnDataSourceMissingHelper();
+ }
+
+ /**
+ * Returns the playlist from the {@link MediaPlaylistAgent}.
+ * <p>
+ * This list may differ with the list that was specified with
+ * {@link #setPlaylist(List, MediaMetadata2)} depending on the {@link MediaPlaylistAgent}
+ * implementation. Use media items returned here for other playlist agent APIs such as
+ * {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}.
+ *
+ * @return playlist
+ * @see MediaPlaylistAgent#getPlaylist()
+ * @see SessionCallback#onPlaylistChanged(
+ * MediaSession2, MediaPlaylistAgent, List, MediaMetadata2)
+ */
+ @Override
+ public List<MediaItem2> getPlaylist() {
+ return mImpl.getPlaylist();
+ }
+
+ /**
+ * Sets a list of {@link MediaItem2} to the {@link MediaPlaylistAgent}. Ensure uniqueness of
+ * each {@link MediaItem2} in the playlist so the session can uniquely identity individual
+ * items.
+ * <p>
+ * This may be an asynchronous call, and {@link MediaPlaylistAgent} may keep the copy of the
+ * list. Wait for {@link SessionCallback#onPlaylistChanged(MediaSession2, MediaPlaylistAgent,
+ * List, MediaMetadata2)} to know the operation finishes.
+ * <p>
+ * You may specify a {@link MediaItem2} without {@link DataSourceDesc}. In that case,
+ * {@link MediaPlaylistAgent} has responsibility to dynamically query {link DataSourceDesc}
+ * when such media item is ready for preparation or play. Default implementation needs
+ * {@link OnDataSourceMissingHelper} for such case.
+ * <p>
+ * It's recommended to fill {@link MediaMetadata2} in each {@link MediaItem2} especially for the
+ * duration information with the key {@link MediaMetadata2#METADATA_KEY_DURATION}. Without the
+ * duration information in the metadata, session will do extra work to get the duration and send
+ * it to the controller.
+ *
+ * @param list A list of {@link MediaItem2} objects to set as a play list.
+ * @throws IllegalArgumentException if given list is {@code null}, or has duplicated media
+ * items.
+ * @see MediaPlaylistAgent#setPlaylist(List, MediaMetadata2)
+ * @see SessionCallback#onPlaylistChanged(
+ * MediaSession2, MediaPlaylistAgent, List, MediaMetadata2)
+ * @see #setOnDataSourceMissingHelper
+ */
+ @Override
+ public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
+ mImpl.setPlaylist(list, metadata);
+ }
+
+ /**
+ * Skips to the item in the playlist.
+ * <p>
+ * This calls {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)} and the behavior depends
+ * on the playlist agent implementation, especially with the shuffle/repeat mode.
+ *
+ * @param item The item in the playlist you want to play
+ * @see #getShuffleMode()
+ * @see #getRepeatMode()
+ */
+ @Override
+ public void skipToPlaylistItem(@NonNull MediaItem2 item) {
+ mImpl.skipToPlaylistItem(item);
+ }
+
+ /**
+ * Skips to the previous item.
+ * <p>
+ * This calls {@link MediaPlaylistAgent#skipToPreviousItem()} and the behavior depends on the
+ * playlist agent implementation, especially with the shuffle/repeat mode.
+ *
+ * @see #getShuffleMode()
+ * @see #getRepeatMode()
+ **/
+ @Override
+ public void skipToPreviousItem() {
+ mImpl.skipToPreviousItem();
+ }
+
+ /**
+ * Skips to the next item.
+ * <p>
+ * This calls {@link MediaPlaylistAgent#skipToNextItem()} and the behavior depends on the
+ * playlist agent implementation, especially with the shuffle/repeat mode.
+ *
+ * @see #getShuffleMode()
+ * @see #getRepeatMode()
+ */
+ @Override
+ public void skipToNextItem() {
+ mImpl.skipToNextItem();
+ }
+
+ /**
+ * Gets the playlist metadata from the {@link MediaPlaylistAgent}.
+ *
+ * @return the playlist metadata
+ */
+ @Override
+ public MediaMetadata2 getPlaylistMetadata() {
+ return mImpl.getPlaylistMetadata();
+ }
+
+ /**
+ * Adds the media item to the playlist at position index. Index equals or greater than
+ * the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of
+ * the playlist.
+ * <p>
+ * This will not change the currently playing media item.
+ * If index is less than or equal to the current index of the play list,
+ * the current index of the play list will be incremented correspondingly.
+ *
+ * @param index the index you want to add
+ * @param item the media item you want to add
+ */
+ @Override
+ public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
+ mImpl.addPlaylistItem(index, item);
+ }
+
+ /**
+ * Removes the media item in the playlist.
+ * <p>
+ * If the item is the currently playing item of the playlist, current playback
+ * will be stopped and playback moves to next source in the list.
+ *
+ * @param item the media item you want to add
+ */
+ @Override
+ public void removePlaylistItem(@NonNull MediaItem2 item) {
+ mImpl.removePlaylistItem(item);
+ }
+
+ /**
+ * Replaces the media item at index in the playlist. This can be also used to update metadata of
+ * an item.
+ *
+ * @param index the index of the item to replace
+ * @param item the new item
+ */
+ @Override
+ public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
+ mImpl.replacePlaylistItem(index, item);
+ }
+
+ /**
+ * Return currently playing media item.
+ *
+ * @return currently playing media item
+ */
+ @Override
+ public MediaItem2 getCurrentMediaItem() {
+ return mImpl.getCurrentMediaItem();
+ }
+
+ /**
+ * Updates the playlist metadata to the {@link MediaPlaylistAgent}.
+ *
+ * @param metadata metadata of the playlist
+ */
+ @Override
+ public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
+ mImpl.updatePlaylistMetadata(metadata);
+ }
+
+ /**
+ * Gets the repeat mode from the {@link MediaPlaylistAgent}.
+ *
+ * @return repeat mode
+ * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+ * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+ */
+ @Override
+ public @RepeatMode int getRepeatMode() {
+ return mImpl.getRepeatMode();
+ }
+
+ /**
+ * Sets the repeat mode to the {@link MediaPlaylistAgent}.
+ *
+ * @param repeatMode repeat mode
+ * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+ * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+ */
+ @Override
+ public void setRepeatMode(@RepeatMode int repeatMode) {
+ mImpl.setRepeatMode(repeatMode);
+ }
+
+ /**
+ * Gets the shuffle mode from the {@link MediaPlaylistAgent}.
+ *
+ * @return The shuffle mode
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+ */
+ @Override
+ public @ShuffleMode int getShuffleMode() {
+ return mImpl.getShuffleMode();
+ }
+
+ /**
+ * Sets the shuffle mode to the {@link MediaPlaylistAgent}.
+ *
+ * @param shuffleMode The shuffle mode
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+ */
+ @Override
+ public void setShuffleMode(@ShuffleMode int shuffleMode) {
+ mImpl.setShuffleMode(shuffleMode);
+ }
+
/**
* Interface definition of a callback to be invoked when a {@link MediaItem2} in the playlist
* didn't have a {@link DataSourceDesc} but it's needed now for preparing or playing it.
@@ -217,7 +812,7 @@
* Called when a controller sent a command which will be sent directly to one of the
* following:
* <ul>
- * <li> {@link MediaPlayerBase} </li>
+ * <li> {@link MediaPlayerInterface} </li>
* <li> {@link MediaPlaylistAgent} </li>
* <li> {@link android.media.AudioManager} or {@link VolumeProviderCompat} </li>
* </ul>
@@ -336,7 +931,7 @@
* <p>
* During the preparation, a session should not hold audio focus in order to allow other
* sessions play seamlessly. The state of playback should be updated to
- * {@link MediaPlayerBase#PLAYER_STATE_PAUSED} after the preparation is done.
+ * {@link MediaPlayerInterface#PLAYER_STATE_PAUSED} after the preparation is done.
* <p>
* The playback of the prepared content should start in the later calls of
* {@link MediaSession2#play()}.
@@ -361,8 +956,9 @@
* An empty query indicates that the app may prepare any music. The implementation should
* attempt to make a smart choice about what to play.
* <p>
- * The state of playback should be updated to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}
- * after the preparation is done. The playback of the prepared content should start in the
+ * The state of playback should be updated to
+ * {@link MediaPlayerInterface#PLAYER_STATE_PAUSED} after the preparation is done.
+ * The playback of the prepared content should start in the
* later calls of {@link MediaSession2#play()}.
* <p>
* Override {@link #onPlayFromSearch} to handle requests for starting playback without
@@ -384,7 +980,7 @@
* <p>
* During the preparation, a session should not hold audio focus in order to allow
* other sessions play seamlessly. The state of playback should be updated to
- * {@link MediaPlayerBase#PLAYER_STATE_PAUSED} after the preparation is done.
+ * {@link MediaPlayerInterface#PLAYER_STATE_PAUSED} after the preparation is done.
* <p>
* The playback of the prepared content should start in the later calls of
* {@link MediaSession2#play()}.
@@ -461,7 +1057,7 @@
* @param item new item
*/
public void onCurrentMediaItemChanged(@NonNull MediaSession2 session,
- @NonNull MediaPlayerBase player, @NonNull MediaItem2 item) { }
+ @NonNull MediaPlayerInterface player, @Nullable MediaItem2 item) { }
/**
* Called when the player is <i>prepared</i>, i.e. it is ready to play the content
@@ -470,18 +1066,18 @@
* @param player the player for this event
* @param item the media item for which buffering is happening
*/
- public void onMediaPrepared(@NonNull MediaSession2 session, @NonNull MediaPlayerBase player,
- @NonNull MediaItem2 item) { }
+ public void onMediaPrepared(@NonNull MediaSession2 session,
+ @NonNull MediaPlayerInterface player, @NonNull MediaItem2 item) { }
/**
* Called to indicate that the state of the player has changed.
- * See {@link MediaPlayerBase#getPlayerState()} for polling the player state.
+ * See {@link MediaPlayerInterface#getPlayerState()} for polling the player state.
* @param session the session for this event
* @param player the player for this event
* @param state the new state of the player.
*/
public void onPlayerStateChanged(@NonNull MediaSession2 session,
- @NonNull MediaPlayerBase player, @PlayerState int state) { }
+ @NonNull MediaPlayerInterface player, @PlayerState int state) { }
/**
* Called to report buffering events for a data source.
@@ -492,7 +1088,8 @@
* @param state the new buffering state.
*/
public void onBufferingStateChanged(@NonNull MediaSession2 session,
- @NonNull MediaPlayerBase player, @NonNull MediaItem2 item, @BuffState int state) { }
+ @NonNull MediaPlayerInterface player, @NonNull MediaItem2 item,
+ @BuffState int state) { }
/**
* Called to indicate that the playback speed has changed.
@@ -501,18 +1098,18 @@
* @param speed the new playback speed.
*/
public void onPlaybackSpeedChanged(@NonNull MediaSession2 session,
- @NonNull MediaPlayerBase player, float speed) { }
+ @NonNull MediaPlayerInterface player, float speed) { }
/**
* Called to indicate that {@link #seekTo(long)} is completed.
*
* @param session the session for this event.
- * @param mpb the player that has completed seeking.
+ * @param player the player that has completed seeking.
* @param position the previous seeking request.
* @see #seekTo(long)
*/
- public void onSeekCompleted(@NonNull MediaSession2 session, @NonNull MediaPlayerBase mpb,
- long position) { }
+ public void onSeekCompleted(@NonNull MediaSession2 session,
+ @NonNull MediaPlayerInterface player, long position) { }
/**
* Called when a playlist is changed from the {@link MediaPlaylistAgent}.
@@ -571,153 +1168,6 @@
}
/**
- * Base builder class for MediaSession2 and its subclass. Any change in this class should be
- * also applied to the subclasses {@link MediaSession2.Builder} and
- * {@link MediaLibraryService2.MediaLibrarySession.Builder}.
- * <p>
- * APIs here should be package private, but should have documentations for developers.
- * Otherwise, javadoc will generate documentation with the generic types such as follows.
- * <pre>U extends BuilderBase<T, U, C> setSessionCallback(Executor executor, C callback)</pre>
- * <p>
- * This class is hidden to prevent from generating test stub, which fails with
- * 'unexpected bound' because it tries to auto generate stub class as follows.
- * <pre>abstract static class BuilderBase<
- * T extends android.media.MediaSession2,
- * U extends android.media.MediaSession2.BuilderBase<
- * T, U, C extends android.media.MediaSession2.SessionCallback>, C></pre>
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- abstract static class BuilderBase
- <T extends MediaSession2, U extends BuilderBase<T, U, C>, C extends SessionCallback> {
- final Context mContext;
- MediaSession2ImplBase.BuilderBase<T, C> mBaseImpl;
- MediaPlayerBase mPlayer;
- String mId;
- Executor mCallbackExecutor;
- C mCallback;
- MediaPlaylistAgent mPlaylistAgent;
- VolumeProviderCompat mVolumeProvider;
- PendingIntent mSessionActivity;
-
- BuilderBase(Context context) {
- if (context == null) {
- throw new IllegalArgumentException("context shouldn't be null");
- }
- mContext = context;
- // Ensure non-null
- mId = "";
- }
-
- /**
- * Sets the underlying {@link MediaPlayerBase} for this session to dispatch incoming event
- * to.
- *
- * @param player a {@link MediaPlayerBase} that handles actual media playback in your app.
- */
- @NonNull U setPlayer(@NonNull MediaPlayerBase player) {
- if (player == null) {
- throw new IllegalArgumentException("player shouldn't be null");
- }
- mBaseImpl.setPlayer(player);
- return (U) this;
- }
-
- /**
- * Sets the {@link MediaPlaylistAgent} for this session to manages playlist of the
- * underlying {@link MediaPlayerBase}. The playlist agent should manage
- * {@link MediaPlayerBase} for calling {@link MediaPlayerBase#setNextDataSources(List)}.
- * <p>
- * If the {@link MediaPlaylistAgent} isn't set, session will create the default playlist
- * agent.
- *
- * @param playlistAgent a {@link MediaPlaylistAgent} that manages playlist of the
- * {@code player}
- */
- U setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) {
- if (playlistAgent == null) {
- throw new IllegalArgumentException("playlistAgent shouldn't be null");
- }
- mBaseImpl.setPlaylistAgent(playlistAgent);
- return (U) this;
- }
-
- /**
- * Sets the {@link VolumeProviderCompat} for this session to handle volume events. If not
- * set, system will adjust the appropriate stream volume for this session's player.
- *
- * @param volumeProvider The provider that will receive volume button events.
- */
- @NonNull U setVolumeProvider(@Nullable VolumeProviderCompat volumeProvider) {
- mBaseImpl.setVolumeProvider(volumeProvider);
- return (U) this;
- }
-
- /**
- * Set an intent for launching UI for this Session. This can be used as a
- * quick link to an ongoing media screen. The intent should be for an
- * activity that may be started using {@link Context#startActivity(Intent)}.
- *
- * @param pi The intent to launch to show UI for this session.
- */
- @NonNull U setSessionActivity(@Nullable PendingIntent pi) {
- mBaseImpl.setSessionActivity(pi);
- return (U) this;
- }
-
- /**
- * Set ID of the session. If it's not set, an empty string with used to create a session.
- * <p>
- * Use this if and only if your app supports multiple playback at the same time and also
- * wants to provide external apps to have finer controls of them.
- *
- * @param id id of the session. Must be unique per package.
- * @throws IllegalArgumentException if id is {@code null}
- * @return
- */
- @NonNull U setId(@NonNull String id) {
- if (id == null) {
- throw new IllegalArgumentException("id shouldn't be null");
- }
- mBaseImpl.setId(id);
- return (U) this;
- }
-
- /**
- * Set callback for the session.
- *
- * @param executor callback executor
- * @param callback session callback.
- * @return
- */
- @NonNull U setSessionCallback(@NonNull Executor executor, @NonNull C callback) {
- if (executor == null) {
- throw new IllegalArgumentException("executor shouldn't be null");
- }
- if (callback == null) {
- throw new IllegalArgumentException("callback shouldn't be null");
- }
- mBaseImpl.setSessionCallback(executor, callback);
- return (U) this;
- }
-
- /**
- * Build {@link MediaSession2}.
- *
- * @return a new session
- * @throws IllegalStateException if the session with the same id is already exists for the
- * package.
- */
- @NonNull T build() {
- return mBaseImpl.build();
- }
-
- void setImpl(MediaSession2ImplBase.BuilderBase<T, C> impl) {
- mBaseImpl = impl;
- }
- }
-
- /**
* Builder for {@link MediaSession2}.
* <p>
* Any incoming event from the {@link MediaController2} will be handled on the thread
@@ -733,7 +1183,7 @@
}
@Override
- public @NonNull Builder setPlayer(@NonNull MediaPlayerBase player) {
+ public @NonNull Builder setPlayer(@NonNull MediaPlayerInterface player) {
return super.setPlayer(player);
}
@@ -775,20 +1225,18 @@
public static final class ControllerInfo {
private final int mUid;
private final String mPackageName;
- // Note: IMediaControllerCallback should be used only for MediaSession2ImplBase
- private final IMediaControllerCallback mIControllerCallback;
private final boolean mIsTrusted;
+ private final ControllerCb mControllerCb;
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
- public ControllerInfo(@NonNull Context context, int uid, int pid,
- @NonNull String packageName, @NonNull IMediaControllerCallback callback) {
+ ControllerInfo(@NonNull String packageName, int pid, int uid, @NonNull ControllerCb cb) {
mUid = uid;
mPackageName = packageName;
- mIControllerCallback = callback;
mIsTrusted = false;
+ mControllerCb = cb;
}
/**
@@ -818,13 +1266,9 @@
return mIsTrusted;
}
- IBinder getId() {
- return mIControllerCallback.asBinder();
- }
-
@Override
public int hashCode() {
- return mIControllerCallback.hashCode();
+ return mControllerCb.hashCode();
}
@Override
@@ -833,7 +1277,7 @@
return false;
}
ControllerInfo other = (ControllerInfo) obj;
- return mIControllerCallback.asBinder().equals(other.mIControllerCallback.asBinder());
+ return mControllerCb.equals(other.mControllerCb);
}
@Override
@@ -841,26 +1285,12 @@
return "ControllerInfo {pkg=" + mPackageName + ", uid=" + mUid + "})";
}
- /**
- * @hide
- * @return Bundle
- */
- @RestrictTo(LIBRARY_GROUP)
- public @NonNull Bundle toBundle() {
- return new Bundle();
+ @NonNull IBinder getId() {
+ return mControllerCb.getId();
}
- /**
- * @hide
- * @return Bundle
- */
- @RestrictTo(LIBRARY_GROUP)
- public static @NonNull ControllerInfo fromBundle(@NonNull Context context, Bundle bundle) {
- return new ControllerInfo(context, -1, -1, "TODO", null);
- }
-
- IMediaControllerCallback getControllerBinder() {
- return mIControllerCallback;
+ @NonNull ControllerCb getControllerCb() {
+ return mControllerCb;
}
}
@@ -1060,12 +1490,60 @@
}
}
+ abstract static class ControllerCb {
+ @Override
+ public int hashCode() {
+ return getId().hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof ControllerCb)) {
+ return false;
+ }
+ ControllerCb other = (ControllerCb) obj;
+ return getId().equals(other.getId());
+ }
+
+ abstract @NonNull IBinder getId();
+
+ // Mostly matched with the methods in MediaController2.ControllerCallback
+ abstract void onCustomLayoutChanged(@NonNull List<CommandButton> layout)
+ throws RemoteException;
+ abstract void onPlaybackInfoChanged(@NonNull PlaybackInfo info) throws RemoteException;
+ abstract void onAllowedCommandsChanged(@NonNull SessionCommandGroup2 commands)
+ throws RemoteException;
+ abstract void onCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args,
+ @Nullable ResultReceiver receiver) throws RemoteException;
+ abstract void onPlayerStateChanged(int playerState) throws RemoteException;
+ abstract void onPlaybackSpeedChanged(float speed) throws RemoteException;
+ abstract void onBufferingStateChanged(@NonNull MediaItem2 item,
+ @MediaPlayerInterface.BuffState int state) throws RemoteException;
+ abstract void onSeekCompleted(long position) throws RemoteException;
+ abstract void onError(@ErrorCode int errorCode, @Nullable Bundle extras)
+ throws RemoteException;
+ abstract void onCurrentMediaItemChanged(@Nullable MediaItem2 item) throws RemoteException;
+ abstract void onPlaylistChanged(@NonNull List<MediaItem2> playlist,
+ @Nullable MediaMetadata2 metadata) throws RemoteException;
+ abstract void onPlaylistMetadataChanged(@Nullable MediaMetadata2 metadata)
+ throws RemoteException;
+ abstract void onShuffleModeChanged(@MediaPlaylistAgent.ShuffleMode int shuffleMode)
+ throws RemoteException;
+ abstract void onRepeatModeChanged(@MediaPlaylistAgent.RepeatMode int repeatMode)
+ throws RemoteException;
+ abstract void onRoutesInfoChanged(@Nullable List<Bundle> routes) throws RemoteException;
+ abstract void onChildrenChanged(@NonNull String parentId, int itemCount,
+ @Nullable Bundle extras) throws RemoteException;
+ abstract void onSearchResultChanged(@NonNull String query, int itemCount,
+ @Nullable Bundle extras) throws RemoteException;
+ }
+
abstract static class SupportLibraryImpl extends MediaInterface2.SessionPlayer
implements AutoCloseable {
- abstract void updatePlayer(@NonNull MediaPlayerBase player,
+ abstract void updatePlayer(@NonNull MediaPlayerInterface player,
@Nullable MediaPlaylistAgent playlistAgent,
@Nullable VolumeProviderCompat volumeProvider);
- abstract @NonNull MediaPlayerBase getPlayer();
+ abstract @NonNull MediaPlayerInterface getPlayer();
abstract @NonNull MediaPlaylistAgent getPlaylistAgent();
abstract @Nullable VolumeProviderCompat getVolumeProvider();
abstract @NonNull SessionToken2 getToken();
@@ -1083,9 +1561,17 @@
abstract void notifyRoutesInfoChanged(@NonNull ControllerInfo controller,
@Nullable List<Bundle> routes);
+ // LibrarySession methods
+ abstract void notifyChildrenChanged(@NonNull ControllerInfo controller,
+ @NonNull String parentId, int itemCount, @Nullable Bundle extras,
+ @NonNull List<MediaSessionManager.RemoteUserInfo> subscribingBrowsers);
+ abstract void notifySearchResultChanged(@NonNull ControllerInfo controller,
+ @NonNull String query, int itemCount, @Nullable Bundle extras);
+
// Internally used methods
- abstract void setInstance(MediaSession2 session);
+ abstract MediaSession2 createInstance();
abstract MediaSession2 getInstance();
+ abstract MediaSessionCompat getSessionCompat();
abstract Context getContext();
abstract Executor getCallbackExecutor();
abstract SessionCallback getCallback();
@@ -1094,577 +1580,152 @@
abstract PlaybackInfo getPlaybackInfo();
}
- static final String TAG = "MediaSession2";
-
- private final SupportLibraryImpl mImpl;
-
- MediaSession2(SupportLibraryImpl impl) {
- mImpl = impl;
- mImpl.setInstance(this);
- }
-
/**
- * Sets the underlying {@link MediaPlayerBase} and {@link MediaPlaylistAgent} for this session
- * to dispatch incoming event to.
+ * Base builder class for MediaSession2 and its subclass. Any change in this class should be
+ * also applied to the subclasses {@link MediaSession2.Builder} and
+ * {@link MediaLibraryService2.MediaLibrarySession.Builder}.
* <p>
- * When a {@link MediaPlaylistAgent} is specified here, the playlist agent should manage
- * {@link MediaPlayerBase} for calling {@link MediaPlayerBase#setNextDataSources(List)}.
+ * APIs here should be package private, but should have documentations for developers.
+ * Otherwise, javadoc will generate documentation with the generic types such as follows.
+ * <pre>U extends BuilderBase<T, U, C> setSessionCallback(Executor executor, C callback)</pre>
* <p>
- * If the {@link MediaPlaylistAgent} isn't set, session will recreate the default playlist
- * agent.
- *
- * @param player a {@link MediaPlayerBase} that handles actual media playback in your app
- * @param playlistAgent a {@link MediaPlaylistAgent} that manages playlist of the {@code player}
- * @param volumeProvider a {@link VolumeProviderCompat}. If {@code null}, system will adjust the
- * appropriate stream volume for this session's player.
+ * This class is hidden to prevent from generating test stub, which fails with
+ * 'unexpected bound' because it tries to auto generate stub class as follows.
+ * <pre>abstract static class BuilderBase<
+ * T extends android.media.MediaSession2,
+ * U extends android.media.MediaSession2.BuilderBase<
+ * T, U, C extends android.media.MediaSession2.SessionCallback>, C></pre>
+ * @hide
*/
- public void updatePlayer(@NonNull MediaPlayerBase player,
- @Nullable MediaPlaylistAgent playlistAgent,
- @Nullable VolumeProviderCompat volumeProvider) {
- mImpl.updatePlayer(player, playlistAgent, volumeProvider);
- }
+ @RestrictTo(LIBRARY_GROUP)
+ abstract static class BuilderBase
+ <T extends MediaSession2, U extends BuilderBase<T, U, C>, C extends SessionCallback> {
+ final Context mContext;
+ MediaSession2ImplBase.BuilderBase<T, C> mBaseImpl;
+ MediaPlayerInterface mPlayer;
+ String mId;
+ Executor mCallbackExecutor;
+ C mCallback;
+ MediaPlaylistAgent mPlaylistAgent;
+ VolumeProviderCompat mVolumeProvider;
+ PendingIntent mSessionActivity;
- @Override
- public void close() {
- try {
- mImpl.close();
- } catch (Exception e) {
- // Should not be here.
+ BuilderBase(Context context) {
+ if (context == null) {
+ throw new IllegalArgumentException("context shouldn't be null");
+ }
+ mContext = context;
+ // Ensure non-null
+ mId = "";
}
- }
- /**
- * @return player
- */
- public @NonNull MediaPlayerBase getPlayer() {
- return mImpl.getPlayer();
- }
+ /**
+ * Sets the underlying {@link MediaPlayerInterface} for this session to dispatch incoming
+ * event to.
+ *
+ * @param player a {@link MediaPlayerInterface} that handles actual media playback in your
+ * app.
+ */
+ @NonNull U setPlayer(@NonNull MediaPlayerInterface player) {
+ if (player == null) {
+ throw new IllegalArgumentException("player shouldn't be null");
+ }
+ mBaseImpl.setPlayer(player);
+ return (U) this;
+ }
- /**
- * @return playlist agent
- */
- public @NonNull MediaPlaylistAgent getPlaylistAgent() {
- return mImpl.getPlaylistAgent();
- }
+ /**
+ * Sets the {@link MediaPlaylistAgent} for this session to manages playlist of the
+ * underlying {@link MediaPlayerInterface}. The playlist agent should manage
+ * {@link MediaPlayerInterface} for calling
+ * {@link MediaPlayerInterface#setNextDataSources(List)}.
+ * <p>
+ * If the {@link MediaPlaylistAgent} isn't set, session will create the default playlist
+ * agent.
+ *
+ * @param playlistAgent a {@link MediaPlaylistAgent} that manages playlist of the
+ * {@code player}
+ */
+ U setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) {
+ if (playlistAgent == null) {
+ throw new IllegalArgumentException("playlistAgent shouldn't be null");
+ }
+ mBaseImpl.setPlaylistAgent(playlistAgent);
+ return (U) this;
+ }
- /**
- * @return volume provider
- */
- public @Nullable VolumeProviderCompat getVolumeProvider() {
- return mImpl.getVolumeProvider();
- }
+ /**
+ * Sets the {@link VolumeProviderCompat} for this session to handle volume events. If not
+ * set, system will adjust the appropriate stream volume for this session's player.
+ *
+ * @param volumeProvider The provider that will receive volume button events.
+ */
+ @NonNull U setVolumeProvider(@Nullable VolumeProviderCompat volumeProvider) {
+ mBaseImpl.setVolumeProvider(volumeProvider);
+ return (U) this;
+ }
- /**
- * Returns the {@link SessionToken2} for creating {@link MediaController2}.
- */
- public @NonNull SessionToken2 getToken() {
- return mImpl.getToken();
- }
+ /**
+ * Set an intent for launching UI for this Session. This can be used as a
+ * quick link to an ongoing media screen. The intent should be for an
+ * activity that may be started using {@link Context#startActivity(Intent)}.
+ *
+ * @param pi The intent to launch to show UI for this session.
+ */
+ @NonNull U setSessionActivity(@Nullable PendingIntent pi) {
+ mBaseImpl.setSessionActivity(pi);
+ return (U) this;
+ }
- @NonNull Context getContext() {
- return mImpl.getContext();
- }
+ /**
+ * Set ID of the session. If it's not set, an empty string with used to create a session.
+ * <p>
+ * Use this if and only if your app supports multiple playback at the same time and also
+ * wants to provide external apps to have finer controls of them.
+ *
+ * @param id id of the session. Must be unique per package.
+ * @throws IllegalArgumentException if id is {@code null}
+ * @return
+ */
+ @NonNull U setId(@NonNull String id) {
+ if (id == null) {
+ throw new IllegalArgumentException("id shouldn't be null");
+ }
+ mBaseImpl.setId(id);
+ return (U) this;
+ }
- @NonNull Executor getCallbackExecutor() {
- return mImpl.getCallbackExecutor();
- }
+ /**
+ * Set callback for the session.
+ *
+ * @param executor callback executor
+ * @param callback session callback.
+ * @return
+ */
+ @NonNull U setSessionCallback(@NonNull Executor executor, @NonNull C callback) {
+ if (executor == null) {
+ throw new IllegalArgumentException("executor shouldn't be null");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("callback shouldn't be null");
+ }
+ mBaseImpl.setSessionCallback(executor, callback);
+ return (U) this;
+ }
- @NonNull SessionCallback getCallback() {
- return mImpl.getCallback();
- }
+ /**
+ * Build {@link MediaSession2}.
+ *
+ * @return a new session
+ * @throws IllegalStateException if the session with the same id is already exists for the
+ * package.
+ */
+ @NonNull T build() {
+ return mBaseImpl.build();
+ }
- /**
- * Returns the list of connected controller.
- *
- * @return list of {@link ControllerInfo}
- */
- public @NonNull List<ControllerInfo> getConnectedControllers() {
- return mImpl.getConnectedControllers();
- }
-
- /**
- * Set the {@link AudioFocusRequest} to obtain the audio focus
- *
- * @param afr the full request parameters
- */
- public void setAudioFocusRequest(@Nullable AudioFocusRequest afr) {
- mImpl.setAudioFocusRequest(afr);
- }
-
- /**
- * Sets ordered list of {@link CommandButton} for controllers to build UI with it.
- * <p>
- * It's up to controller's decision how to represent the layout in its own UI.
- * Here's the same way
- * (layout[i] means a CommandButton at index i in the given list)
- * For 5 icons row
- * layout[3] layout[1] layout[0] layout[2] layout[4]
- * For 3 icons row
- * layout[1] layout[0] layout[2]
- * For 5 icons row with overflow icon (can show +5 extra buttons with overflow button)
- * expanded row: layout[5] layout[6] layout[7] layout[8] layout[9]
- * main row: layout[3] layout[1] layout[0] layout[2] layout[4]
- * <p>
- * This API can be called in the
- * {@link SessionCallback#onConnect(MediaSession2, ControllerInfo)}.
- *
- * @param controller controller to specify layout.
- * @param layout ordered list of layout.
- */
- public void setCustomLayout(@NonNull ControllerInfo controller,
- @NonNull List<CommandButton> layout) {
- mImpl.setCustomLayout(controller, layout);
- }
-
- /**
- * Set the new allowed command group for the controller
- *
- * @param controller controller to change allowed commands
- * @param commands new allowed commands
- */
- public void setAllowedCommands(@NonNull ControllerInfo controller,
- @NonNull SessionCommandGroup2 commands) {
- mImpl.setAllowedCommands(controller, commands);
- }
-
- /**
- * Send custom command to all connected controllers.
- *
- * @param command a command
- * @param args optional argument
- */
- public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args) {
- mImpl.sendCustomCommand(command, args);
- }
-
- /**
- * Send custom command to a specific controller.
- *
- * @param command a command
- * @param args optional argument
- * @param receiver result receiver for the session
- */
- public void sendCustomCommand(@NonNull ControllerInfo controller,
- @NonNull SessionCommand2 command, @Nullable Bundle args,
- @Nullable ResultReceiver receiver) {
- mImpl.sendCustomCommand(controller, command, args, receiver);
- }
-
- /**
- * Play playback.
- * <p>
- * This calls {@link MediaPlayerBase#play()}.
- */
- @Override
- public void play() {
- mImpl.play();
- }
-
- /**
- * Pause playback.
- * <p>
- * This calls {@link MediaPlayerBase#pause()}.
- */
- @Override
- public void pause() {
- mImpl.pause();
- }
-
- /**
- * Stop playback, and reset the player to the initial state.
- * <p>
- * This calls {@link MediaPlayerBase#reset()}.
- */
- @Override
- public void reset() {
- mImpl.reset();
- }
-
- /**
- * Request that the player prepare its playback. In other words, other sessions can continue
- * to play during the preparation of this session. This method can be used to speed up the
- * start of the playback. Once the preparation is done, the session will change its playback
- * state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be called
- * to start playback.
- * <p>
- * This calls {@link MediaPlayerBase#reset()}.
- */
- @Override
- public void prepare() {
- mImpl.prepare();
- }
-
- /**
- * Move to a new location in the media stream.
- *
- * @param pos Position to move to, in milliseconds.
- */
- @Override
- public void seekTo(long pos) {
- mImpl.seekTo(pos);
- }
-
- /**
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- @Override
- public void skipForward() {
- mImpl.skipForward();
- }
-
- /**
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- @Override
- public void skipBackward() {
- mImpl.skipBackward();
- }
-
- /**
- * Notify errors to the connected controllers
- *
- * @param errorCode error code
- * @param extras extras
- */
- @Override
- public void notifyError(@ErrorCode int errorCode, @Nullable Bundle extras) {
- mImpl.notifyError(errorCode, extras);
- }
-
- /**
- * Notify routes information to a connected controller
- *
- * @param controller controller information
- * @param routes The routes information. Each bundle should be from
- * MediaRouteDescritor.asBundle().
- */
- public void notifyRoutesInfoChanged(@NonNull ControllerInfo controller,
- @Nullable List<Bundle> routes) {
- mImpl.notifyRoutesInfoChanged(controller, routes);
- }
-
- /**
- * Gets the current player state.
- *
- * @return the current player state
- */
- @Override
- public @PlayerState int getPlayerState() {
- return mImpl.getPlayerState();
- }
-
- /**
- * Gets the current position.
- *
- * @return the current playback position in ms, or {@link MediaPlayerBase#UNKNOWN_TIME} if
- * unknown.
- */
- @Override
- public long getCurrentPosition() {
- return mImpl.getCurrentPosition();
- }
-
- @Override
- public long getDuration() {
- return mImpl.getDuration();
- }
-
- /**
- * Gets the buffered position, or {@link MediaPlayerBase#UNKNOWN_TIME} if unknown.
- *
- * @return the buffered position in ms, or {@link MediaPlayerBase#UNKNOWN_TIME}.
- */
- @Override
- public long getBufferedPosition() {
- return mImpl.getBufferedPosition();
- }
-
- /**
- * Gets the current buffering state of the player.
- * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
- * buffered.
- *
- * @return the buffering state.
- */
- @Override
- public @BuffState int getBufferingState() {
- return mImpl.getBufferingState();
- }
-
- /**
- * Get the playback speed.
- *
- * @return speed
- */
- @Override
- public float getPlaybackSpeed() {
- return mImpl.getPlaybackSpeed();
- }
-
- /**
- * Set the playback speed.
- */
- @Override
- public void setPlaybackSpeed(float speed) {
- mImpl.setPlaybackSpeed(speed);
- }
-
- /**
- * Sets the data source missing helper. Helper will be used to provide default implementation of
- * {@link MediaPlaylistAgent} when it isn't set by developer.
- * <p>
- * Default implementation of the {@link MediaPlaylistAgent} will call helper when a
- * {@link MediaItem2} in the playlist doesn't have a {@link DataSourceDesc}. This may happen
- * when
- * <ul>
- * <li>{@link MediaItem2} specified by {@link #setPlaylist(List, MediaMetadata2)} doesn't
- * have {@link DataSourceDesc}</li>
- * <li>{@link MediaController2#addPlaylistItem(int, MediaItem2)} is called and accepted
- * by {@link SessionCallback#onCommandRequest(
- * MediaSession2, ControllerInfo, SessionCommand2)}.
- * In that case, an item would be added automatically without the data source.</li>
- * </ul>
- * <p>
- * If it's not set, playback wouldn't happen for the item without data source descriptor.
- * <p>
- * The helper will be run on the executor that was specified by
- * {@link Builder#setSessionCallback(Executor, SessionCallback)}.
- *
- * @param helper a data source missing helper.
- * @throws IllegalStateException when the helper is set when the playlist agent is set
- * @see #setPlaylist(List, MediaMetadata2)
- * @see SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)
- * @see SessionCommand2#COMMAND_CODE_PLAYLIST_ADD_ITEM
- * @see SessionCommand2#COMMAND_CODE_PLAYLIST_REPLACE_ITEM
- */
- @Override
- public void setOnDataSourceMissingHelper(@NonNull OnDataSourceMissingHelper helper) {
- mImpl.setOnDataSourceMissingHelper(helper);
- }
-
- /**
- * Clears the data source missing helper.
- *
- * @see #setOnDataSourceMissingHelper(OnDataSourceMissingHelper)
- */
- @Override
- public void clearOnDataSourceMissingHelper() {
- mImpl.clearOnDataSourceMissingHelper();
- }
-
- /**
- * Returns the playlist from the {@link MediaPlaylistAgent}.
- * <p>
- * This list may differ with the list that was specified with
- * {@link #setPlaylist(List, MediaMetadata2)} depending on the {@link MediaPlaylistAgent}
- * implementation. Use media items returned here for other playlist agent APIs such as
- * {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}.
- *
- * @return playlist
- * @see MediaPlaylistAgent#getPlaylist()
- * @see SessionCallback#onPlaylistChanged(
- * MediaSession2, MediaPlaylistAgent, List, MediaMetadata2)
- */
- @Override
- public List<MediaItem2> getPlaylist() {
- return mImpl.getPlaylist();
- }
-
- /**
- * Sets a list of {@link MediaItem2} to the {@link MediaPlaylistAgent}. Ensure uniqueness of
- * each {@link MediaItem2} in the playlist so the session can uniquely identity individual
- * items.
- * <p>
- * This may be an asynchronous call, and {@link MediaPlaylistAgent} may keep the copy of the
- * list. Wait for {@link SessionCallback#onPlaylistChanged(MediaSession2, MediaPlaylistAgent,
- * List, MediaMetadata2)} to know the operation finishes.
- * <p>
- * You may specify a {@link MediaItem2} without {@link DataSourceDesc}. In that case,
- * {@link MediaPlaylistAgent} has responsibility to dynamically query {link DataSourceDesc}
- * when such media item is ready for preparation or play. Default implementation needs
- * {@link OnDataSourceMissingHelper} for such case.
- *
- * @param list A list of {@link MediaItem2} objects to set as a play list.
- * @throws IllegalArgumentException if given list is {@code null}, or has duplicated media
- * items.
- * @see MediaPlaylistAgent#setPlaylist(List, MediaMetadata2)
- * @see SessionCallback#onPlaylistChanged(
- * MediaSession2, MediaPlaylistAgent, List, MediaMetadata2)
- * @see #setOnDataSourceMissingHelper
- */
- @Override
- public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
- mImpl.setPlaylist(list, metadata);
- }
-
- /**
- * Skips to the item in the playlist.
- * <p>
- * This calls {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)} and the behavior depends
- * on the playlist agent implementation, especially with the shuffle/repeat mode.
- *
- * @param item The item in the playlist you want to play
- * @see #getShuffleMode()
- * @see #getRepeatMode()
- */
- @Override
- public void skipToPlaylistItem(@NonNull MediaItem2 item) {
- mImpl.skipToPlaylistItem(item);
- }
-
- /**
- * Skips to the previous item.
- * <p>
- * This calls {@link MediaPlaylistAgent#skipToPreviousItem()} and the behavior depends on the
- * playlist agent implementation, especially with the shuffle/repeat mode.
- *
- * @see #getShuffleMode()
- * @see #getRepeatMode()
- **/
- @Override
- public void skipToPreviousItem() {
- mImpl.skipToPreviousItem();
- }
-
- /**
- * Skips to the next item.
- * <p>
- * This calls {@link MediaPlaylistAgent#skipToNextItem()} and the behavior depends on the
- * playlist agent implementation, especially with the shuffle/repeat mode.
- *
- * @see #getShuffleMode()
- * @see #getRepeatMode()
- */
- @Override
- public void skipToNextItem() {
- mImpl.skipToNextItem();
- }
-
- /**
- * Gets the playlist metadata from the {@link MediaPlaylistAgent}.
- *
- * @return the playlist metadata
- */
- @Override
- public MediaMetadata2 getPlaylistMetadata() {
- return mImpl.getPlaylistMetadata();
- }
-
- /**
- * Adds the media item to the playlist at position index. Index equals or greater than
- * the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of
- * the playlist.
- * <p>
- * This will not change the currently playing media item.
- * If index is less than or equal to the current index of the play list,
- * the current index of the play list will be incremented correspondingly.
- *
- * @param index the index you want to add
- * @param item the media item you want to add
- */
- @Override
- public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
- mImpl.addPlaylistItem(index, item);
- }
-
- /**
- * Removes the media item in the playlist.
- * <p>
- * If the item is the currently playing item of the playlist, current playback
- * will be stopped and playback moves to next source in the list.
- *
- * @param item the media item you want to add
- */
- @Override
- public void removePlaylistItem(@NonNull MediaItem2 item) {
- mImpl.removePlaylistItem(item);
- }
-
- /**
- * Replaces the media item at index in the playlist. This can be also used to update metadata of
- * an item.
- *
- * @param index the index of the item to replace
- * @param item the new item
- */
- @Override
- public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
- mImpl.replacePlaylistItem(index, item);
- }
-
- /**
- * Return currently playing media item.
- *
- * @return currently playing media item
- */
- @Override
- public MediaItem2 getCurrentMediaItem() {
- return mImpl.getCurrentMediaItem();
- }
-
- /**
- * Updates the playlist metadata to the {@link MediaPlaylistAgent}.
- *
- * @param metadata metadata of the playlist
- */
- @Override
- public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
- mImpl.updatePlaylistMetadata(metadata);
- }
-
- /**
- * Gets the repeat mode from the {@link MediaPlaylistAgent}.
- *
- * @return repeat mode
- * @see MediaPlaylistAgent#REPEAT_MODE_NONE
- * @see MediaPlaylistAgent#REPEAT_MODE_ONE
- * @see MediaPlaylistAgent#REPEAT_MODE_ALL
- * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
- */
- @Override
- public @RepeatMode int getRepeatMode() {
- return mImpl.getRepeatMode();
- }
-
- /**
- * Sets the repeat mode to the {@link MediaPlaylistAgent}.
- *
- * @param repeatMode repeat mode
- * @see MediaPlaylistAgent#REPEAT_MODE_NONE
- * @see MediaPlaylistAgent#REPEAT_MODE_ONE
- * @see MediaPlaylistAgent#REPEAT_MODE_ALL
- * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
- */
- @Override
- public void setRepeatMode(@RepeatMode int repeatMode) {
- mImpl.setRepeatMode(repeatMode);
- }
-
- /**
- * Gets the shuffle mode from the {@link MediaPlaylistAgent}.
- *
- * @return The shuffle mode
- * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
- * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
- * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
- */
- @Override
- public @ShuffleMode int getShuffleMode() {
- return mImpl.getShuffleMode();
- }
-
- /**
- * Sets the shuffle mode to the {@link MediaPlaylistAgent}.
- *
- * @param shuffleMode The shuffle mode
- * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
- * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
- * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
- */
- @Override
- public void setShuffleMode(@ShuffleMode int shuffleMode) {
- mImpl.setShuffleMode(shuffleMode);
+ void setImpl(MediaSession2ImplBase.BuilderBase<T, C> impl) {
+ mBaseImpl = impl;
+ }
}
}
diff --git a/media/src/main/java/androidx/media/MediaSession2ImplBase.java b/media/src/main/java/androidx/media/MediaSession2ImplBase.java
index e474b45..1e768a0 100644
--- a/media/src/main/java/androidx/media/MediaSession2ImplBase.java
+++ b/media/src/main/java/androidx/media/MediaSession2ImplBase.java
@@ -16,7 +16,8 @@
package androidx.media;
-import static androidx.media.MediaPlayerBase.BUFFERING_STATE_UNKNOWN;
+import static androidx.media.MediaPlayerInterface.BUFFERING_STATE_UNKNOWN;
+import static androidx.media.MediaSession2.ControllerCb;
import static androidx.media.MediaSession2.ControllerInfo;
import static androidx.media.MediaSession2.ErrorCode;
import static androidx.media.MediaSession2.OnDataSourceMissingHelper;
@@ -35,9 +36,11 @@
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
+import android.os.DeadObjectException;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Process;
+import android.os.RemoteException;
import android.os.ResultReceiver;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
@@ -47,8 +50,9 @@
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.util.ObjectsCompat;
import androidx.media.MediaController2.PlaybackInfo;
-import androidx.media.MediaPlayerBase.PlayerEventCallback;
+import androidx.media.MediaPlayerInterface.PlayerEventCallback;
import androidx.media.MediaPlaylistAgent.PlaylistEventCallback;
import java.lang.ref.WeakReference;
@@ -68,19 +72,19 @@
private final HandlerThread mHandlerThread;
private final Handler mHandler;
private final MediaSessionCompat mSessionCompat;
- private final MediaSession2StubImplBase mSession2Stub;
+ private final MediaSession2Stub mSession2Stub;
+ private final MediaSessionLegacyStub mSessionLegacyStub;
private final String mId;
private final Executor mCallbackExecutor;
private final SessionCallback mCallback;
private final SessionToken2 mSessionToken;
private final AudioManager mAudioManager;
- private final MediaPlayerBase.PlayerEventCallback mPlayerEventCallback;
+ private final MediaPlayerInterface.PlayerEventCallback mPlayerEventCallback;
private final MediaPlaylistAgent.PlaylistEventCallback mPlaylistEventCallback;
-
- private WeakReference<MediaSession2> mInstance;
+ private final MediaSession2 mInstance;
@GuardedBy("mLock")
- private MediaPlayerBase mPlayer;
+ private MediaPlayerInterface mPlayer;
@GuardedBy("mLock")
private MediaPlaylistAgent mPlaylistAgent;
@GuardedBy("mLock")
@@ -90,21 +94,21 @@
@GuardedBy("mLock")
private OnDataSourceMissingHelper mDsmHelper;
@GuardedBy("mLock")
- private PlaybackStateCompat mPlaybackStateCompat;
- @GuardedBy("mLock")
private PlaybackInfo mPlaybackInfo;
MediaSession2ImplBase(Context context, MediaSessionCompat sessionCompat, String id,
- MediaPlayerBase player, MediaPlaylistAgent playlistAgent,
+ MediaPlayerInterface player, MediaPlaylistAgent playlistAgent,
VolumeProviderCompat volumeProvider, PendingIntent sessionActivity,
Executor callbackExecutor, SessionCallback callback) {
mContext = context;
+ mInstance = createInstance();
mHandlerThread = new HandlerThread("MediaController2_Thread");
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
mSessionCompat = sessionCompat;
- mSession2Stub = new MediaSession2StubImplBase(this);
+ mSession2Stub = new MediaSession2Stub(this);
+ mSessionLegacyStub = new MediaSessionLegacyStub(this);
mSessionCompat.setCallback(mSession2Stub, mHandler);
mSessionCompat.setSessionActivity(sessionActivity);
@@ -113,7 +117,6 @@
mCallbackExecutor = callbackExecutor;
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
- // TODO: Set callback values properly
mPlayerEventCallback = new MyPlayerEventCallback(this);
mPlaylistEventCallback = new MyPlaylistEventCallback(this);
@@ -137,16 +140,22 @@
}
@Override
- public void updatePlayer(@NonNull MediaPlayerBase player,
+ public void updatePlayer(@NonNull MediaPlayerInterface player,
@Nullable MediaPlaylistAgent playlistAgent,
@Nullable VolumeProviderCompat volumeProvider) {
if (player == null) {
throw new IllegalArgumentException("player shouldn't be null");
}
- final MediaPlayerBase oldPlayer;
+ final boolean hasPlayerChanged;
+ final boolean hasAgentChanged;
+ final boolean hasPlaybackInfoChanged;
+ final MediaPlayerInterface oldPlayer;
final MediaPlaylistAgent oldAgent;
final PlaybackInfo info = createPlaybackInfo(volumeProvider, player.getAudioAttributes());
synchronized (mLock) {
+ hasPlayerChanged = (mPlayer != player);
+ hasAgentChanged = (mPlaylistAgent != playlistAgent);
+ hasPlaybackInfoChanged = (mPlaybackInfo != info);
oldPlayer = mPlayer;
oldAgent = mPlaylistAgent;
mPlayer = player;
@@ -161,6 +170,10 @@
mVolumeProvider = volumeProvider;
mPlaybackInfo = info;
}
+ if (volumeProvider == null) {
+ int stream = getLegacyStreamType(player.getAudioAttributes());
+ mSessionCompat.setPlaybackToLocal(stream);
+ }
if (player != oldPlayer) {
player.registerPlayerEventCallback(mCallbackExecutor, mPlayerEventCallback);
if (oldPlayer != null) {
@@ -171,35 +184,40 @@
if (playlistAgent != oldAgent) {
playlistAgent.registerPlaylistEventCallback(mCallbackExecutor, mPlaylistEventCallback);
if (oldAgent != null) {
- // Warning: Poorly implement player may ignore this
+ // Warning: Poorly implement agent may ignore this
oldAgent.unregisterPlaylistEventCallback(mPlaylistEventCallback);
}
}
if (oldPlayer != null) {
- mSession2Stub.notifyPlaybackInfoChanged(info);
- notifyPlayerUpdatedNotLocked(oldPlayer);
+ // If it's not the first updatePlayer(), tell changes in the player, agent, and playback
+ // info.
+ if (hasAgentChanged) {
+ // Update agent first. Otherwise current position may be changed off the current
+ // media item's duration, and controller may consider it as a bug.
+ notifyAgentUpdatedNotLocked(oldAgent);
+ }
+ if (hasPlayerChanged) {
+ notifyPlayerUpdatedNotLocked(oldPlayer);
+ }
+ if (hasPlaybackInfoChanged) {
+ // Currently hasPlaybackInfo is always true, but check this in case that we're
+ // adding PlaybackInfo#equals().
+ notifyToAllControllers(new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onPlaybackInfoChanged(info);
+ }
+ });
+ }
}
- // TODO(jaewan): Repeat the same thing for the playlist agent.
}
private PlaybackInfo createPlaybackInfo(VolumeProviderCompat volumeProvider,
AudioAttributesCompat attrs) {
PlaybackInfo info;
if (volumeProvider == null) {
- int stream;
- if (attrs == null) {
- stream = AudioManager.STREAM_MUSIC;
- } else {
- stream = attrs.getVolumeControlStream();
- if (stream == AudioManager.USE_DEFAULT_STREAM_TYPE) {
- // It may happen if the AudioAttributes doesn't have usage.
- // Change it to the STREAM_MUSIC because it's not supported by audio manager
- // for querying volume level.
- stream = AudioManager.STREAM_MUSIC;
- }
- }
-
+ int stream = getLegacyStreamType(attrs);
int controlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
controlType = VolumeProviderCompat.VOLUME_CONTROL_FIXED;
@@ -221,6 +239,23 @@
return info;
}
+ private int getLegacyStreamType(@Nullable AudioAttributesCompat attrs) {
+ int stream;
+ if (attrs == null) {
+ stream = AudioManager.STREAM_MUSIC;
+ } else {
+ stream = attrs.getLegacyStreamType();
+ if (stream == AudioManager.USE_DEFAULT_STREAM_TYPE) {
+ // Usually, AudioAttributesCompat#getLegacyStreamType() does not return
+ // USE_DEFAULT_STREAM_TYPE unless the developer sets it with
+ // AudioAttributesCompat.Builder#setLegacyStreamType().
+ // But for safety, let's convert USE_DEFAULT_STREAM_TYPE to STREAM_MUSIC here.
+ stream = AudioManager.STREAM_MUSIC;
+ }
+ }
+ return stream;
+ }
+
@Override
public void close() {
synchronized (mLock) {
@@ -232,13 +267,17 @@
mSessionCompat.release();
mHandler.removeCallbacksAndMessages(null);
if (mHandlerThread.isAlive()) {
- mHandlerThread.quitSafely();
+ if (Build.VERSION.SDK_INT >= 18) {
+ mHandlerThread.quitSafely();
+ } else {
+ mHandlerThread.quit();
+ }
}
}
}
@Override
- public @NonNull MediaPlayerBase getPlayer() {
+ public @NonNull MediaPlayerInterface getPlayer() {
synchronized (mLock) {
return mPlayer;
}
@@ -264,31 +303,34 @@
}
@Override
- public @NonNull List<MediaSession2.ControllerInfo> getConnectedControllers() {
+ public @NonNull List<ControllerInfo> getConnectedControllers() {
return mSession2Stub.getConnectedControllers();
}
@Override
public void setAudioFocusRequest(@Nullable AudioFocusRequest afr) {
- // TODO(jaewan): implement this (b/72529899)
- // mProvider.setAudioFocusRequest_impl(focusGain);
}
@Override
public void setCustomLayout(@NonNull ControllerInfo controller,
- @NonNull List<MediaSession2.CommandButton> layout) {
+ @NonNull final List<MediaSession2.CommandButton> layout) {
if (controller == null) {
throw new IllegalArgumentException("controller shouldn't be null");
}
if (layout == null) {
throw new IllegalArgumentException("layout shouldn't be null");
}
- mSession2Stub.notifyCustomLayout(controller, layout);
+ notifyToController(controller, new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onCustomLayoutChanged(layout);
+ }
+ });
}
@Override
public void setAllowedCommands(@NonNull ControllerInfo controller,
- @NonNull SessionCommandGroup2 commands) {
+ @NonNull final SessionCommandGroup2 commands) {
if (controller == null) {
throw new IllegalArgumentException("controller shouldn't be null");
}
@@ -296,32 +338,49 @@
throw new IllegalArgumentException("commands shouldn't be null");
}
mSession2Stub.setAllowedCommands(controller, commands);
+ notifyToController(controller, new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onAllowedCommandsChanged(commands);
+ }
+ });
}
@Override
- public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args) {
+ public void sendCustomCommand(@NonNull final SessionCommand2 command,
+ @Nullable final Bundle args) {
if (command == null) {
throw new IllegalArgumentException("command shouldn't be null");
}
- mSession2Stub.sendCustomCommand(command, args);
+ notifyToAllControllers(new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onCustomCommand(command, args, null);
+ }
+ });
}
@Override
public void sendCustomCommand(@NonNull ControllerInfo controller,
- @NonNull SessionCommand2 command, @Nullable Bundle args,
- @Nullable ResultReceiver receiver) {
+ @NonNull final SessionCommand2 command, @Nullable final Bundle args,
+ @Nullable final ResultReceiver receiver) {
if (controller == null) {
throw new IllegalArgumentException("controller shouldn't be null");
}
if (command == null) {
throw new IllegalArgumentException("command shouldn't be null");
}
- mSession2Stub.sendCustomCommand(controller, command, args, receiver);
+ notifyToController(controller, new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onCustomCommand(command, args, receiver);
+ }
+ });
}
@Override
public void play() {
- MediaPlayerBase player;
+ MediaPlayerInterface player;
synchronized (mLock) {
player = mPlayer;
}
@@ -334,7 +393,7 @@
@Override
public void pause() {
- MediaPlayerBase player;
+ MediaPlayerInterface player;
synchronized (mLock) {
player = mPlayer;
}
@@ -347,7 +406,7 @@
@Override
public void reset() {
- MediaPlayerBase player;
+ MediaPlayerInterface player;
synchronized (mLock) {
player = mPlayer;
}
@@ -360,7 +419,7 @@
@Override
public void prepare() {
- MediaPlayerBase player;
+ MediaPlayerInterface player;
synchronized (mLock) {
player = mPlayer;
}
@@ -373,7 +432,7 @@
@Override
public void seekTo(long pos) {
- MediaPlayerBase player;
+ MediaPlayerInterface player;
synchronized (mLock) {
player = mPlayer;
}
@@ -395,19 +454,29 @@
}
@Override
- public void notifyError(@ErrorCode int errorCode, @Nullable Bundle extras) {
- mSession2Stub.notifyError(errorCode, extras);
+ public void notifyError(@ErrorCode final int errorCode, @Nullable final Bundle extras) {
+ notifyToAllControllers(new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onError(errorCode, extras);
+ }
+ });
}
@Override
public void notifyRoutesInfoChanged(@NonNull ControllerInfo controller,
- @Nullable List<Bundle> routes) {
- mSession2Stub.notifyRoutesInfoChanged(controller, routes);
+ @Nullable final List<Bundle> routes) {
+ notifyToController(controller, new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onRoutesInfoChanged(routes);
+ }
+ });
}
@Override
- public @MediaPlayerBase.PlayerState int getPlayerState() {
- MediaPlayerBase player;
+ public @MediaPlayerInterface.PlayerState int getPlayerState() {
+ MediaPlayerInterface player;
synchronized (mLock) {
player = mPlayer;
}
@@ -416,12 +485,12 @@
} else if (DEBUG) {
Log.d(TAG, "API calls after the close()", new IllegalStateException());
}
- return MediaPlayerBase.PLAYER_STATE_ERROR;
+ return MediaPlayerInterface.PLAYER_STATE_ERROR;
}
@Override
public long getCurrentPosition() {
- MediaPlayerBase player;
+ MediaPlayerInterface player;
synchronized (mLock) {
player = mPlayer;
}
@@ -430,18 +499,28 @@
} else if (DEBUG) {
Log.d(TAG, "API calls after the close()", new IllegalStateException());
}
- return MediaPlayerBase.UNKNOWN_TIME;
+ return MediaPlayerInterface.UNKNOWN_TIME;
}
@Override
public long getDuration() {
- // TODO: implement
- return 0;
+ MediaPlayerInterface player;
+ synchronized (mLock) {
+ player = mPlayer;
+ }
+ if (player != null) {
+ // Note: This should be the same as
+ // getCurrentMediaItem().getMetadata().getLong(METADATA_KEY_DURATION)
+ return player.getDuration();
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ return MediaPlayerInterface.UNKNOWN_TIME;
}
@Override
public long getBufferedPosition() {
- MediaPlayerBase player;
+ MediaPlayerInterface player;
synchronized (mLock) {
player = mPlayer;
}
@@ -450,12 +529,12 @@
} else if (DEBUG) {
Log.d(TAG, "API calls after the close()", new IllegalStateException());
}
- return MediaPlayerBase.UNKNOWN_TIME;
+ return MediaPlayerInterface.UNKNOWN_TIME;
}
@Override
- public @MediaPlayerBase.BuffState int getBufferingState() {
- MediaPlayerBase player;
+ public @MediaPlayerInterface.BuffState int getBufferingState() {
+ MediaPlayerInterface player;
synchronized (mLock) {
player = mPlayer;
}
@@ -469,7 +548,7 @@
@Override
public float getPlaybackSpeed() {
- MediaPlayerBase player;
+ MediaPlayerInterface player;
synchronized (mLock) {
player = mPlayer;
}
@@ -483,7 +562,7 @@
@Override
public void setPlaybackSpeed(float speed) {
- MediaPlayerBase player;
+ MediaPlayerInterface player;
synchronized (mLock) {
player = mPlayer;
}
@@ -740,18 +819,63 @@
}
///////////////////////////////////////////////////
- // package private and private methods
+ // LibrarySession Methods
///////////////////////////////////////////////////
@Override
- void setInstance(MediaSession2 session) {
- mInstance = new WeakReference<>(session);
+ void notifyChildrenChanged(ControllerInfo controller, final String parentId,
+ final int itemCount, final Bundle extras,
+ List<MediaSessionManager.RemoteUserInfo> subscribingBrowsers) {
+ if (controller == null) {
+ throw new IllegalArgumentException("controller shouldn't be null");
+ }
+ if (TextUtils.isEmpty(parentId)) {
+ throw new IllegalArgumentException("query shouldn't be empty");
+ }
+ // Notify controller only if it has subscribed the parentId.
+ for (MediaSessionManager.RemoteUserInfo info : subscribingBrowsers) {
+ if (info.getPackageName().equals(controller.getPackageName())
+ && info.getUid() == controller.getUid()) {
+ notifyToController(controller, new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onChildrenChanged(parentId, itemCount, extras);
+ }
+ });
+ return;
+ }
+ }
}
@Override
- MediaSession2 getInstance() {
- return mInstance.get();
+ void notifySearchResultChanged(ControllerInfo controller, final String query,
+ final int itemCount, final Bundle extras) {
+ if (controller == null) {
+ throw new IllegalArgumentException("controller shouldn't be null");
+ }
+ if (TextUtils.isEmpty(query)) {
+ throw new IllegalArgumentException("query shouldn't be empty");
+ }
+ notifyToController(controller, new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onSearchResultChanged(query, itemCount, extras);
+ }
+ });
+ }
+
+ ///////////////////////////////////////////////////
+ // package private and private methods
+ ///////////////////////////////////////////////////
+ @Override
+ MediaSession2 createInstance() {
+ return new MediaSession2(this);
+ }
+
+ @Override
+ @NonNull MediaSession2 getInstance() {
+ return mInstance;
}
@Override
@@ -770,6 +894,11 @@
}
@Override
+ MediaSessionCompat getSessionCompat() {
+ return mSessionCompat;
+ }
+
+ @Override
boolean isClosed() {
return !mHandlerThread.isAlive();
}
@@ -779,12 +908,6 @@
synchronized (mLock) {
int state = MediaUtils2.createPlaybackStateCompatState(getPlayerState(),
getBufferingState());
- // TODO: Consider following missing stuff
- // - setCustomAction(): Fill custom layout
- // - setErrorMessage(): Fill error message when notifyError() is called.
- // - setActiveQueueItemId(): Fill here with the current media item...
- // - setExtra(): No idea at this moment.
- // TODO: generate actions from the allowed commands.
long allActions = PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_PAUSE
| PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_REWIND
| PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
@@ -816,11 +939,6 @@
return mPlaybackInfo;
}
}
-
- MediaSession2StubImplBase getSession2Stub() {
- return mSession2Stub;
- }
-
private static String getServiceName(Context context, String serviceAction, String id) {
PackageManager manager = context.getPackageManager();
Intent serviceIntent = new Intent(serviceAction);
@@ -846,105 +964,203 @@
return serviceName;
}
- private void notifyPlayerUpdatedNotLocked(MediaPlayerBase oldPlayer) {
- MediaPlayerBase player;
- synchronized (mLock) {
- player = mPlayer;
+ private void notifyAgentUpdatedNotLocked(MediaPlaylistAgent oldAgent) {
+ // Tells the playlist change first, to current item can change be notified with an item
+ // within the playlist.
+ List<MediaItem2> oldPlaylist = oldAgent.getPlaylist();
+ final List<MediaItem2> newPlaylist = getPlaylist();
+ if (!ObjectsCompat.equals(oldPlaylist, newPlaylist)) {
+ notifyToAllControllers(new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onPlaylistChanged(
+ newPlaylist, getPlaylistMetadata());
+ }
+ });
+ } else {
+ MediaMetadata2 oldMetadata = oldAgent.getPlaylistMetadata();
+ final MediaMetadata2 newMetadata = getPlaylistMetadata();
+ if (!ObjectsCompat.equals(oldMetadata, newMetadata)) {
+ notifyToAllControllers(new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onPlaylistMetadataChanged(newMetadata);
+ }
+ });
+ }
}
- // TODO(jaewan): (Can be post-P) Find better way for player.getPlayerState() //
- // In theory, Session.getXXX() may not be the same as Player.getXXX()
- // and we should notify information of the session.getXXX() instead of
- // player.getXXX()
- // Notify to controllers as well.
- final int state = player.getPlayerState();
- if (state != oldPlayer.getPlayerState()) {
- // TODO: implement
- mSession2Stub.notifyPlayerStateChanged(state);
+ MediaItem2 oldCurrentItem = oldAgent.getCurrentMediaItem();
+ final MediaItem2 newCurrentItem = getCurrentMediaItem();
+ if (!ObjectsCompat.equals(oldCurrentItem, newCurrentItem)) {
+ notifyToAllControllers(new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onCurrentMediaItemChanged(newCurrentItem);
+ }
+ });
}
+ final int repeatMode = getRepeatMode();
+ if (oldAgent.getRepeatMode() != repeatMode) {
+ notifyToAllControllers(new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onRepeatModeChanged(repeatMode);
+ }
+ });
+ }
+ final int shuffleMode = getShuffleMode();
+ if (oldAgent.getShuffleMode() != shuffleMode) {
+ notifyToAllControllers(new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onShuffleModeChanged(shuffleMode);
+ }
+ });
+ }
+ }
- final long currentTimeMs = System.currentTimeMillis();
- final long position = player.getCurrentPosition();
- if (position != oldPlayer.getCurrentPosition()) {
- // TODO: implement
- //mSession2Stub.notifyPositionChangedNotLocked(currentTimeMs, position);
+ private void notifyPlayerUpdatedNotLocked(MediaPlayerInterface oldPlayer) {
+ // Always forcefully send the player state and buffered state to send the current position
+ // and buffered position.
+ final int playerState = getPlayerState();
+ notifyToAllControllers(new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onPlayerStateChanged(playerState);
+ }
+ });
+ final MediaItem2 item = getCurrentMediaItem();
+ if (item != null) {
+ final int bufferingState = getBufferingState();
+ notifyToAllControllers(new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onBufferingStateChanged(item, bufferingState);
+ }
+ });
}
-
- final float speed = player.getPlaybackSpeed();
+ final float speed = getPlaybackSpeed();
if (speed != oldPlayer.getPlaybackSpeed()) {
- // TODO: implement
- //mSession2Stub.notifyPlaybackSpeedChangedNotLocked(speed);
+ notifyToAllControllers(new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onPlaybackSpeedChanged(speed);
+ }
+ });
}
-
- final long bufferedPosition = player.getBufferedPosition();
- if (bufferedPosition != oldPlayer.getBufferedPosition()) {
- // TODO: implement
- //mSession2Stub.notifyBufferedPositionChangedNotLocked(bufferedPosition);
- }
+ // Note: AudioInfo is updated outside of this API.
}
private void notifyPlaylistChangedOnExecutor(MediaPlaylistAgent playlistAgent,
- List<MediaItem2> list, MediaMetadata2 metadata) {
+ final List<MediaItem2> list, final MediaMetadata2 metadata) {
synchronized (mLock) {
if (playlistAgent != mPlaylistAgent) {
// Ignore calls from the old agent.
return;
}
}
- MediaSession2 session2 = mInstance.get();
- if (session2 != null) {
- mCallback.onPlaylistChanged(session2, playlistAgent, list, metadata);
- mSession2Stub.notifyPlaylistChanged(list, metadata);
- }
+ mCallback.onPlaylistChanged(mInstance, playlistAgent, list, metadata);
+ notifyToAllControllers(new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onPlaylistChanged(list, metadata);
+ }
+ });
}
private void notifyPlaylistMetadataChangedOnExecutor(MediaPlaylistAgent playlistAgent,
- MediaMetadata2 metadata) {
+ final MediaMetadata2 metadata) {
synchronized (mLock) {
if (playlistAgent != mPlaylistAgent) {
// Ignore calls from the old agent.
return;
}
}
- MediaSession2 session2 = mInstance.get();
- if (session2 != null) {
- mCallback.onPlaylistMetadataChanged(session2, playlistAgent, metadata);
- mSession2Stub.notifyPlaylistMetadataChanged(metadata);
- }
+ mCallback.onPlaylistMetadataChanged(mInstance, playlistAgent, metadata);
+ notifyToAllControllers(new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onPlaylistMetadataChanged(metadata);
+ }
+ });
}
private void notifyRepeatModeChangedOnExecutor(MediaPlaylistAgent playlistAgent,
- int repeatMode) {
+ final int repeatMode) {
synchronized (mLock) {
if (playlistAgent != mPlaylistAgent) {
// Ignore calls from the old agent.
return;
}
}
- MediaSession2 session2 = mInstance.get();
- if (session2 != null) {
- mCallback.onRepeatModeChanged(session2, playlistAgent, repeatMode);
- mSession2Stub.notifyRepeatModeChanged(repeatMode);
- }
+ mCallback.onRepeatModeChanged(mInstance, playlistAgent, repeatMode);
+ notifyToAllControllers(new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onRepeatModeChanged(repeatMode);
+ }
+ });
}
private void notifyShuffleModeChangedOnExecutor(MediaPlaylistAgent playlistAgent,
- int shuffleMode) {
+ final int shuffleMode) {
synchronized (mLock) {
if (playlistAgent != mPlaylistAgent) {
// Ignore calls from the old agent.
return;
}
}
- MediaSession2 session2 = mInstance.get();
- if (session2 != null) {
- mCallback.onShuffleModeChanged(session2, playlistAgent, shuffleMode);
- mSession2Stub.notifyShuffleModeChanged(shuffleMode);
+ mCallback.onShuffleModeChanged(mInstance, playlistAgent, shuffleMode);
+ notifyToAllControllers(new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onShuffleModeChanged(shuffleMode);
+ }
+ });
+ }
+
+ private void notifyToController(@NonNull final ControllerInfo controller,
+ @NonNull NotifyRunnable runnable) {
+ if (controller == null) {
+ return;
+ }
+ try {
+ runnable.run(controller.getControllerCb());
+ } catch (DeadObjectException e) {
+ if (DEBUG) {
+ Log.d(TAG, controller.toString() + " is gone", e);
+ }
+ mSession2Stub.removeControllerInfo(controller);
+ mCallbackExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onDisconnected(MediaSession2ImplBase.this.getInstance(), controller);
+ }
+ });
+ } catch (RemoteException e) {
+ // Currently it's TransactionTooLargeException or DeadSystemException.
+ // We'd better to leave log for those cases because
+ // - TransactionTooLargeException means that we may need to fix our code.
+ // (e.g. add pagination or special way to deliver Bitmap)
+ // - DeadSystemException means that errors around it can be ignored.
+ Log.w(TAG, "Exception in " + controller.toString(), e);
+ }
+ }
+
+ private void notifyToAllControllers(@NonNull NotifyRunnable runnable) {
+ List<ControllerInfo> controllers = getConnectedControllers();
+ for (int i = 0; i < controllers.size(); i++) {
+ notifyToController(controllers.get(i), runnable);
}
}
///////////////////////////////////////////////////
// Inner classes
///////////////////////////////////////////////////
+ @FunctionalInterface
+ private interface NotifyRunnable {
+ void run(ControllerCb callback) throws RemoteException;
+ }
private static class MyPlayerEventCallback extends PlayerEventCallback {
private final WeakReference<MediaSession2ImplBase> mSession;
@@ -954,66 +1170,41 @@
}
@Override
- public void onCurrentDataSourceChanged(final MediaPlayerBase mpb,
+ public void onCurrentDataSourceChanged(final MediaPlayerInterface player,
final DataSourceDesc dsd) {
final MediaSession2ImplBase session = getSession();
- // TODO: handle properly when dsd == null
- if (session == null || dsd == null) {
- return;
- }
- session.getCallbackExecutor().execute(new Runnable() {
- @Override
- public void run() {
- MediaItem2 item = MyPlayerEventCallback.this.getMediaItem(session, dsd);
- if (item == null) {
- return;
- }
- session.getCallback().onCurrentMediaItemChanged(session.getInstance(), mpb,
- item);
- if (item.equals(session.getCurrentMediaItem())) {
- session.getSession2Stub().notifyCurrentMediaItemChanged(item);
- }
- }
- });
- }
-
- @Override
- public void onMediaPrepared(final MediaPlayerBase mpb, final DataSourceDesc dsd) {
- final MediaSession2ImplBase session = getSession();
- if (session == null || dsd == null) {
- return;
- }
- session.getCallbackExecutor().execute(new Runnable() {
- @Override
- public void run() {
- MediaItem2 item = MyPlayerEventCallback.this.getMediaItem(session, dsd);
- if (item == null) {
- return;
- }
- session.getCallback().onMediaPrepared(session.getInstance(), mpb, item);
- // TODO (jaewan): Notify controllers through appropriate callback. (b/74505936)
- }
- });
- }
-
- @Override
- public void onPlayerStateChanged(final MediaPlayerBase mpb, final int state) {
- final MediaSession2ImplBase session = getSession();
if (session == null) {
return;
}
session.getCallbackExecutor().execute(new Runnable() {
@Override
public void run() {
- session.getCallback().onPlayerStateChanged(session.getInstance(), mpb, state);
- session.getSession2Stub().notifyPlayerStateChanged(state);
+ final MediaItem2 item;
+ if (dsd == null) {
+ // This is OK because onCurrentDataSourceChanged() can be called with the
+ // null dsd, so onCurrentMediaItemChanged() can be as well.
+ item = null;
+ } else {
+ item = MyPlayerEventCallback.this.getMediaItem(session, dsd);
+ if (item == null) {
+ Log.w(TAG, "Cannot obtain media item from the dsd=" + dsd);
+ return;
+ }
+ }
+ session.getCallback().onCurrentMediaItemChanged(session.getInstance(), player,
+ item);
+ session.notifyToAllControllers(new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onCurrentMediaItemChanged(item);
+ }
+ });
}
});
}
@Override
- public void onBufferingStateChanged(final MediaPlayerBase mpb, final DataSourceDesc dsd,
- final int state) {
+ public void onMediaPrepared(final MediaPlayerInterface mpb, final DataSourceDesc dsd) {
final MediaSession2ImplBase session = getSession();
if (session == null || dsd == null) {
return;
@@ -1025,15 +1216,105 @@
if (item == null) {
return;
}
- session.getCallback().onBufferingStateChanged(
- session.getInstance(), mpb, item, state);
- session.getSession2Stub().notifyBufferingStateChanged(item, state);
+ if (item.equals(session.getCurrentMediaItem())) {
+ long duration = session.getDuration();
+ if (duration < 0) {
+ return;
+ }
+ MediaMetadata2 metadata = item.getMetadata();
+ if (metadata != null) {
+ if (!metadata.containsKey(MediaMetadata2.METADATA_KEY_DURATION)) {
+ metadata = new MediaMetadata2.Builder(metadata).putLong(
+ MediaMetadata2.METADATA_KEY_DURATION, duration).build();
+ } else {
+ long durationFromMetadata =
+ metadata.getLong(MediaMetadata2.METADATA_KEY_DURATION);
+ if (duration != durationFromMetadata) {
+ // Warns developers about the mismatch. Don't log media item
+ // here to keep metadata secure.
+ Log.w(TAG, "duration mismatch for an item."
+ + " duration from player=" + duration
+ + " duration from metadata=" + durationFromMetadata
+ + ". May be a timing issue?");
+ }
+ // Trust duration in the metadata set by developer.
+ // In theory, duration may differ if the current item has been
+ // changed before the getDuration(). So it's better not touch
+ // duration set by developer.
+ metadata = null;
+ }
+ } else {
+ metadata = new MediaMetadata2.Builder()
+ .putLong(MediaMetadata2.METADATA_KEY_DURATION, duration)
+ .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID,
+ item.getMediaId())
+ .build();
+ }
+ if (metadata != null) {
+ item.setMetadata(metadata);
+ session.notifyToAllControllers(new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onPlaylistChanged(
+ session.getPlaylist(), session.getPlaylistMetadata());
+ }
+ });
+ }
+ }
+ session.getCallback().onMediaPrepared(session.getInstance(), mpb, item);
}
});
}
@Override
- public void onPlaybackSpeedChanged(final MediaPlayerBase mpb, final float speed) {
+ public void onPlayerStateChanged(final MediaPlayerInterface player, final int state) {
+ final MediaSession2ImplBase session = getSession();
+ if (session == null) {
+ return;
+ }
+ session.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ session.getCallback().onPlayerStateChanged(
+ session.getInstance(), player, state);
+ session.notifyToAllControllers(new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onPlayerStateChanged(state);
+ }
+ });
+ }
+ });
+ }
+
+ @Override
+ public void onBufferingStateChanged(final MediaPlayerInterface mpb,
+ final DataSourceDesc dsd, final int state) {
+ final MediaSession2ImplBase session = getSession();
+ if (session == null || dsd == null) {
+ return;
+ }
+ session.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ final MediaItem2 item = MyPlayerEventCallback.this.getMediaItem(session, dsd);
+ if (item == null) {
+ return;
+ }
+ session.getCallback().onBufferingStateChanged(
+ session.getInstance(), mpb, item, state);
+ session.notifyToAllControllers(new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onBufferingStateChanged(item, state);
+ }
+ });
+ }
+ });
+ }
+
+ @Override
+ public void onPlaybackSpeedChanged(final MediaPlayerInterface mpb, final float speed) {
final MediaSession2ImplBase session = getSession();
if (session == null) {
return;
@@ -1042,7 +1323,32 @@
@Override
public void run() {
session.getCallback().onPlaybackSpeedChanged(session.getInstance(), mpb, speed);
- session.getSession2Stub().notifyPlaybackSpeedChanged(speed);
+ session.notifyToAllControllers(new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onPlaybackSpeedChanged(speed);
+ }
+ });
+ }
+ });
+ }
+
+ @Override
+ public void onSeekCompleted(final MediaPlayerInterface mpb, final long position) {
+ final MediaSession2ImplBase session = getSession();
+ if (session == null) {
+ return;
+ }
+ session.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ session.getCallback().onSeekCompleted(session.getInstance(), mpb, position);
+ session.notifyToAllControllers(new NotifyRunnable() {
+ @Override
+ public void run(ControllerCb callback) throws RemoteException {
+ callback.onSeekCompleted(position);
+ }
+ });
}
});
}
@@ -1123,7 +1429,7 @@
abstract static class BuilderBase
<T extends MediaSession2, C extends SessionCallback> {
final Context mContext;
- MediaPlayerBase mPlayer;
+ MediaPlayerInterface mPlayer;
String mId;
Executor mCallbackExecutor;
C mCallback;
@@ -1140,7 +1446,7 @@
mId = TAG;
}
- void setPlayer(@NonNull MediaPlayerBase player) {
+ void setPlayer(@NonNull MediaPlayerInterface player) {
if (player == null) {
throw new IllegalArgumentException("player shouldn't be null");
}
diff --git a/media/src/main/java/androidx/media/MediaSession2StubImplBase.java b/media/src/main/java/androidx/media/MediaSession2Stub.java
similarity index 68%
rename from media/src/main/java/androidx/media/MediaSession2StubImplBase.java
rename to media/src/main/java/androidx/media/MediaSession2Stub.java
index 48e641e..87b17cf 100644
--- a/media/src/main/java/androidx/media/MediaSession2StubImplBase.java
+++ b/media/src/main/java/androidx/media/MediaSession2Stub.java
@@ -25,6 +25,7 @@
import static androidx.media.MediaConstants2.ARGUMENT_ERROR_CODE;
import static androidx.media.MediaConstants2.ARGUMENT_EXTRAS;
import static androidx.media.MediaConstants2.ARGUMENT_ICONTROLLER_CALLBACK;
+import static androidx.media.MediaConstants2.ARGUMENT_ITEM_COUNT;
import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ID;
import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ITEM;
import static androidx.media.MediaConstants2.ARGUMENT_PACKAGE_NAME;
@@ -55,7 +56,8 @@
import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_CONNECT;
import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_DISCONNECT;
import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED;
-import static androidx.media.MediaConstants2.SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_BUFFERING_STATE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CHILDREN_CHANGED;
import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED;
import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ERROR;
import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED;
@@ -65,9 +67,12 @@
import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED;
import static androidx.media.MediaConstants2.SESSION_EVENT_ON_REPEAT_MODE_CHANGED;
import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ROUTES_INFO_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SEARCH_RESULT_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SEEK_COMPLETED;
import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED;
import static androidx.media.MediaConstants2.SESSION_EVENT_SEND_CUSTOM_COMMAND;
import static androidx.media.MediaConstants2.SESSION_EVENT_SET_CUSTOM_LAYOUT;
+import static androidx.media.SessionCommand2.COMMAND_CODE_CUSTOM;
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE;
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY;
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE;
@@ -106,21 +111,22 @@
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
-import android.os.DeadObjectException;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.support.v4.media.session.IMediaControllerCallback;
import android.support.v4.media.session.MediaSessionCompat;
-import android.util.ArrayMap;
import android.util.Log;
import android.util.SparseArray;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.collection.ArrayMap;
+import androidx.core.app.BundleCompat;
import androidx.media.MediaController2.PlaybackInfo;
import androidx.media.MediaSession2.CommandButton;
+import androidx.media.MediaSession2.ControllerCb;
import androidx.media.MediaSession2.ControllerInfo;
import java.util.ArrayList;
@@ -129,7 +135,7 @@
import java.util.Set;
@TargetApi(Build.VERSION_CODES.KITKAT)
-class MediaSession2StubImplBase extends MediaSessionCompat.Callback {
+class MediaSession2Stub extends MediaSessionCompat.Callback {
private static final String TAG = "MS2StubImplBase";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -161,78 +167,16 @@
private final ArrayMap<ControllerInfo, SessionCommandGroup2> mAllowedCommandGroupMap =
new ArrayMap<>();
- MediaSession2StubImplBase(MediaSession2.SupportLibraryImpl session) {
+ MediaSession2Stub(MediaSession2.SupportLibraryImpl session) {
mSession = session;
mContext = mSession.getContext();
}
@Override
- public void onPrepare() {
- mSession.getCallbackExecutor().execute(new Runnable() {
- @Override
- public void run() {
- if (mSession.isClosed()) {
- return;
- }
- mSession.prepare();
- }
- });
- }
-
- @Override
- public void onPlay() {
- mSession.getCallbackExecutor().execute(new Runnable() {
- @Override
- public void run() {
- if (mSession.isClosed()) {
- return;
- }
- mSession.play();
- }
- });
- }
-
- @Override
- public void onPause() {
- mSession.getCallbackExecutor().execute(new Runnable() {
- @Override
- public void run() {
- if (mSession.isClosed()) {
- return;
- }
- mSession.pause();
- }
- });
- }
-
- @Override
- public void onStop() {
- mSession.getCallbackExecutor().execute(new Runnable() {
- @Override
- public void run() {
- if (mSession.isClosed()) {
- return;
- }
- mSession.reset();
- }
- });
- }
-
- @Override
- public void onSeekTo(final long pos) {
- mSession.getCallbackExecutor().execute(new Runnable() {
- @Override
- public void run() {
- if (mSession.isClosed()) {
- return;
- }
- mSession.seekTo(pos);
- }
- });
- }
-
- @Override
public void onCommand(String command, final Bundle extras, final ResultReceiver cb) {
+ if (extras != null) {
+ extras.setClassLoader(MediaSession2.class.getClassLoader());
+ }
switch (command) {
case CONTROLLER_COMMAND_CONNECT:
connect(extras, cb);
@@ -242,8 +186,8 @@
break;
case CONTROLLER_COMMAND_BY_COMMAND_CODE: {
final int commandCode = extras.getInt(ARGUMENT_COMMAND_CODE);
- IMediaControllerCallback caller =
- (IMediaControllerCallback) extras.getBinder(ARGUMENT_ICONTROLLER_CALLBACK);
+ IMediaControllerCallback caller = IMediaControllerCallback.Stub.asInterface(
+ BundleCompat.getBinder(extras, ARGUMENT_ICONTROLLER_CALLBACK));
if (caller == null) {
return;
}
@@ -332,7 +276,10 @@
int flags = extras.getInt(ARGUMENT_VOLUME_FLAGS);
VolumeProviderCompat vp = mSession.getVolumeProvider();
if (vp == null) {
- // TODO: Revisit
+ MediaSessionCompat sessionCompat = mSession.getSessionCompat();
+ if (sessionCompat != null) {
+ sessionCompat.getController().setVolumeTo(value, flags);
+ }
} else {
vp.onSetVolumeTo(value);
}
@@ -343,7 +290,11 @@
int flags = extras.getInt(ARGUMENT_VOLUME_FLAGS);
VolumeProviderCompat vp = mSession.getVolumeProvider();
if (vp == null) {
- // TODO: Revisit
+ MediaSessionCompat sessionCompat = mSession.getSessionCompat();
+ if (sessionCompat != null) {
+ sessionCompat.getController().adjustVolume(
+ direction, flags);
+ }
} else {
vp.onAdjustVolume(direction);
}
@@ -438,8 +389,8 @@
case CONTROLLER_COMMAND_BY_CUSTOM_COMMAND: {
final SessionCommand2 customCommand =
SessionCommand2.fromBundle(extras.getBundle(ARGUMENT_CUSTOM_COMMAND));
- IMediaControllerCallback caller =
- (IMediaControllerCallback) extras.getBinder(ARGUMENT_ICONTROLLER_CALLBACK);
+ IMediaControllerCallback caller = IMediaControllerCallback.Stub.asInterface(
+ BundleCompat.getBinder(extras, ARGUMENT_ICONTROLLER_CALLBACK));
if (caller == null || customCommand == null) {
return;
}
@@ -467,261 +418,10 @@
return controllers;
}
- void notifyCustomLayout(ControllerInfo controller, final List<CommandButton> layout) {
- notifyInternal(controller, new Session2Runnable() {
- @Override
- public void run(ControllerInfo controller) throws RemoteException {
- Bundle bundle = new Bundle();
- bundle.putParcelableArray(ARGUMENT_COMMAND_BUTTONS,
- MediaUtils2.toCommandButtonParcelableArray(layout));
- controller.getControllerBinder().onEvent(SESSION_EVENT_SET_CUSTOM_LAYOUT, bundle);
- }
- });
- }
-
void setAllowedCommands(ControllerInfo controller, final SessionCommandGroup2 commands) {
synchronized (mLock) {
mAllowedCommandGroupMap.put(controller, commands);
}
- notifyInternal(controller, new Session2Runnable() {
- @Override
- public void run(ControllerInfo controller) throws RemoteException {
- Bundle bundle = new Bundle();
- bundle.putBundle(ARGUMENT_ALLOWED_COMMANDS, commands.toBundle());
- controller.getControllerBinder().onEvent(
- SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED, bundle);
- }
- });
- }
-
- public void sendCustomCommand(ControllerInfo controller, final SessionCommand2 command,
- final Bundle args, final ResultReceiver receiver) {
- if (receiver != null && controller == null) {
- throw new IllegalArgumentException("Controller shouldn't be null if result receiver is"
- + " specified");
- }
- if (command == null) {
- throw new IllegalArgumentException("command shouldn't be null");
- }
- notifyInternal(controller, new Session2Runnable() {
- @Override
- public void run(ControllerInfo controller) throws RemoteException {
- // TODO: Send this event through MediaSessionCompat.XXX()
- Bundle bundle = new Bundle();
- bundle.putBundle(ARGUMENT_CUSTOM_COMMAND, command.toBundle());
- bundle.putBundle(ARGUMENT_ARGUMENTS, args);
- bundle.putParcelable(ARGUMENT_RESULT_RECEIVER, receiver);
- controller.getControllerBinder().onEvent(SESSION_EVENT_SEND_CUSTOM_COMMAND, bundle);
- }
- });
- }
-
- public void sendCustomCommand(final SessionCommand2 command, final Bundle args) {
- if (command == null) {
- throw new IllegalArgumentException("command shouldn't be null");
- }
- final Bundle bundle = new Bundle();
- bundle.putBundle(ARGUMENT_CUSTOM_COMMAND, command.toBundle());
- bundle.putBundle(ARGUMENT_ARGUMENTS, args);
- notifyAll(new Session2Runnable() {
- @Override
- public void run(ControllerInfo controller) throws RemoteException {
- controller.getControllerBinder().onEvent(SESSION_EVENT_SEND_CUSTOM_COMMAND, bundle);
- }
- });
- }
-
- void notifyCurrentMediaItemChanged(final MediaItem2 item) {
- notifyAll(COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM, new Session2Runnable() {
- @Override
- public void run(ControllerInfo controller) throws RemoteException {
- Bundle bundle = new Bundle();
- bundle.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
- controller.getControllerBinder().onEvent(
- SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED, bundle);
- }
- });
- }
-
- void notifyPlaybackInfoChanged(final PlaybackInfo info) {
- notifyAll(new Session2Runnable() {
- @Override
- public void run(ControllerInfo controller) throws RemoteException {
- Bundle bundle = new Bundle();
- bundle.putBundle(ARGUMENT_PLAYBACK_INFO, info.toBundle());
- controller.getControllerBinder().onEvent(
- SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED, bundle);
- }
- });
- }
-
- void notifyPlayerStateChanged(final int state) {
- notifyAll(new Session2Runnable() {
- @Override
- public void run(ControllerInfo controller) throws RemoteException {
- Bundle bundle = new Bundle();
- bundle.putInt(ARGUMENT_PLAYER_STATE, state);
- controller.getControllerBinder().onEvent(
- SESSION_EVENT_ON_PLAYER_STATE_CHANGED, bundle);
- }
- });
- }
-
- void notifyPlaybackSpeedChanged(final float speed) {
- notifyAll(new Session2Runnable() {
- @Override
- public void run(ControllerInfo controller) throws RemoteException {
- Bundle bundle = new Bundle();
- bundle.putParcelable(
- ARGUMENT_PLAYBACK_STATE_COMPAT, mSession.getPlaybackStateCompat());
- controller.getControllerBinder().onEvent(
- SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED, bundle);
- }
- });
- }
-
- void notifyBufferingStateChanged(final MediaItem2 item, final int bufferingState) {
- notifyAll(new Session2Runnable() {
- @Override
- public void run(ControllerInfo controller) throws RemoteException {
- Bundle bundle = new Bundle();
- bundle.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
- bundle.putInt(ARGUMENT_BUFFERING_STATE, bufferingState);
- controller.getControllerBinder().onEvent(
- SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED, bundle);
- }
- });
- }
-
- void notifyError(final int errorCode, final Bundle extras) {
- notifyAll(new Session2Runnable() {
- @Override
- public void run(ControllerInfo controller) throws RemoteException {
- Bundle bundle = new Bundle();
- bundle.putInt(ARGUMENT_ERROR_CODE, errorCode);
- bundle.putBundle(ARGUMENT_EXTRAS, extras);
- controller.getControllerBinder().onEvent(SESSION_EVENT_ON_ERROR, bundle);
- }
- });
- }
-
- void notifyRoutesInfoChanged(@NonNull final ControllerInfo controller,
- @Nullable final List<Bundle> routes) {
- notifyInternal(controller, new Session2Runnable() {
- @Override
- public void run(ControllerInfo controller) throws RemoteException {
- Bundle bundle = null;
- if (routes != null) {
- bundle = new Bundle();
- bundle.putParcelableArray(ARGUMENT_ROUTE_BUNDLE, routes.toArray(new Bundle[0]));
- }
- controller.getControllerBinder().onEvent(
- SESSION_EVENT_ON_ROUTES_INFO_CHANGED, bundle);
- }
- });
- }
-
- void notifyPlaylistChanged(final List<MediaItem2> playlist,
- final MediaMetadata2 metadata) {
- notifyAll(COMMAND_CODE_PLAYLIST_GET_LIST, new Session2Runnable() {
- @Override
- public void run(ControllerInfo controller) throws RemoteException {
- Bundle bundle = new Bundle();
- bundle.putParcelableArray(ARGUMENT_PLAYLIST,
- MediaUtils2.toMediaItem2ParcelableArray(playlist));
- bundle.putBundle(ARGUMENT_PLAYLIST_METADATA,
- metadata == null ? null : metadata.toBundle());
- controller.getControllerBinder().onEvent(
- SESSION_EVENT_ON_PLAYLIST_CHANGED, bundle);
- }
- });
- }
-
- void notifyPlaylistMetadataChanged(final MediaMetadata2 metadata) {
- notifyAll(SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST_METADATA, new Session2Runnable() {
- @Override
- public void run(ControllerInfo controller) throws RemoteException {
- Bundle bundle = new Bundle();
- bundle.putBundle(ARGUMENT_PLAYLIST_METADATA,
- metadata == null ? null : metadata.toBundle());
- controller.getControllerBinder().onEvent(
- SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED, bundle);
- }
- });
- }
-
- void notifyRepeatModeChanged(final int repeatMode) {
- notifyAll(new Session2Runnable() {
- @Override
- public void run(ControllerInfo controller) throws RemoteException {
- Bundle bundle = new Bundle();
- bundle.putInt(ARGUMENT_REPEAT_MODE, repeatMode);
- controller.getControllerBinder().onEvent(
- SESSION_EVENT_ON_REPEAT_MODE_CHANGED, bundle);
- }
- });
- }
-
- void notifyShuffleModeChanged(final int shuffleMode) {
- notifyAll(new Session2Runnable() {
- @Override
- public void run(ControllerInfo controller) throws RemoteException {
- Bundle bundle = new Bundle();
- bundle.putInt(ARGUMENT_SHUFFLE_MODE, shuffleMode);
- controller.getControllerBinder().onEvent(
- SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED, bundle);
- }
- });
- }
-
- private List<ControllerInfo> getControllers() {
- ArrayList<ControllerInfo> controllers = new ArrayList<>();
- synchronized (mLock) {
- for (int i = 0; i < mControllers.size(); i++) {
- controllers.add(mControllers.valueAt(i));
- }
- }
- return controllers;
- }
-
- private void notifyAll(@NonNull Session2Runnable runnable) {
- List<ControllerInfo> controllers = getControllers();
- for (int i = 0; i < controllers.size(); i++) {
- notifyInternal(controllers.get(i), runnable);
- }
- }
-
- private void notifyAll(int commandCode, @NonNull Session2Runnable runnable) {
- List<ControllerInfo> controllers = getControllers();
- for (int i = 0; i < controllers.size(); i++) {
- ControllerInfo controller = controllers.get(i);
- if (isAllowedCommand(controller, commandCode)) {
- notifyInternal(controller, runnable);
- }
- }
- }
-
- // TODO: Add a way to check permission from here.
- private void notifyInternal(@NonNull ControllerInfo controller,
- @NonNull Session2Runnable runnable) {
- if (controller == null || controller.getControllerBinder() == null) {
- return;
- }
- try {
- runnable.run(controller);
- } catch (DeadObjectException e) {
- if (DEBUG) {
- Log.d(TAG, controller.toString() + " is gone", e);
- }
- onControllerClosed(controller.getControllerBinder());
- } catch (RemoteException e) {
- // Currently it's TransactionTooLargeException or DeadSystemException.
- // We'd better to leave log for those cases because
- // - TransactionTooLargeException means that we may need to fix our code.
- // (e.g. add pagination or special way to deliver Bitmap)
- // - DeadSystemException means that errors around it can be ignored.
- Log.w(TAG, "Exception in " + controller.toString(), e);
- }
}
private boolean isAllowedCommand(ControllerInfo controller, SessionCommand2 command) {
@@ -742,12 +442,17 @@
private void onCommand2(@NonNull IBinder caller, final int commandCode,
@NonNull final Session2Runnable runnable) {
- // TODO: Prevent instantiation of SessionCommand2
- onCommand2(caller, new SessionCommand2(commandCode), runnable);
+ onCommand2Internal(caller, null, commandCode, runnable);
}
private void onCommand2(@NonNull IBinder caller, @NonNull final SessionCommand2 sessionCommand,
@NonNull final Session2Runnable runnable) {
+ onCommand2Internal(caller, sessionCommand, COMMAND_CODE_CUSTOM, runnable);
+ }
+
+ private void onCommand2Internal(@NonNull IBinder caller,
+ @Nullable final SessionCommand2 sessionCommand, final int commandCode,
+ @NonNull final Session2Runnable runnable) {
final ControllerInfo controller;
synchronized (mLock) {
controller = mControllers.get(caller);
@@ -758,18 +463,25 @@
mSession.getCallbackExecutor().execute(new Runnable() {
@Override
public void run() {
- if (!isAllowedCommand(controller, sessionCommand)) {
- return;
+ SessionCommand2 command;
+ if (sessionCommand != null) {
+ if (!isAllowedCommand(controller, sessionCommand)) {
+ return;
+ }
+ command = sCommandsForOnCommandRequest.get(sessionCommand.getCommandCode());
+ } else {
+ if (!isAllowedCommand(controller, commandCode)) {
+ return;
+ }
+ command = sCommandsForOnCommandRequest.get(commandCode);
}
- int commandCode = sessionCommand.getCommandCode();
- SessionCommand2 command = sCommandsForOnCommandRequest.get(commandCode);
if (command != null) {
boolean accepted = mSession.getCallback().onCommandRequest(
mSession.getInstance(), controller, command);
if (!accepted) {
// Don't run rejected command.
if (DEBUG) {
- Log.d(TAG, "Command (code=" + commandCode + ") from "
+ Log.d(TAG, "Command (" + command + ") from "
+ controller + " was rejected by " + mSession);
}
return;
@@ -789,35 +501,22 @@
});
}
- private void onControllerClosed(IMediaControllerCallback iController) {
- ControllerInfo controller;
+ void removeControllerInfo(ControllerInfo controller) {
synchronized (mLock) {
- controller = mControllers.remove(iController.asBinder());
+ controller = mControllers.remove(controller.getId());
if (DEBUG) {
Log.d(TAG, "releasing " + controller);
}
}
- if (controller == null) {
- return;
- }
- final ControllerInfo removedController = controller;
- mSession.getCallbackExecutor().execute(new Runnable() {
- @Override
- public void run() {
- mSession.getCallback().onDisconnected(mSession.getInstance(), removedController);
- }
- });
}
private ControllerInfo createControllerInfo(Bundle extras) {
IMediaControllerCallback callback = IMediaControllerCallback.Stub.asInterface(
- extras.getBinder(ARGUMENT_ICONTROLLER_CALLBACK));
+ BundleCompat.getBinder(extras, ARGUMENT_ICONTROLLER_CALLBACK));
String packageName = extras.getString(ARGUMENT_PACKAGE_NAME);
int uid = extras.getInt(ARGUMENT_UID);
int pid = extras.getInt(ARGUMENT_PID);
- // TODO: sanity check for packageName, uid, and pid.
-
- return new ControllerInfo(mContext, uid, pid, packageName, callback);
+ return new ControllerInfo(packageName, pid, uid, new Controller2Cb(callback));
}
private void connect(Bundle extras, final ResultReceiver cb) {
@@ -926,4 +625,173 @@
private interface Session2Runnable {
void run(ControllerInfo controller) throws RemoteException;
}
+
+ final class Controller2Cb extends ControllerCb {
+ private final IMediaControllerCallback mIControllerCallback;
+
+ Controller2Cb(@NonNull IMediaControllerCallback callback) {
+ mIControllerCallback = callback;
+ }
+
+ @Override
+ @NonNull IBinder getId() {
+ return mIControllerCallback.asBinder();
+ }
+
+ @Override
+ void onCustomLayoutChanged(List<CommandButton> layout) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putParcelableArray(ARGUMENT_COMMAND_BUTTONS,
+ MediaUtils2.toCommandButtonParcelableArray(layout));
+ mIControllerCallback.onEvent(SESSION_EVENT_SET_CUSTOM_LAYOUT, bundle);
+ }
+
+ @Override
+ void onPlaybackInfoChanged(PlaybackInfo info) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putBundle(ARGUMENT_PLAYBACK_INFO, info.toBundle());
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED, bundle);
+
+ }
+
+ @Override
+ void onAllowedCommandsChanged(SessionCommandGroup2 commands) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putBundle(ARGUMENT_ALLOWED_COMMANDS, commands.toBundle());
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED, bundle);
+ }
+
+ @Override
+ void onCustomCommand(SessionCommand2 command, Bundle args, ResultReceiver receiver)
+ throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putBundle(ARGUMENT_CUSTOM_COMMAND, command.toBundle());
+ bundle.putBundle(ARGUMENT_ARGUMENTS, args);
+ bundle.putParcelable(ARGUMENT_RESULT_RECEIVER, receiver);
+ mIControllerCallback.onEvent(SESSION_EVENT_SEND_CUSTOM_COMMAND, bundle);
+ }
+
+ @Override
+ void onPlayerStateChanged(int playerState)
+ throws RemoteException {
+ // Note: current position should be also sent to the controller here for controller
+ // to calculate the position more correctly.
+ Bundle bundle = new Bundle();
+ bundle.putInt(ARGUMENT_PLAYER_STATE, playerState);
+ bundle.putParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT, mSession.getPlaybackStateCompat());
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYER_STATE_CHANGED, bundle);
+ }
+
+ @Override
+ void onPlaybackSpeedChanged(float speed) throws RemoteException {
+ // Note: current position should be also sent to the controller here for controller
+ // to calculate the position more correctly.
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(
+ ARGUMENT_PLAYBACK_STATE_COMPAT, mSession.getPlaybackStateCompat());
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED, bundle);
+ }
+
+ @Override
+ void onBufferingStateChanged(MediaItem2 item, int state) throws RemoteException {
+ // Note: buffered position should be also sent to the controller here. It's to
+ // follow the behavior of MediaPlayerInterface.PlayerEventCallback.
+ Bundle bundle = new Bundle();
+ bundle.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
+ bundle.putInt(ARGUMENT_BUFFERING_STATE, state);
+ bundle.putParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT,
+ mSession.getPlaybackStateCompat());
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_BUFFERING_STATE_CHANGED, bundle);
+
+ }
+
+ @Override
+ void onSeekCompleted(long position) throws RemoteException {
+ // Note: current position should be also sent to the controller here because the
+ // position here may refer to the parameter of the previous seek() API calls.
+ Bundle bundle = new Bundle();
+ bundle.putLong(ARGUMENT_SEEK_POSITION, position);
+ bundle.putParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT,
+ mSession.getPlaybackStateCompat());
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_SEEK_COMPLETED, bundle);
+ }
+
+ @Override
+ void onError(int errorCode, Bundle extras) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putInt(ARGUMENT_ERROR_CODE, errorCode);
+ bundle.putBundle(ARGUMENT_EXTRAS, extras);
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_ERROR, bundle);
+ }
+
+ @Override
+ void onCurrentMediaItemChanged(MediaItem2 item) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putBundle(ARGUMENT_MEDIA_ITEM, (item == null) ? null : item.toBundle());
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED, bundle);
+ }
+
+ @Override
+ void onPlaylistChanged(List<MediaItem2> playlist, MediaMetadata2 metadata)
+ throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putParcelableArray(ARGUMENT_PLAYLIST,
+ MediaUtils2.toMediaItem2ParcelableArray(playlist));
+ bundle.putBundle(ARGUMENT_PLAYLIST_METADATA,
+ metadata == null ? null : metadata.toBundle());
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYLIST_CHANGED, bundle);
+ }
+
+ @Override
+ void onPlaylistMetadataChanged(MediaMetadata2 metadata) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putBundle(ARGUMENT_PLAYLIST_METADATA,
+ metadata == null ? null : metadata.toBundle());
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED, bundle);
+ }
+
+ @Override
+ void onShuffleModeChanged(int shuffleMode) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putInt(ARGUMENT_SHUFFLE_MODE, shuffleMode);
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED, bundle);
+ }
+
+ @Override
+ void onRepeatModeChanged(int repeatMode) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putInt(ARGUMENT_REPEAT_MODE, repeatMode);
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_REPEAT_MODE_CHANGED, bundle);
+ }
+
+ @Override
+ void onRoutesInfoChanged(List<Bundle> routes) throws RemoteException {
+ Bundle bundle = null;
+ if (routes != null) {
+ bundle = new Bundle();
+ bundle.putParcelableArray(ARGUMENT_ROUTE_BUNDLE, routes.toArray(new Bundle[0]));
+ }
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_ROUTES_INFO_CHANGED, bundle);
+ }
+
+ @Override
+ void onChildrenChanged(String parentId, int itemCount, Bundle extras)
+ throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putString(ARGUMENT_MEDIA_ID, parentId);
+ bundle.putInt(ARGUMENT_ITEM_COUNT, itemCount);
+ bundle.putBundle(ARGUMENT_EXTRAS, extras);
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_CHILDREN_CHANGED, bundle);
+ }
+
+ @Override
+ void onSearchResultChanged(String query, int itemCount, Bundle extras)
+ throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putString(ARGUMENT_QUERY, query);
+ bundle.putInt(ARGUMENT_ITEM_COUNT, itemCount);
+ bundle.putBundle(ARGUMENT_EXTRAS, extras);
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_SEARCH_RESULT_CHANGED, bundle);
+ }
+ }
}
diff --git a/media/src/main/java/androidx/media/MediaSessionLegacyStub.java b/media/src/main/java/androidx/media/MediaSessionLegacyStub.java
new file mode 100644
index 0000000..2e0f945
--- /dev/null
+++ b/media/src/main/java/androidx/media/MediaSessionLegacyStub.java
@@ -0,0 +1,583 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static androidx.media.MediaConstants2.ARGUMENT_ALLOWED_COMMANDS;
+import static androidx.media.MediaConstants2.ARGUMENT_ARGUMENTS;
+import static androidx.media.MediaConstants2.ARGUMENT_BUFFERING_STATE;
+import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_BUTTONS;
+import static androidx.media.MediaConstants2.ARGUMENT_CUSTOM_COMMAND;
+import static androidx.media.MediaConstants2.ARGUMENT_ERROR_CODE;
+import static androidx.media.MediaConstants2.ARGUMENT_EXTRAS;
+import static androidx.media.MediaConstants2.ARGUMENT_ICONTROLLER_CALLBACK;
+import static androidx.media.MediaConstants2.ARGUMENT_ITEM_COUNT;
+import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ID;
+import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ITEM;
+import static androidx.media.MediaConstants2.ARGUMENT_PACKAGE_NAME;
+import static androidx.media.MediaConstants2.ARGUMENT_PID;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_INFO;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_STATE_COMPAT;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYER_STATE;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_METADATA;
+import static androidx.media.MediaConstants2.ARGUMENT_QUERY;
+import static androidx.media.MediaConstants2.ARGUMENT_REPEAT_MODE;
+import static androidx.media.MediaConstants2.ARGUMENT_RESULT_RECEIVER;
+import static androidx.media.MediaConstants2.ARGUMENT_ROUTE_BUNDLE;
+import static androidx.media.MediaConstants2.ARGUMENT_SEEK_POSITION;
+import static androidx.media.MediaConstants2.ARGUMENT_SHUFFLE_MODE;
+import static androidx.media.MediaConstants2.ARGUMENT_UID;
+import static androidx.media.MediaConstants2.CONNECT_RESULT_CONNECTED;
+import static androidx.media.MediaConstants2.CONNECT_RESULT_DISCONNECTED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_BUFFERING_STATE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CHILDREN_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ERROR;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYER_STATE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_REPEAT_MODE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ROUTES_INFO_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SEARCH_RESULT_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SEEK_COMPLETED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_SEND_CUSTOM_COMMAND;
+import static androidx.media.MediaConstants2.SESSION_EVENT_SET_CUSTOM_LAYOUT;
+import static androidx.media.SessionCommand2.COMMAND_CODE_CUSTOM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.support.v4.media.session.IMediaControllerCallback;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.util.Log;
+import android.util.SparseArray;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.collection.ArrayMap;
+import androidx.core.app.BundleCompat;
+import androidx.media.MediaController2.PlaybackInfo;
+import androidx.media.MediaSession2.CommandButton;
+import androidx.media.MediaSession2.ControllerCb;
+import androidx.media.MediaSession2.ControllerInfo;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@TargetApi(Build.VERSION_CODES.KITKAT)
+class MediaSessionLegacyStub extends MediaSessionCompat.Callback {
+
+ private static final String TAG = "MS2StubImplBase";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private static final SparseArray<SessionCommand2> sCommandsForOnCommandRequest =
+ new SparseArray<>();
+
+ static {
+ SessionCommandGroup2 group = new SessionCommandGroup2();
+ group.addAllPlaybackCommands();
+ group.addAllPlaylistCommands();
+ group.addAllVolumeCommands();
+ Set<SessionCommand2> commands = group.getCommands();
+ for (SessionCommand2 command : commands) {
+ sCommandsForOnCommandRequest.append(command.getCommandCode(), command);
+ }
+ }
+
+ private final Object mLock = new Object();
+
+ final MediaSession2.SupportLibraryImpl mSession;
+ final Context mContext;
+
+ @GuardedBy("mLock")
+ private final ArrayMap<IBinder, ControllerInfo> mControllers = new ArrayMap<>();
+ @GuardedBy("mLock")
+ private final Set<IBinder> mConnectingControllers = new HashSet<>();
+ @GuardedBy("mLock")
+ private final ArrayMap<ControllerInfo, SessionCommandGroup2> mAllowedCommandGroupMap =
+ new ArrayMap<>();
+
+ MediaSessionLegacyStub(MediaSession2.SupportLibraryImpl session) {
+ mSession = session;
+ mContext = mSession.getContext();
+ }
+
+ @Override
+ public void onPrepare() {
+ mSession.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ if (mSession.isClosed()) {
+ return;
+ }
+ mSession.prepare();
+ }
+ });
+ }
+
+ @Override
+ public void onPlay() {
+ mSession.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ if (mSession.isClosed()) {
+ return;
+ }
+ mSession.play();
+ }
+ });
+ }
+
+ @Override
+ public void onPause() {
+ mSession.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ if (mSession.isClosed()) {
+ return;
+ }
+ mSession.pause();
+ }
+ });
+ }
+
+ @Override
+ public void onStop() {
+ mSession.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ if (mSession.isClosed()) {
+ return;
+ }
+ mSession.reset();
+ }
+ });
+ }
+
+ @Override
+ public void onSeekTo(final long pos) {
+ mSession.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ if (mSession.isClosed()) {
+ return;
+ }
+ mSession.seekTo(pos);
+ }
+ });
+ }
+
+ List<ControllerInfo> getConnectedControllers() {
+ ArrayList<ControllerInfo> controllers = new ArrayList<>();
+ synchronized (mLock) {
+ for (int i = 0; i < mControllers.size(); i++) {
+ controllers.add(mControllers.valueAt(i));
+ }
+ }
+ return controllers;
+ }
+
+ void setAllowedCommands(ControllerInfo controller, final SessionCommandGroup2 commands) {
+ synchronized (mLock) {
+ mAllowedCommandGroupMap.put(controller, commands);
+ }
+ }
+
+ private boolean isAllowedCommand(ControllerInfo controller, SessionCommand2 command) {
+ SessionCommandGroup2 allowedCommands;
+ synchronized (mLock) {
+ allowedCommands = mAllowedCommandGroupMap.get(controller);
+ }
+ return allowedCommands != null && allowedCommands.hasCommand(command);
+ }
+
+ private boolean isAllowedCommand(ControllerInfo controller, int commandCode) {
+ SessionCommandGroup2 allowedCommands;
+ synchronized (mLock) {
+ allowedCommands = mAllowedCommandGroupMap.get(controller);
+ }
+ return allowedCommands != null && allowedCommands.hasCommand(commandCode);
+ }
+
+ private void onCommand2(@NonNull IBinder caller, final int commandCode,
+ @NonNull final Session2Runnable runnable) {
+ onCommand2Internal(caller, null, commandCode, runnable);
+ }
+
+ private void onCommand2(@NonNull IBinder caller, @NonNull final SessionCommand2 sessionCommand,
+ @NonNull final Session2Runnable runnable) {
+ onCommand2Internal(caller, sessionCommand, COMMAND_CODE_CUSTOM, runnable);
+ }
+
+ private void onCommand2Internal(@NonNull IBinder caller,
+ @Nullable final SessionCommand2 sessionCommand, final int commandCode,
+ @NonNull final Session2Runnable runnable) {
+ final ControllerInfo controller;
+ synchronized (mLock) {
+ controller = mControllers.get(caller);
+ }
+ if (mSession == null || controller == null) {
+ return;
+ }
+ mSession.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ SessionCommand2 command;
+ if (sessionCommand != null) {
+ if (!isAllowedCommand(controller, sessionCommand)) {
+ return;
+ }
+ command = sCommandsForOnCommandRequest.get(sessionCommand.getCommandCode());
+ } else {
+ if (!isAllowedCommand(controller, commandCode)) {
+ return;
+ }
+ command = sCommandsForOnCommandRequest.get(commandCode);
+ }
+ if (command != null) {
+ boolean accepted = mSession.getCallback().onCommandRequest(
+ mSession.getInstance(), controller, command);
+ if (!accepted) {
+ // Don't run rejected command.
+ if (DEBUG) {
+ Log.d(TAG, "Command (" + command + ") from "
+ + controller + " was rejected by " + mSession);
+ }
+ return;
+ }
+ }
+ try {
+ runnable.run(controller);
+ } catch (RemoteException e) {
+ // Currently it's TransactionTooLargeException or DeadSystemException.
+ // We'd better to leave log for those cases because
+ // - TransactionTooLargeException means that we may need to fix our code.
+ // (e.g. add pagination or special way to deliver Bitmap)
+ // - DeadSystemException means that errors around it can be ignored.
+ Log.w(TAG, "Exception in " + controller.toString(), e);
+ }
+ }
+ });
+ }
+
+ void removeControllerInfo(ControllerInfo controller) {
+ synchronized (mLock) {
+ controller = mControllers.remove(controller.getId());
+ if (DEBUG) {
+ Log.d(TAG, "releasing " + controller);
+ }
+ }
+ }
+
+ private ControllerInfo createControllerInfo(Bundle extras) {
+ IMediaControllerCallback callback = IMediaControllerCallback.Stub.asInterface(
+ BundleCompat.getBinder(extras, ARGUMENT_ICONTROLLER_CALLBACK));
+ String packageName = extras.getString(ARGUMENT_PACKAGE_NAME);
+ int uid = extras.getInt(ARGUMENT_UID);
+ int pid = extras.getInt(ARGUMENT_PID);
+ return new ControllerInfo(packageName, pid, uid, new ControllerLegacyCb(callback));
+ }
+
+ private void connect(Bundle extras, final ResultReceiver cb) {
+ final ControllerInfo controllerInfo = createControllerInfo(extras);
+ mSession.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ if (mSession.isClosed()) {
+ return;
+ }
+ synchronized (mLock) {
+ // Keep connecting controllers.
+ // This helps sessions to call APIs in the onConnect()
+ // (e.g. setCustomLayout()) instead of pending them.
+ mConnectingControllers.add(controllerInfo.getId());
+ }
+ SessionCommandGroup2 allowedCommands = mSession.getCallback().onConnect(
+ mSession.getInstance(), controllerInfo);
+ // Don't reject connection for the request from trusted app.
+ // Otherwise server will fail to retrieve session's information to dispatch
+ // media keys to.
+ boolean accept = allowedCommands != null || controllerInfo.isTrusted();
+ if (accept) {
+ if (DEBUG) {
+ Log.d(TAG, "Accepting connection, controllerInfo=" + controllerInfo
+ + " allowedCommands=" + allowedCommands);
+ }
+ if (allowedCommands == null) {
+ // For trusted apps, send non-null allowed commands to keep
+ // connection.
+ allowedCommands = new SessionCommandGroup2();
+ }
+ synchronized (mLock) {
+ mConnectingControllers.remove(controllerInfo.getId());
+ mControllers.put(controllerInfo.getId(), controllerInfo);
+ mAllowedCommandGroupMap.put(controllerInfo, allowedCommands);
+ }
+ // If connection is accepted, notify the current state to the
+ // controller. It's needed because we cannot call synchronous calls
+ // between session/controller.
+ // Note: We're doing this after the onConnectionChanged(), but there's
+ // no guarantee that events here are notified after the
+ // onConnected() because IMediaController2 is oneway (i.e. async
+ // call) and Stub will use thread poll for incoming calls.
+ final Bundle resultData = new Bundle();
+ resultData.putBundle(ARGUMENT_ALLOWED_COMMANDS,
+ allowedCommands.toBundle());
+ resultData.putInt(ARGUMENT_PLAYER_STATE, mSession.getPlayerState());
+ resultData.putInt(ARGUMENT_BUFFERING_STATE, mSession.getBufferingState());
+ resultData.putParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT,
+ mSession.getPlaybackStateCompat());
+ resultData.putInt(ARGUMENT_REPEAT_MODE, mSession.getRepeatMode());
+ resultData.putInt(ARGUMENT_SHUFFLE_MODE, mSession.getShuffleMode());
+ final List<MediaItem2> playlist = allowedCommands.hasCommand(
+ COMMAND_CODE_PLAYLIST_GET_LIST) ? mSession.getPlaylist() : null;
+ if (playlist != null) {
+ resultData.putParcelableArray(ARGUMENT_PLAYLIST,
+ MediaUtils2.toMediaItem2ParcelableArray(playlist));
+ }
+ final MediaItem2 currentMediaItem =
+ allowedCommands.hasCommand(COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM)
+ ? mSession.getCurrentMediaItem() : null;
+ if (currentMediaItem != null) {
+ resultData.putBundle(ARGUMENT_MEDIA_ITEM, currentMediaItem.toBundle());
+ }
+ resultData.putBundle(ARGUMENT_PLAYBACK_INFO,
+ mSession.getPlaybackInfo().toBundle());
+ final MediaMetadata2 playlistMetadata = mSession.getPlaylistMetadata();
+ if (playlistMetadata != null) {
+ resultData.putBundle(ARGUMENT_PLAYLIST_METADATA,
+ playlistMetadata.toBundle());
+ }
+ // Double check if session is still there, because close() can be
+ // called in another thread.
+ if (mSession.isClosed()) {
+ return;
+ }
+ cb.send(CONNECT_RESULT_CONNECTED, resultData);
+ } else {
+ synchronized (mLock) {
+ mConnectingControllers.remove(controllerInfo.getId());
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Rejecting connection, controllerInfo=" + controllerInfo);
+ }
+ cb.send(CONNECT_RESULT_DISCONNECTED, null);
+ }
+ }
+ });
+ }
+
+ private void disconnect(Bundle extras) {
+ final ControllerInfo controllerInfo = createControllerInfo(extras);
+ mSession.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ if (mSession.isClosed()) {
+ return;
+ }
+ mSession.getCallback().onDisconnected(mSession.getInstance(), controllerInfo);
+ }
+ });
+ }
+
+ @FunctionalInterface
+ private interface Session2Runnable {
+ void run(ControllerInfo controller) throws RemoteException;
+ }
+
+ final class ControllerLegacyCb extends ControllerCb {
+ private final IMediaControllerCallback mIControllerCallback;
+
+ ControllerLegacyCb(@NonNull IMediaControllerCallback callback) {
+ mIControllerCallback = callback;
+ }
+
+ @Override
+ @NonNull IBinder getId() {
+ return mIControllerCallback.asBinder();
+ }
+
+ @Override
+ void onCustomLayoutChanged(List<CommandButton> layout) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putParcelableArray(ARGUMENT_COMMAND_BUTTONS,
+ MediaUtils2.toCommandButtonParcelableArray(layout));
+ mIControllerCallback.onEvent(SESSION_EVENT_SET_CUSTOM_LAYOUT, bundle);
+ }
+
+ @Override
+ void onPlaybackInfoChanged(PlaybackInfo info) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putBundle(ARGUMENT_PLAYBACK_INFO, info.toBundle());
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED, bundle);
+
+ }
+
+ @Override
+ void onAllowedCommandsChanged(SessionCommandGroup2 commands) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putBundle(ARGUMENT_ALLOWED_COMMANDS, commands.toBundle());
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED, bundle);
+ }
+
+ @Override
+ void onCustomCommand(SessionCommand2 command, Bundle args, ResultReceiver receiver)
+ throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putBundle(ARGUMENT_CUSTOM_COMMAND, command.toBundle());
+ bundle.putBundle(ARGUMENT_ARGUMENTS, args);
+ bundle.putParcelable(ARGUMENT_RESULT_RECEIVER, receiver);
+ mIControllerCallback.onEvent(SESSION_EVENT_SEND_CUSTOM_COMMAND, bundle);
+ }
+
+ @Override
+ void onPlayerStateChanged(int playerState)
+ throws RemoteException {
+ // Note: current position should be also sent to the controller here for controller
+ // to calculate the position more correctly.
+ Bundle bundle = new Bundle();
+ bundle.putInt(ARGUMENT_PLAYER_STATE, playerState);
+ bundle.putParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT, mSession.getPlaybackStateCompat());
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYER_STATE_CHANGED, bundle);
+ }
+
+ @Override
+ void onPlaybackSpeedChanged(float speed) throws RemoteException {
+ // Note: current position should be also sent to the controller here for controller
+ // to calculate the position more correctly.
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(
+ ARGUMENT_PLAYBACK_STATE_COMPAT, mSession.getPlaybackStateCompat());
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED, bundle);
+ }
+
+ @Override
+ void onBufferingStateChanged(MediaItem2 item, int state) throws RemoteException {
+ // Note: buffered position should be also sent to the controller here. It's to
+ // follow the behavior of MediaPlayerInterface.PlayerEventCallback.
+ Bundle bundle = new Bundle();
+ bundle.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
+ bundle.putInt(ARGUMENT_BUFFERING_STATE, state);
+ bundle.putParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT,
+ mSession.getPlaybackStateCompat());
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_BUFFERING_STATE_CHANGED, bundle);
+
+ }
+
+ @Override
+ void onSeekCompleted(long position) throws RemoteException {
+ // Note: current position should be also sent to the controller here because the
+ // position here may refer to the parameter of the previous seek() API calls.
+ Bundle bundle = new Bundle();
+ bundle.putLong(ARGUMENT_SEEK_POSITION, position);
+ bundle.putParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT,
+ mSession.getPlaybackStateCompat());
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_SEEK_COMPLETED, bundle);
+ }
+
+ @Override
+ void onError(int errorCode, Bundle extras) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putInt(ARGUMENT_ERROR_CODE, errorCode);
+ bundle.putBundle(ARGUMENT_EXTRAS, extras);
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_ERROR, bundle);
+ }
+
+ @Override
+ void onCurrentMediaItemChanged(MediaItem2 item) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putBundle(ARGUMENT_MEDIA_ITEM, (item == null) ? null : item.toBundle());
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED, bundle);
+ }
+
+ @Override
+ void onPlaylistChanged(List<MediaItem2> playlist, MediaMetadata2 metadata)
+ throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putParcelableArray(ARGUMENT_PLAYLIST,
+ MediaUtils2.toMediaItem2ParcelableArray(playlist));
+ bundle.putBundle(ARGUMENT_PLAYLIST_METADATA,
+ metadata == null ? null : metadata.toBundle());
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYLIST_CHANGED, bundle);
+ }
+
+ @Override
+ void onPlaylistMetadataChanged(MediaMetadata2 metadata) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putBundle(ARGUMENT_PLAYLIST_METADATA,
+ metadata == null ? null : metadata.toBundle());
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED, bundle);
+ }
+
+ @Override
+ void onShuffleModeChanged(int shuffleMode) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putInt(ARGUMENT_SHUFFLE_MODE, shuffleMode);
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED, bundle);
+ }
+
+ @Override
+ void onRepeatModeChanged(int repeatMode) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putInt(ARGUMENT_REPEAT_MODE, repeatMode);
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_REPEAT_MODE_CHANGED, bundle);
+ }
+
+ @Override
+ void onRoutesInfoChanged(List<Bundle> routes) throws RemoteException {
+ Bundle bundle = null;
+ if (routes != null) {
+ bundle = new Bundle();
+ bundle.putParcelableArray(ARGUMENT_ROUTE_BUNDLE, routes.toArray(new Bundle[0]));
+ }
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_ROUTES_INFO_CHANGED, bundle);
+ }
+
+ @Override
+ void onChildrenChanged(String parentId, int itemCount, Bundle extras)
+ throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putString(ARGUMENT_MEDIA_ID, parentId);
+ bundle.putInt(ARGUMENT_ITEM_COUNT, itemCount);
+ bundle.putBundle(ARGUMENT_EXTRAS, extras);
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_CHILDREN_CHANGED, bundle);
+ }
+
+ @Override
+ void onSearchResultChanged(String query, int itemCount, Bundle extras)
+ throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putString(ARGUMENT_QUERY, query);
+ bundle.putInt(ARGUMENT_ITEM_COUNT, itemCount);
+ bundle.putBundle(ARGUMENT_EXTRAS, extras);
+ mIControllerCallback.onEvent(SESSION_EVENT_ON_SEARCH_RESULT_CHANGED, bundle);
+ }
+ }
+}
diff --git a/media/src/main/java/androidx/media/MediaSessionManager.java b/media/src/main/java/androidx/media/MediaSessionManager.java
new file mode 100644
index 0000000..8c11228
--- /dev/null
+++ b/media/src/main/java/androidx/media/MediaSessionManager.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import android.content.Context;
+import android.os.Build;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.os.BuildCompat;
+
+/**
+ * Provides support for interacting with {@link MediaSessionCompat media sessions} that
+ * applications have published to express their ongoing media playback state.
+ *
+ * @see MediaSessionCompat
+ * @see MediaControllerCompat
+ */
+public final class MediaSessionManager {
+ static final String TAG = "MediaSessionManager";
+ static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private static final Object sLock = new Object();
+ private static volatile MediaSessionManager sSessionManager;
+
+ MediaSessionManagerImpl mImpl;
+
+ /**
+ * Gets an instance of the media session manager associated with the context.
+ *
+ * @return The MediaSessionManager instance for this context.
+ */
+ public static @NonNull MediaSessionManager getSessionManager(@NonNull Context context) {
+ MediaSessionManager manager = sSessionManager;
+ if (manager == null) {
+ synchronized (sLock) {
+ manager = sSessionManager;
+ if (manager == null) {
+ sSessionManager = new MediaSessionManager(context.getApplicationContext());
+ manager = sSessionManager;
+ }
+ }
+ }
+ return manager;
+ }
+
+ private MediaSessionManager(Context context) {
+ if (BuildCompat.isAtLeastP()) {
+ mImpl = new MediaSessionManagerImplApi28(context);
+ } else if (Build.VERSION.SDK_INT >= 21) {
+ mImpl = new MediaSessionManagerImplApi21(context);
+ } else {
+ mImpl = new MediaSessionManagerImplBase(context);
+ }
+ }
+
+ /**
+ * Checks whether the remote user is a trusted app.
+ * <p>
+ * An app is trusted if the app holds the android.Manifest.permission.MEDIA_CONTENT_CONTROL
+ * permission or has an enabled notification listener.
+ *
+ * @param userInfo The remote user info from either
+ * {@link MediaSessionCompat#getCurrentControllerInfo()} and
+ * {@link MediaBrowserServiceCompat#getCurrentBrowserInfo()}.
+ * @return {@code true} if the remote user is trusted and its package name matches with the UID.
+ * {@code false} otherwise.
+ */
+ public boolean isTrustedForMediaControl(@NonNull RemoteUserInfo userInfo) {
+ if (userInfo == null) {
+ throw new IllegalArgumentException("userInfo should not be null");
+ }
+ return mImpl.isTrustedForMediaControl(userInfo.mImpl);
+ }
+
+ Context getContext() {
+ return mImpl.getContext();
+ }
+
+ interface MediaSessionManagerImpl {
+ Context getContext();
+ boolean isTrustedForMediaControl(RemoteUserInfoImpl userInfo);
+ }
+
+ interface RemoteUserInfoImpl {
+ String getPackageName();
+ int getPid();
+ int getUid();
+ }
+
+ /**
+ * Information of a remote user of {@link android.support.v4.media.session.MediaSessionCompat}
+ * or {@link MediaBrowserServiceCompat}.
+ * This can be used to decide whether the remote user is trusted app.
+ *
+ * @see #isTrustedForMediaControl(RemoteUserInfo)
+ */
+ public static final class RemoteUserInfo {
+ /**
+ * Used by {@link #getPackageName()} when the session is connected to the legacy controller
+ * whose exact package name cannot be obtained.
+ */
+ public static String LEGACY_CONTROLLER = "android.media.session.MediaController";
+
+ RemoteUserInfoImpl mImpl;
+
+ public RemoteUserInfo(@NonNull String packageName, int pid, int uid) {
+ if (BuildCompat.isAtLeastP()) {
+ mImpl = new MediaSessionManagerImplApi28.RemoteUserInfo(packageName, pid, uid);
+ } else {
+ mImpl = new MediaSessionManagerImplBase.RemoteUserInfo(packageName, pid, uid);
+ }
+ }
+
+ /**
+ * @return package name of the controller. Can be {@link #LEGACY_CONTROLLER} if the package
+ * name cannot be obtained.
+ */
+ public @NonNull String getPackageName() {
+ return mImpl.getPackageName();
+ }
+
+ /**
+ * @return pid of the controller
+ */
+ public int getPid() {
+ return mImpl.getPid();
+ }
+
+ /**
+ * @return uid of the controller
+ */
+ public int getUid() {
+ return mImpl.getUid();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ return mImpl.equals(obj);
+ }
+
+ @Override
+ public int hashCode() {
+ return mImpl.hashCode();
+ }
+ }
+}
diff --git a/media/src/main/java/androidx/media/MediaSessionManagerImplApi21.java b/media/src/main/java/androidx/media/MediaSessionManagerImplApi21.java
new file mode 100644
index 0000000..4fefe70
--- /dev/null
+++ b/media/src/main/java/androidx/media/MediaSessionManagerImplApi21.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+@RequiresApi(21)
+class MediaSessionManagerImplApi21 extends MediaSessionManagerImplBase {
+ MediaSessionManagerImplApi21(Context context) {
+ super(context);
+ mContext = context;
+ }
+
+ @Override
+ public boolean isTrustedForMediaControl(
+ @NonNull MediaSessionManager.RemoteUserInfoImpl userInfo) {
+
+ return hasMediaControlPermission(userInfo) || super.isTrustedForMediaControl(userInfo);
+ }
+
+ /**
+ * Checks the caller has android.Manifest.permission.MEDIA_CONTENT_CONTROL permission.
+ */
+ private boolean hasMediaControlPermission(
+ @NonNull MediaSessionManager.RemoteUserInfoImpl userInfo) {
+ return getContext().checkPermission(
+ android.Manifest.permission.MEDIA_CONTENT_CONTROL,
+ userInfo.getPid(), userInfo.getUid()) == PackageManager.PERMISSION_GRANTED;
+ }
+}
diff --git a/media/src/main/java/androidx/media/MediaSessionManagerImplApi28.java b/media/src/main/java/androidx/media/MediaSessionManagerImplApi28.java
new file mode 100644
index 0000000..48344fa
--- /dev/null
+++ b/media/src/main/java/androidx/media/MediaSessionManagerImplApi28.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import android.content.Context;
+
+import androidx.annotation.RequiresApi;
+
+@RequiresApi(28)
+class MediaSessionManagerImplApi28 extends MediaSessionManagerImplApi21 {
+ android.media.session.MediaSessionManager mObject;
+
+ MediaSessionManagerImplApi28(Context context) {
+ super(context);
+ mObject = (android.media.session.MediaSessionManager) context
+ .getSystemService(Context.MEDIA_SESSION_SERVICE);
+ }
+
+ @Override
+ public boolean isTrustedForMediaControl(MediaSessionManager.RemoteUserInfoImpl userInfo) {
+ if (userInfo instanceof RemoteUserInfo) {
+ return mObject.isTrustedForMediaControl(((RemoteUserInfo) userInfo).mObject);
+ }
+ return false;
+ }
+
+ static final class RemoteUserInfo implements MediaSessionManager.RemoteUserInfoImpl {
+ android.media.session.MediaSessionManager.RemoteUserInfo mObject;
+
+ RemoteUserInfo(String packageName, int pid, int uid) {
+ mObject = new android.media.session.MediaSessionManager.RemoteUserInfo(
+ packageName, pid, uid);
+ }
+
+ @Override
+ public String getPackageName() {
+ return mObject.getPackageName();
+ }
+
+ @Override
+ public int getPid() {
+ return mObject.getPid();
+ }
+
+ @Override
+ public int getUid() {
+ return mObject.getUid();
+ }
+ }
+}
diff --git a/media/src/main/java/androidx/media/MediaSessionManagerImplBase.java b/media/src/main/java/androidx/media/MediaSessionManagerImplBase.java
new file mode 100644
index 0000000..3fc21f1
--- /dev/null
+++ b/media/src/main/java/androidx/media/MediaSessionManagerImplBase.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Process;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.ObjectsCompat;
+
+class MediaSessionManagerImplBase implements MediaSessionManager.MediaSessionManagerImpl {
+ private static final String TAG = MediaSessionManager.TAG;
+ private static final boolean DEBUG = MediaSessionManager.DEBUG;
+
+ private static final String PERMISSION_STATUS_BAR_SERVICE =
+ "android.permission.STATUS_BAR_SERVICE";
+ private static final String PERMISSION_MEDIA_CONTENT_CONTROL =
+ "android.permission.MEDIA_CONTENT_CONTROL";
+ private static final String ENABLED_NOTIFICATION_LISTENERS = "enabled_notification_listeners";
+
+ Context mContext;
+ ContentResolver mContentResolver;
+
+ MediaSessionManagerImplBase(Context context) {
+ mContext = context;
+ mContentResolver = mContext.getContentResolver();
+ }
+
+ @Override
+ public Context getContext() {
+ return mContext;
+ }
+
+ @Override
+ public boolean isTrustedForMediaControl(
+ @NonNull MediaSessionManager.RemoteUserInfoImpl userInfo) {
+ ApplicationInfo applicationInfo;
+ try {
+ applicationInfo = mContext.getPackageManager().getApplicationInfo(
+ userInfo.getPackageName(), 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ if (DEBUG) {
+ Log.d(TAG, "Package " + userInfo.getPackageName() + " doesn't exist");
+ }
+ return false;
+ }
+
+ if (applicationInfo.uid != userInfo.getUid()) {
+ if (DEBUG) {
+ Log.d(TAG, "Package name " + userInfo.getPackageName()
+ + " doesn't match with the uid " + userInfo.getUid());
+ }
+ return false;
+ }
+ return isPermissionGranted(userInfo, PERMISSION_STATUS_BAR_SERVICE)
+ || isPermissionGranted(userInfo, PERMISSION_MEDIA_CONTENT_CONTROL)
+ || userInfo.getUid() == Process.SYSTEM_UID
+ || isEnabledNotificationListener(userInfo);
+ }
+
+ private boolean isPermissionGranted(MediaSessionManager.RemoteUserInfoImpl userInfo,
+ String permission) {
+ if (userInfo.getPid() < 0) {
+ // This may happen for the MediaBrowserServiceCompat#onGetRoot().
+ return mContext.getPackageManager().checkPermission(
+ permission, userInfo.getPackageName()) == PackageManager.PERMISSION_GRANTED;
+ }
+ return mContext.checkPermission(permission, userInfo.getPid(), userInfo.getUid())
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ /**
+ * This checks if the component is an enabled notification listener for the
+ * specified user. Enabled components may only operate on behalf of the user
+ * they're running as.
+ *
+ * @return True if the component is enabled, false otherwise
+ */
+ @SuppressWarnings("StringSplitter")
+ boolean isEnabledNotificationListener(
+ @NonNull MediaSessionManager.RemoteUserInfoImpl userInfo) {
+ final String enabledNotifListeners = Settings.Secure.getString(mContentResolver,
+ ENABLED_NOTIFICATION_LISTENERS);
+ if (enabledNotifListeners != null) {
+ final String[] components = enabledNotifListeners.split(":");
+ for (int i = 0; i < components.length; i++) {
+ final ComponentName component =
+ ComponentName.unflattenFromString(components[i]);
+ if (component != null) {
+ if (component.getPackageName().equals(userInfo.getPackageName())) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ static class RemoteUserInfo implements MediaSessionManager.RemoteUserInfoImpl {
+ private String mPackageName;
+ private int mPid;
+ private int mUid;
+
+ RemoteUserInfo(String packageName, int pid, int uid) {
+ mPackageName = packageName;
+ mPid = pid;
+ mUid = uid;
+ }
+
+ @Override
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ @Override
+ public int getPid() {
+ return mPid;
+ }
+
+ @Override
+ public int getUid() {
+ return mUid;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof RemoteUserInfo)) {
+ return false;
+ }
+ RemoteUserInfo otherUserInfo = (RemoteUserInfo) obj;
+ return TextUtils.equals(mPackageName, otherUserInfo.mPackageName)
+ && mPid == otherUserInfo.mPid
+ && mUid == otherUserInfo.mUid;
+ }
+
+ @Override
+ public int hashCode() {
+ return ObjectsCompat.hash(mPackageName, mPid, mUid);
+ }
+ }
+}
+
diff --git a/media/src/main/java/androidx/media/MediaSessionService2.java b/media/src/main/java/androidx/media/MediaSessionService2.java
index 7bad65c..03bb086 100644
--- a/media/src/main/java/androidx/media/MediaSessionService2.java
+++ b/media/src/main/java/androidx/media/MediaSessionService2.java
@@ -16,8 +16,6 @@
package androidx.media;
-import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
@@ -31,7 +29,6 @@
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
import androidx.media.MediaBrowserServiceCompat.BrowserRoot;
import androidx.media.MediaSession2.ControllerInfo;
import androidx.media.SessionToken2.TokenType;
@@ -39,7 +36,6 @@
import java.util.List;
/**
- * @hide
* Base class for media session services, which is the service version of the {@link MediaSession2}.
* <p>
* It's highly recommended for an app to use this instead of {@link MediaSession2} if it wants
@@ -97,7 +93,6 @@
* <p>
* After the binding, session's
* {@link MediaSession2.SessionCallback#onConnect(MediaSession2, ControllerInfo)}
- *
* will be called to accept or reject connection request from a controller. If the connection is
* rejected, the controller will unbind. If it's accepted, the controller will be available to use
* and keep binding.
@@ -106,6 +101,9 @@
* is called and service would become a foreground service. It's needed to keep playback after the
* controller is destroyed. The session service becomes background service when the playback is
* stopped.
+ * <p>
+ * The service is destroyed when the session is closed, or no media controller is bounded to the
+ * session while the service is not running as a foreground service.
* <a name="Permissions"></a>
* <h3>Permissions</h3>
* <p>
@@ -113,10 +111,7 @@
* the session service accepted the connection request through
* {@link MediaSession2.SessionCallback#onConnect(MediaSession2, ControllerInfo)}.
*/
-@RestrictTo(LIBRARY_GROUP)
public abstract class MediaSessionService2 extends Service {
- //private final MediaSessionService2Provider mProvider;
-
/**
* This is the interface name that a service implementing a session service should say that it
* support -- that is, this is the action it uses for its intent filter.
@@ -316,8 +311,6 @@
// 2. MediaSessionService2 is defined as the simplified version of the library
// service with no browsing feature, so shouldn't allow MediaBrowserServiceCompat
// specific operations.
- // TODO: Revisit here API not to return stub root here. The fake media ID here may be
- // used by the browser service for real.
return sDefaultBrowserRoot;
}
diff --git a/media/src/main/java/androidx/media/MediaUtils2.java b/media/src/main/java/androidx/media/MediaUtils2.java
index 657e24d..6a011f2 100644
--- a/media/src/main/java/androidx/media/MediaUtils2.java
+++ b/media/src/main/java/androidx/media/MediaUtils2.java
@@ -16,8 +16,6 @@
package androidx.media;
-import static androidx.media.AudioAttributesCompat.CONTENT_TYPE_UNKNOWN;
-import static androidx.media.AudioAttributesCompat.USAGE_UNKNOWN;
import static androidx.media.MediaMetadata2.METADATA_KEY_DISPLAY_DESCRIPTION;
import static androidx.media.MediaMetadata2.METADATA_KEY_DISPLAY_ICON;
import static androidx.media.MediaMetadata2.METADATA_KEY_DISPLAY_ICON_URI;
@@ -44,9 +42,7 @@
import java.util.List;
class MediaUtils2 {
- static final String AUDIO_ATTRIBUTES_USAGE = "androidx.media.audio_attrs.USAGE";
- static final String AUDIO_ATTRIBUTES_CONTENT_TYPE = "androidx.media.audio_attrs.CONTENT_TYPE";
- static final String AUDIO_ATTRIBUTES_FLAGS = "androidx.media.audio_attrs.FLAGS";
+ static final String TAG = "MediaUtils2";
private MediaUtils2() {
}
@@ -116,6 +112,28 @@
.build();
}
+ static List<MediaItem> fromMediaItem2List(List<MediaItem2> items) {
+ if (items == null) {
+ return null;
+ }
+ List<MediaItem> result = new ArrayList<>();
+ for (int i = 0; i < items.size(); i++) {
+ result.add(createMediaItem(items.get(i)));
+ }
+ return result;
+ }
+
+ static List<MediaItem2> toMediaItem2List(List<MediaItem> items) {
+ if (items == null) {
+ return null;
+ }
+ List<MediaItem2> result = new ArrayList<>();
+ for (int i = 0; i < items.size(); i++) {
+ result.add(createMediaItem2(items.get(i)));
+ }
+ return result;
+ }
+
/**
* Creates a {@link MediaMetadata2} from the {@link MediaDescriptionCompat}.
*
@@ -351,28 +369,6 @@
return layout;
}
- static Bundle toAudioAttributesBundle(AudioAttributesCompat attrs) {
- if (attrs == null) {
- return null;
- }
- Bundle bundle = new Bundle();
- bundle.putInt(AUDIO_ATTRIBUTES_USAGE, attrs.getUsage());
- bundle.putInt(AUDIO_ATTRIBUTES_CONTENT_TYPE, attrs.getContentType());
- bundle.putInt(AUDIO_ATTRIBUTES_FLAGS, attrs.getFlags());
- return bundle;
- }
-
- static AudioAttributesCompat fromAudioAttributesBundle(Bundle bundle) {
- if (bundle == null) {
- return null;
- }
- return new AudioAttributesCompat.Builder()
- .setUsage(bundle.getInt(AUDIO_ATTRIBUTES_USAGE, USAGE_UNKNOWN))
- .setContentType(bundle.getInt(AUDIO_ATTRIBUTES_CONTENT_TYPE, CONTENT_TYPE_UNKNOWN))
- .setFlags(bundle.getInt(AUDIO_ATTRIBUTES_FLAGS, 0))
- .build();
- }
-
static List<Bundle> toBundleList(Parcelable[] array) {
if (array == null) {
return null;
@@ -386,17 +382,17 @@
static int createPlaybackStateCompatState(int playerState, int bufferingState) {
switch (playerState) {
- case MediaPlayerBase.PLAYER_STATE_PLAYING:
+ case MediaPlayerInterface.PLAYER_STATE_PLAYING:
switch (bufferingState) {
- case MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_STARVED:
+ case MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_STARVED:
return PlaybackStateCompat.STATE_BUFFERING;
}
return PlaybackStateCompat.STATE_PLAYING;
- case MediaPlayerBase.PLAYER_STATE_PAUSED:
+ case MediaPlayerInterface.PLAYER_STATE_PAUSED:
return PlaybackStateCompat.STATE_PAUSED;
- case MediaPlayerBase.PLAYER_STATE_IDLE:
+ case MediaPlayerInterface.PLAYER_STATE_IDLE:
return PlaybackStateCompat.STATE_NONE;
- case MediaPlayerBase.PLAYER_STATE_ERROR:
+ case MediaPlayerInterface.PLAYER_STATE_ERROR:
return PlaybackStateCompat.STATE_ERROR;
}
// For unknown value
@@ -406,13 +402,13 @@
static int toPlayerState(int playbackStateCompatState) {
switch (playbackStateCompatState) {
case PlaybackStateCompat.STATE_ERROR:
- return MediaPlayerBase.PLAYER_STATE_ERROR;
+ return MediaPlayerInterface.PLAYER_STATE_ERROR;
case PlaybackStateCompat.STATE_NONE:
- return MediaPlayerBase.PLAYER_STATE_IDLE;
+ return MediaPlayerInterface.PLAYER_STATE_IDLE;
case PlaybackStateCompat.STATE_PAUSED:
case PlaybackStateCompat.STATE_STOPPED:
case PlaybackStateCompat.STATE_BUFFERING: // means paused for buffering.
- return MediaPlayerBase.PLAYER_STATE_PAUSED;
+ return MediaPlayerInterface.PLAYER_STATE_PAUSED;
case PlaybackStateCompat.STATE_FAST_FORWARDING:
case PlaybackStateCompat.STATE_PLAYING:
case PlaybackStateCompat.STATE_REWINDING:
@@ -420,12 +416,16 @@
case PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS:
case PlaybackStateCompat.STATE_SKIPPING_TO_QUEUE_ITEM:
case PlaybackStateCompat.STATE_CONNECTING: // Note: there's no perfect match for this.
- return MediaPlayerBase.PLAYER_STATE_PLAYING;
+ return MediaPlayerInterface.PLAYER_STATE_PLAYING;
}
- return MediaPlayerBase.PLAYER_STATE_ERROR;
+ return MediaPlayerInterface.PLAYER_STATE_ERROR;
}
static boolean isDefaultLibraryRootHint(Bundle bundle) {
return bundle != null && bundle.getBoolean(MediaConstants2.ROOT_EXTRA_DEFAULT, false);
}
+
+ static Bundle createBundle(Bundle bundle) {
+ return (bundle == null) ? new Bundle() : new Bundle(bundle);
+ }
}
diff --git a/media/src/main/java/androidx/media/SessionCommand2.java b/media/src/main/java/androidx/media/SessionCommand2.java
index f017941..353688a 100644
--- a/media/src/main/java/androidx/media/SessionCommand2.java
+++ b/media/src/main/java/androidx/media/SessionCommand2.java
@@ -276,52 +276,38 @@
public static final int COMMAND_CODE_SESSION_SELECT_ROUTE = 38;
/**
- * @hide
* Command code for {@link MediaBrowser2#getChildren(String, int, int, Bundle)}.
*/
- @RestrictTo(LIBRARY_GROUP)
public static final int COMMAND_CODE_LIBRARY_GET_CHILDREN = 29;
/**
- * @hide
* Command code for {@link MediaBrowser2#getItem(String)}.
*/
- @RestrictTo(LIBRARY_GROUP)
public static final int COMMAND_CODE_LIBRARY_GET_ITEM = 30;
/**
- * @hide
* Command code for {@link MediaBrowser2#getLibraryRoot(Bundle)}.
*/
- @RestrictTo(LIBRARY_GROUP)
public static final int COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT = 31;
/**
- * @hide
* Command code for {@link MediaBrowser2#getSearchResult(String, int, int, Bundle)}.
*/
- @RestrictTo(LIBRARY_GROUP)
public static final int COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT = 32;
/**
- * @hide
* Command code for {@link MediaBrowser2#search(String, Bundle)}.
*/
- @RestrictTo(LIBRARY_GROUP)
public static final int COMMAND_CODE_LIBRARY_SEARCH = 33;
/**
- * @hide
* Command code for {@link MediaBrowser2#subscribe(String, Bundle)}.
*/
- @RestrictTo(LIBRARY_GROUP)
public static final int COMMAND_CODE_LIBRARY_SUBSCRIBE = 34;
/**
- * @hide
* Command code for {@link MediaBrowser2#unsubscribe(String)}.
*/
- @RestrictTo(LIBRARY_GROUP)
public static final int COMMAND_CODE_LIBRARY_UNSUBSCRIBE = 35;
/**
diff --git a/media/src/main/java/androidx/media/SessionCommandGroup2.java b/media/src/main/java/androidx/media/SessionCommandGroup2.java
index 691eb70..db15bb3 100644
--- a/media/src/main/java/androidx/media/SessionCommandGroup2.java
+++ b/media/src/main/java/androidx/media/SessionCommandGroup2.java
@@ -42,7 +42,7 @@
private static final String KEY_COMMANDS = "android.media.mediasession2.commandgroup.commands";
// Prefix for all command codes
private static final String PREFIX_COMMAND_CODE = "COMMAND_CODE_";
- // Prefix for command codes that will be sent directly to the MediaPlayerBase
+ // Prefix for command codes that will be sent directly to the MediaPlayerInterface
private static final String PREFIX_COMMAND_CODE_PLAYBACK = "COMMAND_CODE_PLAYBACK_";
// Prefix for command codes that will be sent directly to the MediaPlaylistAgent
private static final String PREFIX_COMMAND_CODE_PLAYLIST = "COMMAND_CODE_PLAYLIST_";
diff --git a/media/src/main/java/androidx/media/SessionPlaylistAgentImplBase.java b/media/src/main/java/androidx/media/SessionPlaylistAgentImplBase.java
index 431b188..2cdc9ab 100644
--- a/media/src/main/java/androidx/media/SessionPlaylistAgentImplBase.java
+++ b/media/src/main/java/androidx/media/SessionPlaylistAgentImplBase.java
@@ -18,13 +18,13 @@
import android.annotation.TargetApi;
import android.os.Build;
-import android.util.ArrayMap;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
-import androidx.media.MediaPlayerBase.PlayerEventCallback;
+import androidx.collection.ArrayMap;
+import androidx.media.MediaPlayerInterface.PlayerEventCallback;
import androidx.media.MediaSession2.OnDataSourceMissingHelper;
import java.util.ArrayList;
@@ -46,7 +46,7 @@
private final MyPlayerEventCallback mPlayerCallback;
@GuardedBy("mLock")
- private MediaPlayerBase mPlayer;
+ private MediaPlayerInterface mPlayer;
@GuardedBy("mLock")
private OnDataSourceMissingHelper mDsmHelper;
// TODO: Check if having the same item is okay (b/74090741)
@@ -68,7 +68,7 @@
// Called on session callback executor.
private class MyPlayerEventCallback extends PlayerEventCallback {
@Override
- public void onCurrentDataSourceChanged(@NonNull MediaPlayerBase mpb,
+ public void onCurrentDataSourceChanged(@NonNull MediaPlayerInterface mpb,
@Nullable DataSourceDesc dsd) {
synchronized (mLock) {
if (mPlayer != mpb) {
@@ -133,7 +133,7 @@
}
SessionPlaylistAgentImplBase(@NonNull MediaSession2ImplBase session,
- @NonNull MediaPlayerBase player) {
+ @NonNull MediaPlayerInterface player) {
super();
if (session == null) {
throw new IllegalArgumentException("sessionImpl shouldn't be null");
@@ -147,7 +147,7 @@
mPlayer.registerPlayerEventCallback(mSession.getCallbackExecutor(), mPlayerCallback);
}
- public void setPlayer(@NonNull MediaPlayerBase player) {
+ public void setPlayer(@NonNull MediaPlayerInterface player) {
if (player == null) {
throw new IllegalArgumentException("player shouldn't be null");
}
diff --git a/media/src/main/java/androidx/media/SessionToken2.java b/media/src/main/java/androidx/media/SessionToken2.java
index eb42297..cae6c9b 100644
--- a/media/src/main/java/androidx/media/SessionToken2.java
+++ b/media/src/main/java/androidx/media/SessionToken2.java
@@ -23,7 +23,6 @@
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
-import android.media.session.MediaSessionManager;
import android.os.Bundle;
import android.support.v4.media.session.MediaSessionCompat;
import android.text.TextUtils;
@@ -38,7 +37,8 @@
import java.util.List;
/**
- * Represents an ongoing {@link MediaSession2}.
+ * Represents an ongoing {@link MediaSession2} or a {@link MediaSessionService2}.
+ * If it's representing a session service, it may not be ongoing.
* <p>
* This may be passed to apps by the session owner to allow them to create a
* {@link MediaController2} to communicate with the session.
@@ -65,15 +65,13 @@
public static final int TYPE_SESSION = 0;
/**
- * @hide
+ * Type for {@link MediaSessionService2}.
*/
- @RestrictTo(LIBRARY_GROUP)
public static final int TYPE_SESSION_SERVICE = 1;
/**
- * @hide
+ * Type for {@link MediaLibraryService2}.
*/
- @RestrictTo(LIBRARY_GROUP)
public static final int TYPE_LIBRARY_SERVICE = 2;
//private final SessionToken2Provider mProvider;
@@ -97,14 +95,12 @@
private final ComponentName mComponentName;
/**
- * @hide
* Constructor for the token. You can only create token for session service or library service
* to use by {@link MediaController2} or {@link MediaBrowser2}.
*
* @param context The context.
* @param serviceComponent The component name of the media browser service.
*/
- @RestrictTo(LIBRARY_GROUP)
public SessionToken2(@NonNull Context context, @NonNull ComponentName serviceComponent) {
this(context, serviceComponent, UID_UNKNOWN);
}
@@ -242,6 +238,8 @@
/**
* @return type of the token
* @see #TYPE_SESSION
+ * @see #TYPE_SESSION_SERVICE
+ * @see #TYPE_LIBRARY_SERVICE
*/
public @TokenType int getType() {
return mType;
diff --git a/media/src/main/java/androidx/media/subtitle/Cea608CCParser.java b/media/src/main/java/androidx/media/subtitle/Cea608CCParser.java
new file mode 100644
index 0000000..9205fba
--- /dev/null
+++ b/media/src/main/java/androidx/media/subtitle/Cea608CCParser.java
@@ -0,0 +1,987 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.subtitle;
+
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.TextPaint;
+import android.text.style.CharacterStyle;
+import android.text.style.StyleSpan;
+import android.text.style.UnderlineSpan;
+import android.text.style.UpdateAppearance;
+import android.util.Log;
+import android.view.accessibility.CaptioningManager.CaptionStyle;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * CCParser processes CEA-608 closed caption data.
+ *
+ * It calls back into OnDisplayChangedListener upon
+ * display change with styled text for rendering.
+ *
+ */
+class Cea608CCParser {
+ public static final int MAX_ROWS = 15;
+ public static final int MAX_COLS = 32;
+
+ private static final String TAG = "Cea608CCParser";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private static final int INVALID = -1;
+
+ // EIA-CEA-608: Table 70 - Control Codes
+ private static final int RCL = 0x20;
+ private static final int BS = 0x21;
+ private static final int AOF = 0x22;
+ private static final int AON = 0x23;
+ private static final int DER = 0x24;
+ private static final int RU2 = 0x25;
+ private static final int RU3 = 0x26;
+ private static final int RU4 = 0x27;
+ private static final int FON = 0x28;
+ private static final int RDC = 0x29;
+ private static final int TR = 0x2a;
+ private static final int RTD = 0x2b;
+ private static final int EDM = 0x2c;
+ private static final int CR = 0x2d;
+ private static final int ENM = 0x2e;
+ private static final int EOC = 0x2f;
+
+ // Transparent Space
+ private static final char TS = '\u00A0';
+
+ // Captioning Modes
+ private static final int MODE_UNKNOWN = 0;
+ private static final int MODE_PAINT_ON = 1;
+ private static final int MODE_ROLL_UP = 2;
+ private static final int MODE_POP_ON = 3;
+ private static final int MODE_TEXT = 4;
+
+ private final DisplayListener mListener;
+
+ private int mMode = MODE_PAINT_ON;
+ private int mRollUpSize = 4;
+ private int mPrevCtrlCode = INVALID;
+
+ private CCMemory mDisplay = new CCMemory();
+ private CCMemory mNonDisplay = new CCMemory();
+ private CCMemory mTextMem = new CCMemory();
+
+ Cea608CCParser(DisplayListener listener) {
+ mListener = listener;
+ }
+
+ public void parse(byte[] data) {
+ CCData[] ccData = CCData.fromByteArray(data);
+
+ for (int i = 0; i < ccData.length; i++) {
+ if (DEBUG) {
+ Log.d(TAG, ccData[i].toString());
+ }
+
+ if (handleCtrlCode(ccData[i])
+ || handleTabOffsets(ccData[i])
+ || handlePACCode(ccData[i])
+ || handleMidRowCode(ccData[i])) {
+ continue;
+ }
+
+ handleDisplayableChars(ccData[i]);
+ }
+ }
+
+ interface DisplayListener {
+ void onDisplayChanged(SpannableStringBuilder[] styledTexts);
+ CaptionStyle getCaptionStyle();
+ }
+
+ private CCMemory getMemory() {
+ // get the CC memory to operate on for current mode
+ switch (mMode) {
+ case MODE_POP_ON:
+ return mNonDisplay;
+ case MODE_TEXT:
+ // TODO(chz): support only caption mode for now,
+ // in text mode, dump everything to text mem.
+ return mTextMem;
+ case MODE_PAINT_ON:
+ case MODE_ROLL_UP:
+ return mDisplay;
+ default:
+ Log.w(TAG, "unrecoginized mode: " + mMode);
+ }
+ return mDisplay;
+ }
+
+ private boolean handleDisplayableChars(CCData ccData) {
+ if (!ccData.isDisplayableChar()) {
+ return false;
+ }
+
+ // Extended char includes 1 automatic backspace
+ if (ccData.isExtendedChar()) {
+ getMemory().bs();
+ }
+
+ getMemory().writeText(ccData.getDisplayText());
+
+ if (mMode == MODE_PAINT_ON || mMode == MODE_ROLL_UP) {
+ updateDisplay();
+ }
+
+ return true;
+ }
+
+ private boolean handleMidRowCode(CCData ccData) {
+ StyleCode m = ccData.getMidRow();
+ if (m != null) {
+ getMemory().writeMidRowCode(m);
+ return true;
+ }
+ return false;
+ }
+
+ private boolean handlePACCode(CCData ccData) {
+ PAC pac = ccData.getPAC();
+
+ if (pac != null) {
+ if (mMode == MODE_ROLL_UP) {
+ getMemory().moveBaselineTo(pac.getRow(), mRollUpSize);
+ }
+ getMemory().writePAC(pac);
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean handleTabOffsets(CCData ccData) {
+ int tabs = ccData.getTabOffset();
+
+ if (tabs > 0) {
+ getMemory().tab(tabs);
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean handleCtrlCode(CCData ccData) {
+ int ctrlCode = ccData.getCtrlCode();
+
+ if (mPrevCtrlCode != INVALID && mPrevCtrlCode == ctrlCode) {
+ // discard double ctrl codes (but if there's a 3rd one, we still take that)
+ mPrevCtrlCode = INVALID;
+ return true;
+ }
+
+ switch(ctrlCode) {
+ case RCL:
+ // select pop-on style
+ mMode = MODE_POP_ON;
+ break;
+ case BS:
+ getMemory().bs();
+ break;
+ case DER:
+ getMemory().der();
+ break;
+ case RU2:
+ case RU3:
+ case RU4:
+ mRollUpSize = (ctrlCode - 0x23);
+ // erase memory if currently in other style
+ if (mMode != MODE_ROLL_UP) {
+ mDisplay.erase();
+ mNonDisplay.erase();
+ }
+ // select roll-up style
+ mMode = MODE_ROLL_UP;
+ break;
+ case FON:
+ Log.i(TAG, "Flash On");
+ break;
+ case RDC:
+ // select paint-on style
+ mMode = MODE_PAINT_ON;
+ break;
+ case TR:
+ mMode = MODE_TEXT;
+ mTextMem.erase();
+ break;
+ case RTD:
+ mMode = MODE_TEXT;
+ break;
+ case EDM:
+ // erase display memory
+ mDisplay.erase();
+ updateDisplay();
+ break;
+ case CR:
+ if (mMode == MODE_ROLL_UP) {
+ getMemory().rollUp(mRollUpSize);
+ } else {
+ getMemory().cr();
+ }
+ if (mMode == MODE_ROLL_UP) {
+ updateDisplay();
+ }
+ break;
+ case ENM:
+ // erase non-display memory
+ mNonDisplay.erase();
+ break;
+ case EOC:
+ // swap display/non-display memory
+ swapMemory();
+ // switch to pop-on style
+ mMode = MODE_POP_ON;
+ updateDisplay();
+ break;
+ case INVALID:
+ default:
+ mPrevCtrlCode = INVALID;
+ return false;
+ }
+
+ mPrevCtrlCode = ctrlCode;
+
+ // handled
+ return true;
+ }
+
+ private void updateDisplay() {
+ if (mListener != null) {
+ CaptionStyle captionStyle = mListener.getCaptionStyle();
+ mListener.onDisplayChanged(mDisplay.getStyledText(captionStyle));
+ }
+ }
+
+ private void swapMemory() {
+ CCMemory temp = mDisplay;
+ mDisplay = mNonDisplay;
+ mNonDisplay = temp;
+ }
+
+ private static class StyleCode {
+ static final int COLOR_WHITE = 0;
+ static final int COLOR_GREEN = 1;
+ static final int COLOR_BLUE = 2;
+ static final int COLOR_CYAN = 3;
+ static final int COLOR_RED = 4;
+ static final int COLOR_YELLOW = 5;
+ static final int COLOR_MAGENTA = 6;
+ static final int COLOR_INVALID = 7;
+
+ static final int STYLE_ITALICS = 0x00000001;
+ static final int STYLE_UNDERLINE = 0x00000002;
+
+ static final String[] sColorMap = {
+ "WHITE", "GREEN", "BLUE", "CYAN", "RED", "YELLOW", "MAGENTA", "INVALID"
+ };
+
+ final int mStyle;
+ final int mColor;
+
+ static StyleCode fromByte(byte data2) {
+ int style = 0;
+ int color = (data2 >> 1) & 0x7;
+
+ if ((data2 & 0x1) != 0) {
+ style |= STYLE_UNDERLINE;
+ }
+
+ if (color == COLOR_INVALID) {
+ // WHITE ITALICS
+ color = COLOR_WHITE;
+ style |= STYLE_ITALICS;
+ }
+
+ return new StyleCode(style, color);
+ }
+
+ StyleCode(int style, int color) {
+ mStyle = style;
+ mColor = color;
+ }
+
+ boolean isItalics() {
+ return (mStyle & STYLE_ITALICS) != 0;
+ }
+
+ boolean isUnderline() {
+ return (mStyle & STYLE_UNDERLINE) != 0;
+ }
+
+ int getColor() {
+ return mColor;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder str = new StringBuilder();
+ str.append("{");
+ str.append(sColorMap[mColor]);
+ if ((mStyle & STYLE_ITALICS) != 0) {
+ str.append(", ITALICS");
+ }
+ if ((mStyle & STYLE_UNDERLINE) != 0) {
+ str.append(", UNDERLINE");
+ }
+ str.append("}");
+
+ return str.toString();
+ }
+ }
+
+ private static class PAC extends StyleCode {
+ final int mRow;
+ final int mCol;
+
+ static PAC fromBytes(byte data1, byte data2) {
+ int[] rowTable = {11, 1, 3, 12, 14, 5, 7, 9};
+ int row = rowTable[data1 & 0x07] + ((data2 & 0x20) >> 5);
+ int style = 0;
+ if ((data2 & 1) != 0) {
+ style |= STYLE_UNDERLINE;
+ }
+ if ((data2 & 0x10) != 0) {
+ // indent code
+ int indent = (data2 >> 1) & 0x7;
+ return new PAC(row, indent * 4, style, COLOR_WHITE);
+ } else {
+ // style code
+ int color = (data2 >> 1) & 0x7;
+
+ if (color == COLOR_INVALID) {
+ // WHITE ITALICS
+ color = COLOR_WHITE;
+ style |= STYLE_ITALICS;
+ }
+ return new PAC(row, -1, style, color);
+ }
+ }
+
+ PAC(int row, int col, int style, int color) {
+ super(style, color);
+ mRow = row;
+ mCol = col;
+ }
+
+ boolean isIndentPAC() {
+ return (mCol >= 0);
+ }
+
+ int getRow() {
+ return mRow;
+ }
+
+ int getCol() {
+ return mCol;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("{%d, %d}, %s",
+ mRow, mCol, super.toString());
+ }
+ }
+
+ /**
+ * Mutable version of BackgroundSpan to facilitate text rendering with edge styles.
+ */
+ public static class MutableBackgroundColorSpan extends CharacterStyle
+ implements UpdateAppearance {
+ private int mColor;
+
+ MutableBackgroundColorSpan(int color) {
+ mColor = color;
+ }
+
+ public void setBackgroundColor(int color) {
+ mColor = color;
+ }
+
+ public int getBackgroundColor() {
+ return mColor;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.bgColor = mColor;
+ }
+ }
+
+ /* CCLineBuilder keeps track of displayable chars, as well as
+ * MidRow styles and PACs, for a single line of CC memory.
+ *
+ * It generates styled text via getStyledText() method.
+ */
+ private static class CCLineBuilder {
+ private final StringBuilder mDisplayChars;
+ private final StyleCode[] mMidRowStyles;
+ private final StyleCode[] mPACStyles;
+
+ CCLineBuilder(String str) {
+ mDisplayChars = new StringBuilder(str);
+ mMidRowStyles = new StyleCode[mDisplayChars.length()];
+ mPACStyles = new StyleCode[mDisplayChars.length()];
+ }
+
+ void setCharAt(int index, char ch) {
+ mDisplayChars.setCharAt(index, ch);
+ mMidRowStyles[index] = null;
+ }
+
+ void setMidRowAt(int index, StyleCode m) {
+ mDisplayChars.setCharAt(index, ' ');
+ mMidRowStyles[index] = m;
+ }
+
+ void setPACAt(int index, PAC pac) {
+ mPACStyles[index] = pac;
+ }
+
+ char charAt(int index) {
+ return mDisplayChars.charAt(index);
+ }
+
+ int length() {
+ return mDisplayChars.length();
+ }
+
+ void applyStyleSpan(
+ SpannableStringBuilder styledText,
+ StyleCode s, int start, int end) {
+ if (s.isItalics()) {
+ styledText.setSpan(
+ new StyleSpan(android.graphics.Typeface.ITALIC),
+ start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (s.isUnderline()) {
+ styledText.setSpan(
+ new UnderlineSpan(),
+ start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+
+ SpannableStringBuilder getStyledText(CaptionStyle captionStyle) {
+ SpannableStringBuilder styledText = new SpannableStringBuilder(mDisplayChars);
+ int start = -1, next = 0;
+ int styleStart = -1;
+ StyleCode curStyle = null;
+ while (next < mDisplayChars.length()) {
+ StyleCode newStyle = null;
+ if (mMidRowStyles[next] != null) {
+ // apply mid-row style change
+ newStyle = mMidRowStyles[next];
+ } else if (mPACStyles[next] != null && (styleStart < 0 || start < 0)) {
+ // apply PAC style change, only if:
+ // 1. no style set, or
+ // 2. style set, but prev char is none-displayable
+ newStyle = mPACStyles[next];
+ }
+ if (newStyle != null) {
+ curStyle = newStyle;
+ if (styleStart >= 0 && start >= 0) {
+ applyStyleSpan(styledText, newStyle, styleStart, next);
+ }
+ styleStart = next;
+ }
+
+ if (mDisplayChars.charAt(next) != TS) {
+ if (start < 0) {
+ start = next;
+ }
+ } else if (start >= 0) {
+ int expandedStart = mDisplayChars.charAt(start) == ' ' ? start : start - 1;
+ int expandedEnd = mDisplayChars.charAt(next - 1) == ' ' ? next : next + 1;
+ styledText.setSpan(
+ new MutableBackgroundColorSpan(captionStyle.backgroundColor),
+ expandedStart, expandedEnd,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ if (styleStart >= 0) {
+ applyStyleSpan(styledText, curStyle, styleStart, expandedEnd);
+ }
+ start = -1;
+ }
+ next++;
+ }
+
+ return styledText;
+ }
+ }
+
+ /*
+ * CCMemory models a console-style display.
+ */
+ private static class CCMemory {
+ private final String mBlankLine;
+ private final CCLineBuilder[] mLines = new CCLineBuilder[MAX_ROWS + 2];
+ private int mRow;
+ private int mCol;
+
+ CCMemory() {
+ char[] blank = new char[MAX_COLS + 2];
+ Arrays.fill(blank, TS);
+ mBlankLine = new String(blank);
+ }
+
+ void erase() {
+ // erase all lines
+ for (int i = 0; i < mLines.length; i++) {
+ mLines[i] = null;
+ }
+ mRow = MAX_ROWS;
+ mCol = 1;
+ }
+
+ void der() {
+ if (mLines[mRow] != null) {
+ for (int i = 0; i < mCol; i++) {
+ if (mLines[mRow].charAt(i) != TS) {
+ for (int j = mCol; j < mLines[mRow].length(); j++) {
+ mLines[j].setCharAt(j, TS);
+ }
+ return;
+ }
+ }
+ mLines[mRow] = null;
+ }
+ }
+
+ void tab(int tabs) {
+ moveCursorByCol(tabs);
+ }
+
+ void bs() {
+ moveCursorByCol(-1);
+ if (mLines[mRow] != null) {
+ mLines[mRow].setCharAt(mCol, TS);
+ if (mCol == MAX_COLS - 1) {
+ // Spec recommendation:
+ // if cursor was at col 32, move cursor
+ // back to col 31 and erase both col 31&32
+ mLines[mRow].setCharAt(MAX_COLS, TS);
+ }
+ }
+ }
+
+ void cr() {
+ moveCursorTo(mRow + 1, 1);
+ }
+
+ void rollUp(int windowSize) {
+ int i;
+ for (i = 0; i <= mRow - windowSize; i++) {
+ mLines[i] = null;
+ }
+ int startRow = mRow - windowSize + 1;
+ if (startRow < 1) {
+ startRow = 1;
+ }
+ for (i = startRow; i < mRow; i++) {
+ mLines[i] = mLines[i + 1];
+ }
+ for (i = mRow; i < mLines.length; i++) {
+ // clear base row
+ mLines[i] = null;
+ }
+ // default to col 1, in case PAC is not sent
+ mCol = 1;
+ }
+
+ void writeText(String text) {
+ for (int i = 0; i < text.length(); i++) {
+ getLineBuffer(mRow).setCharAt(mCol, text.charAt(i));
+ moveCursorByCol(1);
+ }
+ }
+
+ void writeMidRowCode(StyleCode m) {
+ getLineBuffer(mRow).setMidRowAt(mCol, m);
+ moveCursorByCol(1);
+ }
+
+ void writePAC(PAC pac) {
+ if (pac.isIndentPAC()) {
+ moveCursorTo(pac.getRow(), pac.getCol());
+ } else {
+ moveCursorTo(pac.getRow(), 1);
+ }
+ getLineBuffer(mRow).setPACAt(mCol, pac);
+ }
+
+ SpannableStringBuilder[] getStyledText(CaptionStyle captionStyle) {
+ ArrayList<SpannableStringBuilder> rows = new ArrayList<>(MAX_ROWS);
+ for (int i = 1; i <= MAX_ROWS; i++) {
+ rows.add(mLines[i] != null ? mLines[i].getStyledText(captionStyle) : null);
+ }
+ return rows.toArray(new SpannableStringBuilder[MAX_ROWS]);
+ }
+
+ private static int clamp(int x, int min, int max) {
+ return x < min ? min : (x > max ? max : x);
+ }
+
+ private void moveCursorTo(int row, int col) {
+ mRow = clamp(row, 1, MAX_ROWS);
+ mCol = clamp(col, 1, MAX_COLS);
+ }
+
+ private void moveCursorToRow(int row) {
+ mRow = clamp(row, 1, MAX_ROWS);
+ }
+
+ private void moveCursorByCol(int col) {
+ mCol = clamp(mCol + col, 1, MAX_COLS);
+ }
+
+ private void moveBaselineTo(int baseRow, int windowSize) {
+ if (mRow == baseRow) {
+ return;
+ }
+ int actualWindowSize = windowSize;
+ if (baseRow < actualWindowSize) {
+ actualWindowSize = baseRow;
+ }
+ if (mRow < actualWindowSize) {
+ actualWindowSize = mRow;
+ }
+
+ int i;
+ if (baseRow < mRow) {
+ // copy from bottom to top row
+ for (i = actualWindowSize - 1; i >= 0; i--) {
+ mLines[baseRow - i] = mLines[mRow - i];
+ }
+ } else {
+ // copy from top to bottom row
+ for (i = 0; i < actualWindowSize; i++) {
+ mLines[baseRow - i] = mLines[mRow - i];
+ }
+ }
+ // clear rest of the rows
+ for (i = 0; i <= baseRow - windowSize; i++) {
+ mLines[i] = null;
+ }
+ for (i = baseRow + 1; i < mLines.length; i++) {
+ mLines[i] = null;
+ }
+ }
+
+ private CCLineBuilder getLineBuffer(int row) {
+ if (mLines[row] == null) {
+ mLines[row] = new CCLineBuilder(mBlankLine);
+ }
+ return mLines[row];
+ }
+ }
+
+ /*
+ * CCData parses the raw CC byte pair into displayable chars,
+ * misc control codes, Mid-Row or Preamble Address Codes.
+ */
+ private static class CCData {
+ private final byte mType;
+ private final byte mData1;
+ private final byte mData2;
+
+ private static final String[] sCtrlCodeMap = {
+ "RCL", "BS" , "AOF", "AON",
+ "DER", "RU2", "RU3", "RU4",
+ "FON", "RDC", "TR" , "RTD",
+ "EDM", "CR" , "ENM", "EOC",
+ };
+
+ private static final String[] sSpecialCharMap = {
+ "\u00AE",
+ "\u00B0",
+ "\u00BD",
+ "\u00BF",
+ "\u2122",
+ "\u00A2",
+ "\u00A3",
+ "\u266A", // Eighth note
+ "\u00E0",
+ "\u00A0", // Transparent space
+ "\u00E8",
+ "\u00E2",
+ "\u00EA",
+ "\u00EE",
+ "\u00F4",
+ "\u00FB",
+ };
+
+ private static final String[] sSpanishCharMap = {
+ // Spanish and misc chars
+ "\u00C1", // A
+ "\u00C9", // E
+ "\u00D3", // I
+ "\u00DA", // O
+ "\u00DC", // U
+ "\u00FC", // u
+ "\u2018", // opening single quote
+ "\u00A1", // inverted exclamation mark
+ "*",
+ "'",
+ "\u2014", // em dash
+ "\u00A9", // Copyright
+ "\u2120", // Servicemark
+ "\u2022", // round bullet
+ "\u201C", // opening double quote
+ "\u201D", // closing double quote
+ // French
+ "\u00C0",
+ "\u00C2",
+ "\u00C7",
+ "\u00C8",
+ "\u00CA",
+ "\u00CB",
+ "\u00EB",
+ "\u00CE",
+ "\u00CF",
+ "\u00EF",
+ "\u00D4",
+ "\u00D9",
+ "\u00F9",
+ "\u00DB",
+ "\u00AB",
+ "\u00BB"
+ };
+
+ private static final String[] sProtugueseCharMap = {
+ // Portuguese
+ "\u00C3",
+ "\u00E3",
+ "\u00CD",
+ "\u00CC",
+ "\u00EC",
+ "\u00D2",
+ "\u00F2",
+ "\u00D5",
+ "\u00F5",
+ "{",
+ "}",
+ "\\",
+ "^",
+ "_",
+ "|",
+ "~",
+ // German and misc chars
+ "\u00C4",
+ "\u00E4",
+ "\u00D6",
+ "\u00F6",
+ "\u00DF",
+ "\u00A5",
+ "\u00A4",
+ "\u2502", // vertical bar
+ "\u00C5",
+ "\u00E5",
+ "\u00D8",
+ "\u00F8",
+ "\u250C", // top-left corner
+ "\u2510", // top-right corner
+ "\u2514", // lower-left corner
+ "\u2518", // lower-right corner
+ };
+
+ static CCData[] fromByteArray(byte[] data) {
+ CCData[] ccData = new CCData[data.length / 3];
+
+ for (int i = 0; i < ccData.length; i++) {
+ ccData[i] = new CCData(
+ data[i * 3],
+ data[i * 3 + 1],
+ data[i * 3 + 2]);
+ }
+
+ return ccData;
+ }
+
+ CCData(byte type, byte data1, byte data2) {
+ mType = type;
+ mData1 = data1;
+ mData2 = data2;
+ }
+
+ int getCtrlCode() {
+ if ((mData1 == 0x14 || mData1 == 0x1c)
+ && mData2 >= 0x20 && mData2 <= 0x2f) {
+ return mData2;
+ }
+ return INVALID;
+ }
+
+ StyleCode getMidRow() {
+ // only support standard Mid-row codes, ignore
+ // optional background/foreground mid-row codes
+ if ((mData1 == 0x11 || mData1 == 0x19)
+ && mData2 >= 0x20 && mData2 <= 0x2f) {
+ return StyleCode.fromByte(mData2);
+ }
+ return null;
+ }
+
+ PAC getPAC() {
+ if ((mData1 & 0x70) == 0x10
+ && (mData2 & 0x40) == 0x40
+ && ((mData1 & 0x07) != 0 || (mData2 & 0x20) == 0)) {
+ return PAC.fromBytes(mData1, mData2);
+ }
+ return null;
+ }
+
+ int getTabOffset() {
+ if ((mData1 == 0x17 || mData1 == 0x1f)
+ && mData2 >= 0x21 && mData2 <= 0x23) {
+ return mData2 & 0x3;
+ }
+ return 0;
+ }
+
+ boolean isDisplayableChar() {
+ return isBasicChar() || isSpecialChar() || isExtendedChar();
+ }
+
+ String getDisplayText() {
+ String str = getBasicChars();
+
+ if (str == null) {
+ str = getSpecialChar();
+
+ if (str == null) {
+ str = getExtendedChar();
+ }
+ }
+
+ return str;
+ }
+
+ private String ctrlCodeToString(int ctrlCode) {
+ return sCtrlCodeMap[ctrlCode - 0x20];
+ }
+
+ private boolean isBasicChar() {
+ return mData1 >= 0x20 && mData1 <= 0x7f;
+ }
+
+ private boolean isSpecialChar() {
+ return ((mData1 == 0x11 || mData1 == 0x19)
+ && mData2 >= 0x30 && mData2 <= 0x3f);
+ }
+
+ private boolean isExtendedChar() {
+ return ((mData1 == 0x12 || mData1 == 0x1A
+ || mData1 == 0x13 || mData1 == 0x1B)
+ && mData2 >= 0x20 && mData2 <= 0x3f);
+ }
+
+ private char getBasicChar(byte data) {
+ char c;
+ // replace the non-ASCII ones
+ switch (data) {
+ case 0x2A: c = '\u00E1'; break;
+ case 0x5C: c = '\u00E9'; break;
+ case 0x5E: c = '\u00ED'; break;
+ case 0x5F: c = '\u00F3'; break;
+ case 0x60: c = '\u00FA'; break;
+ case 0x7B: c = '\u00E7'; break;
+ case 0x7C: c = '\u00F7'; break;
+ case 0x7D: c = '\u00D1'; break;
+ case 0x7E: c = '\u00F1'; break;
+ case 0x7F: c = '\u2588'; break; // Full block
+ default: c = (char) data; break;
+ }
+ return c;
+ }
+
+ private String getBasicChars() {
+ if (mData1 >= 0x20 && mData1 <= 0x7f) {
+ StringBuilder builder = new StringBuilder(2);
+ builder.append(getBasicChar(mData1));
+ if (mData2 >= 0x20 && mData2 <= 0x7f) {
+ builder.append(getBasicChar(mData2));
+ }
+ return builder.toString();
+ }
+
+ return null;
+ }
+
+ private String getSpecialChar() {
+ if ((mData1 == 0x11 || mData1 == 0x19)
+ && mData2 >= 0x30 && mData2 <= 0x3f) {
+ return sSpecialCharMap[mData2 - 0x30];
+ }
+
+ return null;
+ }
+
+ private String getExtendedChar() {
+ if ((mData1 == 0x12 || mData1 == 0x1A) && mData2 >= 0x20 && mData2 <= 0x3f) {
+ // 1 Spanish/French char
+ return sSpanishCharMap[mData2 - 0x20];
+ } else if ((mData1 == 0x13 || mData1 == 0x1B) && mData2 >= 0x20 && mData2 <= 0x3f) {
+ // 1 Portuguese/German/Danish char
+ return sProtugueseCharMap[mData2 - 0x20];
+ }
+
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ String str;
+
+ if (mData1 < 0x10 && mData2 < 0x10) {
+ // Null Pad, ignore
+ return String.format("[%d]Null: %02x %02x", mType, mData1, mData2);
+ }
+
+ int ctrlCode = getCtrlCode();
+ if (ctrlCode != INVALID) {
+ return String.format("[%d]%s", mType, ctrlCodeToString(ctrlCode));
+ }
+
+ int tabOffset = getTabOffset();
+ if (tabOffset > 0) {
+ return String.format("[%d]Tab%d", mType, tabOffset);
+ }
+
+ PAC pac = getPAC();
+ if (pac != null) {
+ return String.format("[%d]PAC: %s", mType, pac.toString());
+ }
+
+ StyleCode m = getMidRow();
+ if (m != null) {
+ return String.format("[%d]Mid-row: %s", mType, m.toString());
+ }
+
+ if (isDisplayableChar()) {
+ return String.format("[%d]Displayable: %s (%02x %02x)",
+ mType, getDisplayText(), mData1, mData2);
+ }
+
+ return String.format("[%d]Invalid: %02x %02x", mType, mData1, mData2);
+ }
+ }
+}
diff --git a/media/src/main/java/androidx/media/subtitle/ClosedCaptionRenderer.java b/media/src/main/java/androidx/media/subtitle/ClosedCaptionRenderer.java
new file mode 100644
index 0000000..90ff516
--- /dev/null
+++ b/media/src/main/java/androidx/media/subtitle/ClosedCaptionRenderer.java
@@ -0,0 +1,402 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.subtitle;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.media.MediaFormat;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.view.accessibility.CaptioningManager.CaptionStyle;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.media.R;
+
+import java.util.ArrayList;
+
+// Note: This is forked from android.media.ClosedCaptionRenderer since P
+/**
+ * @hide
+ */
+@RequiresApi(28)
+@RestrictTo(LIBRARY_GROUP)
+public class ClosedCaptionRenderer extends SubtitleController.Renderer {
+ private final Context mContext;
+ private Cea608CCWidget mCCWidget;
+
+ public ClosedCaptionRenderer(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public boolean supports(MediaFormat format) {
+ if (format.containsKey(MediaFormat.KEY_MIME)) {
+ String mimeType = format.getString(MediaFormat.KEY_MIME);
+ return MediaFormat.MIMETYPE_TEXT_CEA_608.equals(mimeType);
+ }
+ return false;
+ }
+
+ @Override
+ public SubtitleTrack createTrack(MediaFormat format) {
+ String mimeType = format.getString(MediaFormat.KEY_MIME);
+ if (MediaFormat.MIMETYPE_TEXT_CEA_608.equals(mimeType)) {
+ if (mCCWidget == null) {
+ mCCWidget = new Cea608CCWidget(mContext);
+ }
+ return new Cea608CaptionTrack(mCCWidget, format);
+ }
+ throw new RuntimeException("No matching format: " + format.toString());
+ }
+
+ static class Cea608CaptionTrack extends SubtitleTrack {
+ private final Cea608CCParser mCCParser;
+ private final Cea608CCWidget mRenderingWidget;
+
+ Cea608CaptionTrack(Cea608CCWidget renderingWidget, MediaFormat format) {
+ super(format);
+
+ mRenderingWidget = renderingWidget;
+ mCCParser = new Cea608CCParser(mRenderingWidget);
+ }
+
+ @Override
+ public void onData(byte[] data, boolean eos, long runID) {
+ mCCParser.parse(data);
+ }
+
+ @Override
+ public RenderingWidget getRenderingWidget() {
+ return mRenderingWidget;
+ }
+
+ @Override
+ public void updateView(ArrayList<Cue> activeCues) {
+ // Overriding with NO-OP, CC rendering by-passes this
+ }
+ }
+
+ /**
+ * Widget capable of rendering CEA-608 closed captions.
+ */
+ class Cea608CCWidget extends ClosedCaptionWidget implements Cea608CCParser.DisplayListener {
+ private static final String DUMMY_TEXT = "1234567890123456789012345678901234";
+ private final Rect mTextBounds = new Rect();
+
+ Cea608CCWidget(Context context) {
+ this(context, null);
+ }
+
+ Cea608CCWidget(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ Cea608CCWidget(Context context, AttributeSet attrs, int defStyle) {
+ this(context, attrs, defStyle, 0);
+ }
+
+ Cea608CCWidget(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public ClosedCaptionLayout createCaptionLayout(Context context) {
+ return new CCLayout(context);
+ }
+
+ @Override
+ public void onDisplayChanged(SpannableStringBuilder[] styledTexts) {
+ ((CCLayout) mClosedCaptionLayout).update(styledTexts);
+
+ if (mListener != null) {
+ mListener.onChanged(this);
+ }
+ }
+
+ @Override
+ public CaptionStyle getCaptionStyle() {
+ return mCaptionStyle;
+ }
+
+ private class CCLineBox extends TextView {
+ private static final float FONT_PADDING_RATIO = 0.75f;
+ private static final float EDGE_OUTLINE_RATIO = 0.1f;
+ private static final float EDGE_SHADOW_RATIO = 0.05f;
+ private float mOutlineWidth;
+ private float mShadowRadius;
+ private float mShadowOffset;
+
+ private int mTextColor = Color.WHITE;
+ private int mBgColor = Color.BLACK;
+ private int mEdgeType = CaptionStyle.EDGE_TYPE_NONE;
+ private int mEdgeColor = Color.TRANSPARENT;
+
+ CCLineBox(Context context) {
+ super(context);
+ setGravity(Gravity.CENTER);
+ setBackgroundColor(Color.TRANSPARENT);
+ setTextColor(Color.WHITE);
+ setTypeface(Typeface.MONOSPACE);
+ setVisibility(View.INVISIBLE);
+
+ final Resources res = getContext().getResources();
+
+ // get the default (will be updated later during measure)
+ mOutlineWidth = res.getDimensionPixelSize(
+ R.dimen.subtitle_outline_width);
+ mShadowRadius = res.getDimensionPixelSize(
+ R.dimen.subtitle_shadow_radius);
+ mShadowOffset = res.getDimensionPixelSize(
+ R.dimen.subtitle_shadow_offset);
+ }
+
+ void setCaptionStyle(CaptionStyle captionStyle) {
+ mTextColor = captionStyle.foregroundColor;
+ mBgColor = captionStyle.backgroundColor;
+ mEdgeType = captionStyle.edgeType;
+ mEdgeColor = captionStyle.edgeColor;
+
+ setTextColor(mTextColor);
+ if (mEdgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
+ setShadowLayer(mShadowRadius, mShadowOffset, mShadowOffset, mEdgeColor);
+ } else {
+ setShadowLayer(0, 0, 0, 0);
+ }
+ invalidate();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ float fontSize = MeasureSpec.getSize(heightMeasureSpec) * FONT_PADDING_RATIO;
+ setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize);
+
+ mOutlineWidth = EDGE_OUTLINE_RATIO * fontSize + 1.0f;
+ mShadowRadius = EDGE_SHADOW_RATIO * fontSize + 1.0f;
+ mShadowOffset = mShadowRadius;
+
+ // set font scale in the X direction to match the required width
+ setScaleX(1.0f);
+ getPaint().getTextBounds(DUMMY_TEXT, 0, DUMMY_TEXT.length(), mTextBounds);
+ float actualTextWidth = mTextBounds.width();
+ float requiredTextWidth = MeasureSpec.getSize(widthMeasureSpec);
+ setScaleX(requiredTextWidth / actualTextWidth);
+
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ protected void onDraw(Canvas c) {
+ if (mEdgeType == CaptionStyle.EDGE_TYPE_UNSPECIFIED
+ || mEdgeType == CaptionStyle.EDGE_TYPE_NONE
+ || mEdgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
+ // these edge styles don't require a second pass
+ super.onDraw(c);
+ return;
+ }
+
+ if (mEdgeType == CaptionStyle.EDGE_TYPE_OUTLINE) {
+ drawEdgeOutline(c);
+ } else {
+ // Raised or depressed
+ drawEdgeRaisedOrDepressed(c);
+ }
+ }
+
+ @SuppressWarnings("WrongCall")
+ private void drawEdgeOutline(Canvas c) {
+ TextPaint textPaint = getPaint();
+
+ Paint.Style previousStyle = textPaint.getStyle();
+ Paint.Join previousJoin = textPaint.getStrokeJoin();
+ float previousWidth = textPaint.getStrokeWidth();
+
+ setTextColor(mEdgeColor);
+ textPaint.setStyle(Paint.Style.FILL_AND_STROKE);
+ textPaint.setStrokeJoin(Paint.Join.ROUND);
+ textPaint.setStrokeWidth(mOutlineWidth);
+
+ // Draw outline and background only.
+ super.onDraw(c);
+
+ // Restore original settings.
+ setTextColor(mTextColor);
+ textPaint.setStyle(previousStyle);
+ textPaint.setStrokeJoin(previousJoin);
+ textPaint.setStrokeWidth(previousWidth);
+
+ // Remove the background.
+ setBackgroundSpans(Color.TRANSPARENT);
+ // Draw foreground only.
+ super.onDraw(c);
+ // Restore the background.
+ setBackgroundSpans(mBgColor);
+ }
+
+ @SuppressWarnings("WrongCall")
+ private void drawEdgeRaisedOrDepressed(Canvas c) {
+ TextPaint textPaint = getPaint();
+
+ Paint.Style previousStyle = textPaint.getStyle();
+ textPaint.setStyle(Paint.Style.FILL);
+
+ final boolean raised = mEdgeType == CaptionStyle.EDGE_TYPE_RAISED;
+ final int colorUp = raised ? Color.WHITE : mEdgeColor;
+ final int colorDown = raised ? mEdgeColor : Color.WHITE;
+ final float offset = mShadowRadius / 2f;
+
+ // Draw background and text with shadow up
+ setShadowLayer(mShadowRadius, -offset, -offset, colorUp);
+ super.onDraw(c);
+
+ // Remove the background.
+ setBackgroundSpans(Color.TRANSPARENT);
+
+ // Draw text with shadow down
+ setShadowLayer(mShadowRadius, +offset, +offset, colorDown);
+ super.onDraw(c);
+
+ // Restore settings
+ textPaint.setStyle(previousStyle);
+
+ // Restore the background.
+ setBackgroundSpans(mBgColor);
+ }
+
+ private void setBackgroundSpans(int color) {
+ CharSequence text = getText();
+ if (text instanceof Spannable) {
+ Spannable spannable = (Spannable) text;
+ Cea608CCParser.MutableBackgroundColorSpan[] bgSpans = spannable.getSpans(
+ 0, spannable.length(), Cea608CCParser.MutableBackgroundColorSpan.class);
+ for (int i = 0; i < bgSpans.length; i++) {
+ bgSpans[i].setBackgroundColor(color);
+ }
+ }
+ }
+ }
+
+ private class CCLayout extends LinearLayout implements ClosedCaptionLayout {
+ private static final int MAX_ROWS = Cea608CCParser.MAX_ROWS;
+ private static final float SAFE_AREA_RATIO = 0.9f;
+
+ private final CCLineBox[] mLineBoxes = new CCLineBox[MAX_ROWS];
+
+ CCLayout(Context context) {
+ super(context);
+ setGravity(Gravity.START);
+ setOrientation(LinearLayout.VERTICAL);
+ for (int i = 0; i < MAX_ROWS; i++) {
+ mLineBoxes[i] = new CCLineBox(getContext());
+ addView(mLineBoxes[i], LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ }
+ }
+
+ @Override
+ public void setCaptionStyle(CaptionStyle captionStyle) {
+ for (int i = 0; i < MAX_ROWS; i++) {
+ mLineBoxes[i].setCaptionStyle(captionStyle);
+ }
+ }
+
+ @Override
+ public void setFontScale(float fontScale) {
+ // Ignores the font scale changes of the system wide CC preference.
+ }
+
+ void update(SpannableStringBuilder[] textBuffer) {
+ for (int i = 0; i < MAX_ROWS; i++) {
+ if (textBuffer[i] != null) {
+ mLineBoxes[i].setText(textBuffer[i], TextView.BufferType.SPANNABLE);
+ mLineBoxes[i].setVisibility(View.VISIBLE);
+ } else {
+ mLineBoxes[i].setVisibility(View.INVISIBLE);
+ }
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ int safeWidth = getMeasuredWidth();
+ int safeHeight = getMeasuredHeight();
+
+ // CEA-608 assumes 4:3 video
+ if (safeWidth * 3 >= safeHeight * 4) {
+ safeWidth = safeHeight * 4 / 3;
+ } else {
+ safeHeight = safeWidth * 3 / 4;
+ }
+ safeWidth = (int) (safeWidth * SAFE_AREA_RATIO);
+ safeHeight = (int) (safeHeight * SAFE_AREA_RATIO);
+
+ int lineHeight = safeHeight / MAX_ROWS;
+ int lineHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+ lineHeight, MeasureSpec.EXACTLY);
+ int lineWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ safeWidth, MeasureSpec.EXACTLY);
+
+ for (int i = 0; i < MAX_ROWS; i++) {
+ mLineBoxes[i].measure(lineWidthMeasureSpec, lineHeightMeasureSpec);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ // safe caption area
+ int viewPortWidth = r - l;
+ int viewPortHeight = b - t;
+ int safeWidth, safeHeight;
+ // CEA-608 assumes 4:3 video
+ if (viewPortWidth * 3 >= viewPortHeight * 4) {
+ safeWidth = viewPortHeight * 4 / 3;
+ safeHeight = viewPortHeight;
+ } else {
+ safeWidth = viewPortWidth;
+ safeHeight = viewPortWidth * 3 / 4;
+ }
+ safeWidth = (int) (safeWidth * SAFE_AREA_RATIO);
+ safeHeight = (int) (safeHeight * SAFE_AREA_RATIO);
+ int left = (viewPortWidth - safeWidth) / 2;
+ int top = (viewPortHeight - safeHeight) / 2;
+
+ for (int i = 0; i < MAX_ROWS; i++) {
+ mLineBoxes[i].layout(
+ left,
+ top + safeHeight * i / MAX_ROWS,
+ left + safeWidth,
+ top + safeHeight * (i + 1) / MAX_ROWS);
+ }
+ }
+ }
+ }
+}
diff --git a/media/src/main/java/androidx/media/subtitle/ClosedCaptionWidget.java b/media/src/main/java/androidx/media/subtitle/ClosedCaptionWidget.java
new file mode 100644
index 0000000..a3d3e47
--- /dev/null
+++ b/media/src/main/java/androidx/media/subtitle/ClosedCaptionWidget.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.subtitle;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.CaptioningManager;
+import android.view.accessibility.CaptioningManager.CaptionStyle;
+import android.view.accessibility.CaptioningManager.CaptioningChangeListener;
+
+import androidx.annotation.RequiresApi;
+
+/**
+ * Abstract widget class to render a closed caption track.
+ */
+@RequiresApi(28)
+abstract class ClosedCaptionWidget extends ViewGroup implements SubtitleTrack.RenderingWidget {
+
+ interface ClosedCaptionLayout {
+ void setCaptionStyle(CaptionStyle captionStyle);
+ void setFontScale(float scale);
+ }
+
+ /** Captioning manager, used to obtain and track caption properties. */
+ private final CaptioningManager mManager;
+
+ /** Current caption style. */
+ protected CaptionStyle mCaptionStyle;
+
+ /** Callback for rendering changes. */
+ protected OnChangedListener mListener;
+
+ /** Concrete layout of CC. */
+ protected ClosedCaptionLayout mClosedCaptionLayout;
+
+ /** Whether a caption style change listener is registered. */
+ private boolean mHasChangeListener;
+
+ ClosedCaptionWidget(Context context) {
+ this(context, null);
+ }
+
+ ClosedCaptionWidget(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyle) {
+ this(context, attrs, defStyle, 0);
+ }
+
+ ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ // Cannot render text over video when layer type is hardware.
+ setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+
+ mManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
+ mCaptionStyle = mManager.getUserStyle();
+
+ mClosedCaptionLayout = createCaptionLayout(context);
+ mClosedCaptionLayout.setCaptionStyle(mCaptionStyle);
+ mClosedCaptionLayout.setFontScale(mManager.getFontScale());
+ addView((ViewGroup) mClosedCaptionLayout, LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT);
+
+ requestLayout();
+ }
+
+ public abstract ClosedCaptionLayout createCaptionLayout(Context context);
+
+ @Override
+ public void setOnChangedListener(OnChangedListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void setSize(int width, int height) {
+ final int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
+ final int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+
+ measure(widthSpec, heightSpec);
+ layout(0, 0, width, height);
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ if (visible) {
+ setVisibility(View.VISIBLE);
+ } else {
+ setVisibility(View.GONE);
+ }
+
+ manageChangeListener();
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ manageChangeListener();
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ manageChangeListener();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ ((ViewGroup) mClosedCaptionLayout).measure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ ((ViewGroup) mClosedCaptionLayout).layout(l, t, r, b);
+ }
+
+ /**
+ * Manages whether this renderer is listening for caption style changes.
+ */
+ private final CaptioningChangeListener mCaptioningListener = new CaptioningChangeListener() {
+ @Override
+ public void onUserStyleChanged(CaptionStyle userStyle) {
+ mCaptionStyle = userStyle;
+ mClosedCaptionLayout.setCaptionStyle(mCaptionStyle);
+ }
+
+ @Override
+ public void onFontScaleChanged(float fontScale) {
+ mClosedCaptionLayout.setFontScale(fontScale);
+ }
+ };
+
+ private void manageChangeListener() {
+ final boolean needsListener = isAttachedToWindow() && getVisibility() == View.VISIBLE;
+ if (mHasChangeListener != needsListener) {
+ mHasChangeListener = needsListener;
+
+ if (needsListener) {
+ mManager.addCaptioningChangeListener(mCaptioningListener);
+ } else {
+ mManager.removeCaptioningChangeListener(mCaptioningListener);
+ }
+ }
+ }
+}
+
diff --git a/media/src/main/java/androidx/media/subtitle/MediaTimeProvider.java b/media/src/main/java/androidx/media/subtitle/MediaTimeProvider.java
new file mode 100644
index 0000000..b6f0a14
--- /dev/null
+++ b/media/src/main/java/androidx/media/subtitle/MediaTimeProvider.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.subtitle;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import androidx.annotation.RestrictTo;
+
+// Note: This is just copied from android.media.MediaTimeProvider.
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public interface MediaTimeProvider {
+ // we do not allow negative media time
+ /**
+ * Presentation time value if no timed event notification is requested.
+ */
+ long NO_TIME = -1;
+
+ /**
+ * Cancels all previous notification request from this listener if any. It
+ * registers the listener to get seek and stop notifications. If timeUs is
+ * not negative, it also registers the listener for a timed event
+ * notification when the presentation time reaches (becomes greater) than
+ * the value specified. This happens immediately if the current media time
+ * is larger than or equal to timeUs.
+ *
+ * @param timeUs presentation time to get timed event callback at (or
+ * {@link #NO_TIME})
+ */
+ void notifyAt(long timeUs, OnMediaTimeListener listener);
+
+ /**
+ * Cancels all previous notification request from this listener if any. It
+ * registers the listener to get seek and stop notifications. If the media
+ * is stopped, the listener will immediately receive a stop notification.
+ * Otherwise, it will receive a timed event notificaton.
+ */
+ void scheduleUpdate(OnMediaTimeListener listener);
+
+ /**
+ * Cancels all previous notification request from this listener if any.
+ */
+ void cancelNotifications(OnMediaTimeListener listener);
+
+ /**
+ * Get the current presentation time.
+ *
+ * @param precise Whether getting a precise time is important. This is
+ * more costly.
+ * @param monotonic Whether returned time should be monotonic: that is,
+ * greater than or equal to the last returned time. Don't
+ * always set this to true. E.g. this has undesired
+ * consequences if the media is seeked between calls.
+ * @throws IllegalStateException if the media is not initialized
+ */
+ long getCurrentTimeUs(boolean precise, boolean monotonic)
+ throws IllegalStateException;
+
+ /**
+ * Mediatime listener
+ */
+ public interface OnMediaTimeListener {
+ /**
+ * Called when the registered time was reached naturally.
+ *
+ * @param timeUs current media time
+ */
+ void onTimedEvent(long timeUs);
+
+ /**
+ * Called when the media time changed due to seeking.
+ *
+ * @param timeUs current media time
+ */
+ void onSeek(long timeUs);
+
+ /**
+ * Called when the playback stopped. This is not called on pause, only
+ * on full stop, at which point there is no further current media time.
+ */
+ void onStop();
+ }
+}
+
diff --git a/media/src/main/java/androidx/media/subtitle/SubtitleController.java b/media/src/main/java/androidx/media/subtitle/SubtitleController.java
new file mode 100644
index 0000000..b6dfc2b
--- /dev/null
+++ b/media/src/main/java/androidx/media/subtitle/SubtitleController.java
@@ -0,0 +1,534 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.subtitle;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.media.MediaFormat;
+import android.media.MediaPlayer;
+import android.media.MediaPlayer.TrackInfo;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.view.accessibility.CaptioningManager;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.media.subtitle.SubtitleTrack.RenderingWidget;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+// Note: This is forked from android.media.SubtitleController since P
+/**
+ * The subtitle controller provides the architecture to display subtitles for a
+ * media source. It allows specifying which tracks to display, on which anchor
+ * to display them, and also allows adding external, out-of-band subtitle tracks.
+ *
+ * @hide
+ */
+@RequiresApi(28)
+@RestrictTo(LIBRARY_GROUP)
+public class SubtitleController {
+ private MediaTimeProvider mTimeProvider;
+ private ArrayList<Renderer> mRenderers;
+ private ArrayList<SubtitleTrack> mTracks;
+ private final Object mRenderersLock = new Object();
+ private final Object mTracksLock = new Object();
+ private SubtitleTrack mSelectedTrack;
+ private boolean mShowing;
+ private CaptioningManager mCaptioningManager;
+ private Handler mHandler;
+
+ private static final int WHAT_SHOW = 1;
+ private static final int WHAT_HIDE = 2;
+ private static final int WHAT_SELECT_TRACK = 3;
+ private static final int WHAT_SELECT_DEFAULT_TRACK = 4;
+
+ private final Handler.Callback mCallback = new Handler.Callback() {
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case WHAT_SHOW:
+ doShow();
+ return true;
+ case WHAT_HIDE:
+ doHide();
+ return true;
+ case WHAT_SELECT_TRACK:
+ doSelectTrack((SubtitleTrack) msg.obj);
+ return true;
+ case WHAT_SELECT_DEFAULT_TRACK:
+ doSelectDefaultTrack();
+ return true;
+ default:
+ return false;
+ }
+ }
+ };
+
+ private CaptioningManager.CaptioningChangeListener mCaptioningChangeListener =
+ new CaptioningManager.CaptioningChangeListener() {
+ @Override
+ public void onEnabledChanged(boolean enabled) {
+ selectDefaultTrack();
+ }
+
+ @Override
+ public void onLocaleChanged(Locale locale) {
+ selectDefaultTrack();
+ }
+ };
+
+ public SubtitleController(Context context) {
+ this(context, null, null);
+ }
+
+ /**
+ * Creates a subtitle controller for a media playback object that implements
+ * the MediaTimeProvider interface.
+ *
+ * @param timeProvider
+ */
+ public SubtitleController(
+ Context context,
+ MediaTimeProvider timeProvider,
+ Listener listener) {
+ mTimeProvider = timeProvider;
+ mListener = listener;
+
+ mRenderers = new ArrayList<Renderer>();
+ mShowing = false;
+ mTracks = new ArrayList<SubtitleTrack>();
+ mCaptioningManager =
+ (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ mCaptioningManager.removeCaptioningChangeListener(
+ mCaptioningChangeListener);
+ super.finalize();
+ }
+
+ /**
+ * @return the available subtitle tracks for this media. These include
+ * the tracks found by {@link MediaPlayer} as well as any tracks added
+ * manually via {@link #addTrack}.
+ */
+ public SubtitleTrack[] getTracks() {
+ synchronized (mTracksLock) {
+ SubtitleTrack[] tracks = new SubtitleTrack[mTracks.size()];
+ mTracks.toArray(tracks);
+ return tracks;
+ }
+ }
+
+ /**
+ * @return the currently selected subtitle track
+ */
+ public SubtitleTrack getSelectedTrack() {
+ return mSelectedTrack;
+ }
+
+ private RenderingWidget getRenderingWidget() {
+ if (mSelectedTrack == null) {
+ return null;
+ }
+ return mSelectedTrack.getRenderingWidget();
+ }
+
+ /**
+ * Selects a subtitle track. As a result, this track will receive
+ * in-band data from the {@link MediaPlayer}. However, this does
+ * not change the subtitle visibility.
+ *
+ * Should be called from the anchor's (UI) thread. {@see #Anchor.getSubtitleLooper}
+ *
+ * @param track The subtitle track to select. This must be one of the
+ * tracks in {@link #getTracks}.
+ * @return true if the track was successfully selected.
+ */
+ public boolean selectTrack(SubtitleTrack track) {
+ if (track != null && !mTracks.contains(track)) {
+ return false;
+ }
+
+ processOnAnchor(mHandler.obtainMessage(WHAT_SELECT_TRACK, track));
+ return true;
+ }
+
+ private void doSelectTrack(SubtitleTrack track) {
+ mTrackIsExplicit = true;
+ if (mSelectedTrack == track) {
+ return;
+ }
+
+ if (mSelectedTrack != null) {
+ mSelectedTrack.hide();
+ mSelectedTrack.setTimeProvider(null);
+ }
+
+ mSelectedTrack = track;
+ if (mAnchor != null) {
+ mAnchor.setSubtitleWidget(getRenderingWidget());
+ }
+
+ if (mSelectedTrack != null) {
+ mSelectedTrack.setTimeProvider(mTimeProvider);
+ mSelectedTrack.show();
+ }
+
+ if (mListener != null) {
+ mListener.onSubtitleTrackSelected(track);
+ }
+ }
+
+ /**
+ * @return the default subtitle track based on system preferences, or null,
+ * if no such track exists in this manager.
+ *
+ * Supports HLS-flags: AUTOSELECT, FORCED & DEFAULT.
+ *
+ * 1. If captioning is disabled, only consider FORCED tracks. Otherwise,
+ * consider all tracks, but prefer non-FORCED ones.
+ * 2. If user selected "Default" caption language:
+ * a. If there is a considered track with DEFAULT=yes, returns that track
+ * (favor the first one in the current language if there are more than
+ * one default tracks, or the first in general if none of them are in
+ * the current language).
+ * b. Otherwise, if there is a track with AUTOSELECT=yes in the current
+ * language, return that one.
+ * c. If there are no default tracks, and no autoselectable tracks in the
+ * current language, return null.
+ * 3. If there is a track with the caption language, select that one. Prefer
+ * the one with AUTOSELECT=no.
+ *
+ * The default values for these flags are DEFAULT=no, AUTOSELECT=yes
+ * and FORCED=no.
+ */
+ public SubtitleTrack getDefaultTrack() {
+ SubtitleTrack bestTrack = null;
+ int bestScore = -1;
+
+ Locale selectedLocale = mCaptioningManager.getLocale();
+ Locale locale = selectedLocale;
+ if (locale == null) {
+ locale = Locale.getDefault();
+ }
+ boolean selectForced = !mCaptioningManager.isEnabled();
+
+ synchronized (mTracksLock) {
+ for (SubtitleTrack track: mTracks) {
+ MediaFormat format = track.getFormat();
+ String language = format.getString(MediaFormat.KEY_LANGUAGE);
+ boolean forced = MediaFormatUtil
+ .getInteger(format, MediaFormat.KEY_IS_FORCED_SUBTITLE, 0) != 0;
+ boolean autoselect = MediaFormatUtil
+ .getInteger(format, MediaFormat.KEY_IS_AUTOSELECT, 1) != 0;
+ boolean is_default = MediaFormatUtil
+ .getInteger(format, MediaFormat.KEY_IS_DEFAULT, 0) != 0;
+
+ boolean languageMatches = locale == null
+ || locale.getLanguage().equals("")
+ || locale.getISO3Language().equals(language)
+ || locale.getLanguage().equals(language);
+ // is_default is meaningless unless caption language is 'default'
+ int score = (forced ? 0 : 8)
+ + (((selectedLocale == null) && is_default) ? 4 : 0)
+ + (autoselect ? 0 : 2) + (languageMatches ? 1 : 0);
+
+ if (selectForced && !forced) {
+ continue;
+ }
+
+ // we treat null locale/language as matching any language
+ if ((selectedLocale == null && is_default)
+ || (languageMatches && (autoselect || forced || selectedLocale != null))) {
+ if (score > bestScore) {
+ bestScore = score;
+ bestTrack = track;
+ }
+ }
+ }
+ }
+ return bestTrack;
+ }
+
+ static class MediaFormatUtil {
+ MediaFormatUtil() { }
+ static int getInteger(MediaFormat format, String name, int defaultValue) {
+ try {
+ return format.getInteger(name);
+ } catch (NullPointerException | ClassCastException e) {
+ /* no such field or field of different type */
+ }
+ return defaultValue;
+ }
+ }
+
+ private boolean mTrackIsExplicit = false;
+ private boolean mVisibilityIsExplicit = false;
+
+ /** should be called from anchor thread */
+ public void selectDefaultTrack() {
+ processOnAnchor(mHandler.obtainMessage(WHAT_SELECT_DEFAULT_TRACK));
+ }
+
+ private void doSelectDefaultTrack() {
+ if (mTrackIsExplicit) {
+ if (mVisibilityIsExplicit) {
+ return;
+ }
+ // If track selection is explicit, but visibility
+ // is not, it falls back to the captioning setting
+ if (mCaptioningManager.isEnabled()
+ || (mSelectedTrack != null && MediaFormatUtil.getInteger(
+ mSelectedTrack.getFormat(),
+ MediaFormat.KEY_IS_FORCED_SUBTITLE, 0) != 0)) {
+ show();
+ } else if (mSelectedTrack != null
+ && mSelectedTrack.getTrackType() == TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) {
+ hide();
+ }
+ mVisibilityIsExplicit = false;
+ }
+
+ // We can have a default (forced) track even if captioning
+ // is not enabled. This is handled by getDefaultTrack().
+ // Show this track unless subtitles were explicitly hidden.
+ SubtitleTrack track = getDefaultTrack();
+ if (track != null) {
+ selectTrack(track);
+ mTrackIsExplicit = false;
+ if (!mVisibilityIsExplicit) {
+ show();
+ mVisibilityIsExplicit = false;
+ }
+ }
+ }
+
+ /** must be called from anchor thread */
+ public void reset() {
+ checkAnchorLooper();
+ hide();
+ selectTrack(null);
+ mTracks.clear();
+ mTrackIsExplicit = false;
+ mVisibilityIsExplicit = false;
+ mCaptioningManager.removeCaptioningChangeListener(
+ mCaptioningChangeListener);
+ }
+
+ /**
+ * Adds a new, external subtitle track to the manager.
+ *
+ * @param format the format of the track that will include at least
+ * the MIME type {@link MediaFormat@KEY_MIME}.
+ * @return the created {@link SubtitleTrack} object
+ */
+ public SubtitleTrack addTrack(MediaFormat format) {
+ synchronized (mRenderersLock) {
+ for (Renderer renderer: mRenderers) {
+ if (renderer.supports(format)) {
+ SubtitleTrack track = renderer.createTrack(format);
+ if (track != null) {
+ synchronized (mTracksLock) {
+ if (mTracks.size() == 0) {
+ mCaptioningManager.addCaptioningChangeListener(
+ mCaptioningChangeListener);
+ }
+ mTracks.add(track);
+ }
+ return track;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Show the selected (or default) subtitle track.
+ *
+ * Should be called from the anchor's (UI) thread. {@see #Anchor.getSubtitleLooper}
+ */
+ public void show() {
+ processOnAnchor(mHandler.obtainMessage(WHAT_SHOW));
+ }
+
+ private void doShow() {
+ mShowing = true;
+ mVisibilityIsExplicit = true;
+ if (mSelectedTrack != null) {
+ mSelectedTrack.show();
+ }
+ }
+
+ /**
+ * Hide the selected (or default) subtitle track.
+ *
+ * Should be called from the anchor's (UI) thread. {@see #Anchor.getSubtitleLooper}
+ */
+ public void hide() {
+ processOnAnchor(mHandler.obtainMessage(WHAT_HIDE));
+ }
+
+ private void doHide() {
+ mVisibilityIsExplicit = true;
+ if (mSelectedTrack != null) {
+ mSelectedTrack.hide();
+ }
+ mShowing = false;
+ }
+
+ /**
+ * Interface for supporting a single or multiple subtitle types in {@link MediaPlayer}.
+ */
+ public abstract static class Renderer {
+ /**
+ * Called by {@link MediaPlayer}'s {@link SubtitleController} when a new
+ * subtitle track is detected, to see if it should use this object to
+ * parse and display this subtitle track.
+ *
+ * @param format the format of the track that will include at least
+ * the MIME type {@link MediaFormat@KEY_MIME}.
+ *
+ * @return true if and only if the track format is supported by this
+ * renderer
+ */
+ public abstract boolean supports(MediaFormat format);
+
+ /**
+ * Called by {@link MediaPlayer}'s {@link SubtitleController} for each
+ * subtitle track that was detected and is supported by this object to
+ * create a {@link SubtitleTrack} object. This object will be created
+ * for each track that was found. If the track is selected for display,
+ * this object will be used to parse and display the track data.
+ *
+ * @param format the format of the track that will include at least
+ * the MIME type {@link MediaFormat@KEY_MIME}.
+ * @return a {@link SubtitleTrack} object that will be used to parse
+ * and render the subtitle track.
+ */
+ public abstract SubtitleTrack createTrack(MediaFormat format);
+ }
+
+ /**
+ * Add support for a subtitle format in {@link MediaPlayer}.
+ *
+ * @param renderer a {@link SubtitleController.Renderer} object that adds
+ * support for a subtitle format.
+ */
+ public void registerRenderer(Renderer renderer) {
+ synchronized (mRenderersLock) {
+ // TODO how to get available renderers in the system
+ if (!mRenderers.contains(renderer)) {
+ // TODO should added renderers override existing ones (to allow replacing?)
+ mRenderers.add(renderer);
+ }
+ }
+ }
+
+ /**
+ * Returns true if one of the registered renders supports given media format.
+ *
+ * @param format a {@link MediaFormat} object
+ * @return true if this SubtitleController has a renderer that supports
+ * the media format.
+ */
+ public boolean hasRendererFor(MediaFormat format) {
+ synchronized (mRenderersLock) {
+ // TODO how to get available renderers in the system
+ for (Renderer renderer: mRenderers) {
+ if (renderer.supports(format)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Subtitle anchor, an object that is able to display a subtitle renderer,
+ * e.g. a VideoView.
+ */
+ public interface Anchor {
+ /**
+ * Anchor should use the supplied subtitle rendering widget, or
+ * none if it is null.
+ */
+ void setSubtitleWidget(RenderingWidget subtitleWidget);
+
+ /**
+ * Anchors provide the looper on which all track visibility changes
+ * (track.show/hide, setSubtitleWidget) will take place.
+ */
+ Looper getSubtitleLooper();
+ }
+
+ private Anchor mAnchor;
+
+ /**
+ * called from anchor's looper (if any, both when unsetting and
+ * setting)
+ */
+ public void setAnchor(Anchor anchor) {
+ if (mAnchor == anchor) {
+ return;
+ }
+
+ if (mAnchor != null) {
+ checkAnchorLooper();
+ mAnchor.setSubtitleWidget(null);
+ }
+ mAnchor = anchor;
+ mHandler = null;
+ if (mAnchor != null) {
+ mHandler = new Handler(mAnchor.getSubtitleLooper(), mCallback);
+ checkAnchorLooper();
+ mAnchor.setSubtitleWidget(getRenderingWidget());
+ }
+ }
+
+ private void checkAnchorLooper() {
+ assert mHandler != null : "Should have a looper already";
+ assert Looper.myLooper() == mHandler.getLooper()
+ : "Must be called from the anchor's looper";
+ }
+
+ private void processOnAnchor(Message m) {
+ assert mHandler != null : "Should have a looper already";
+ if (Looper.myLooper() == mHandler.getLooper()) {
+ mHandler.dispatchMessage(m);
+ } else {
+ mHandler.sendMessage(m);
+ }
+ }
+
+ interface Listener {
+ /**
+ * Called when a subtitle track has been selected.
+ *
+ * @param track selected subtitle track or null
+ */
+ void onSubtitleTrackSelected(SubtitleTrack track);
+ }
+
+ private Listener mListener;
+}
diff --git a/media/src/main/java/androidx/media/subtitle/SubtitleTrack.java b/media/src/main/java/androidx/media/subtitle/SubtitleTrack.java
new file mode 100644
index 0000000..30c1316
--- /dev/null
+++ b/media/src/main/java/androidx/media/subtitle/SubtitleTrack.java
@@ -0,0 +1,715 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.subtitle;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.graphics.Canvas;
+import android.media.MediaFormat;
+import android.media.MediaPlayer.TrackInfo;
+import android.media.SubtitleData;
+import android.os.Handler;
+import android.util.Log;
+import android.util.LongSparseArray;
+import android.util.Pair;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+// Note: This is forked from android.media.SubtitleTrack since P
+/**
+ * A subtitle track abstract base class that is responsible for parsing and displaying
+ * an instance of a particular type of subtitle.
+ *
+ * @hide
+ */
+@RequiresApi(28)
+@RestrictTo(LIBRARY_GROUP)
+public abstract class SubtitleTrack implements MediaTimeProvider.OnMediaTimeListener {
+ private static final String TAG = "SubtitleTrack";
+ private long mLastUpdateTimeMs;
+ private long mLastTimeMs;
+
+ private Runnable mRunnable;
+
+ private final LongSparseArray<Run> mRunsByEndTime = new LongSparseArray<Run>();
+ private final LongSparseArray<Run> mRunsByID = new LongSparseArray<Run>();
+
+ private CueList mCues;
+ private final ArrayList<Cue> mActiveCues = new ArrayList<Cue>();
+ protected boolean mVisible;
+
+ public boolean DEBUG = false;
+
+ protected Handler mHandler = new Handler();
+
+ private MediaFormat mFormat;
+
+ public SubtitleTrack(MediaFormat format) {
+ mFormat = format;
+ mCues = new CueList();
+ clearActiveCues();
+ mLastTimeMs = -1;
+ }
+
+ public final MediaFormat getFormat() {
+ return mFormat;
+ }
+
+ private long mNextScheduledTimeMs = -1;
+
+ /**
+ * Called when there is input data for the subtitle track.
+ */
+ public void onData(SubtitleData data) {
+ long runID = data.getStartTimeUs() + 1;
+ onData(data.getData(), true /* eos */, runID);
+ setRunDiscardTimeMs(
+ runID,
+ (data.getStartTimeUs() + data.getDurationUs()) / 1000);
+ }
+
+ /**
+ * Called when there is input data for the subtitle track. The
+ * complete subtitle for a track can include multiple whole units
+ * (runs). Each of these units can have multiple sections. The
+ * contents of a run are submitted in sequential order, with eos
+ * indicating the last section of the run. Calls from different
+ * runs must not be intermixed.
+ *
+ * @param data subtitle data byte buffer
+ * @param eos true if this is the last section of the run.
+ * @param runID mostly-unique ID for this run of data. Subtitle cues
+ * with runID of 0 are discarded immediately after
+ * display. Cues with runID of ~0 are discarded
+ * only at the deletion of the track object. Cues
+ * with other runID-s are discarded at the end of the
+ * run, which defaults to the latest timestamp of
+ * any of its cues (with this runID).
+ */
+ protected abstract void onData(byte[] data, boolean eos, long runID);
+
+ /**
+ * Called when adding the subtitle rendering widget to the view hierarchy,
+ * as well as when showing or hiding the subtitle track, or when the video
+ * surface position has changed.
+ *
+ * @return the widget that renders this subtitle track. For most renderers
+ * there should be a single shared instance that is used for all
+ * tracks supported by that renderer, as at most one subtitle track
+ * is visible at one time.
+ */
+ public abstract RenderingWidget getRenderingWidget();
+
+ /**
+ * Called when the active cues have changed, and the contents of the subtitle
+ * view should be updated.
+ */
+ public abstract void updateView(ArrayList<Cue> activeCues);
+
+ protected synchronized void updateActiveCues(boolean rebuild, long timeMs) {
+ // out-of-order times mean seeking or new active cues being added
+ // (during their own timespan)
+ if (rebuild || mLastUpdateTimeMs > timeMs) {
+ clearActiveCues();
+ }
+
+ for (Iterator<Pair<Long, Cue>> it =
+ mCues.entriesBetween(mLastUpdateTimeMs, timeMs).iterator(); it.hasNext(); ) {
+ Pair<Long, Cue> event = it.next();
+ Cue cue = event.second;
+
+ if (cue.mEndTimeMs == event.first) {
+ // remove past cues
+ if (DEBUG) Log.v(TAG, "Removing " + cue);
+ mActiveCues.remove(cue);
+ if (cue.mRunID == 0) {
+ it.remove();
+ }
+ } else if (cue.mStartTimeMs == event.first) {
+ // add new cues
+ // TRICKY: this will happen in start order
+ if (DEBUG) Log.v(TAG, "Adding " + cue);
+ if (cue.mInnerTimesMs != null) {
+ cue.onTime(timeMs);
+ }
+ mActiveCues.add(cue);
+ } else if (cue.mInnerTimesMs != null) {
+ // cue is modified
+ cue.onTime(timeMs);
+ }
+ }
+
+ /* complete any runs */
+ while (mRunsByEndTime.size() > 0 && mRunsByEndTime.keyAt(0) <= timeMs) {
+ removeRunsByEndTimeIndex(0); // removes element
+ }
+ mLastUpdateTimeMs = timeMs;
+ }
+
+ private void removeRunsByEndTimeIndex(int ix) {
+ Run run = mRunsByEndTime.valueAt(ix);
+ while (run != null) {
+ Cue cue = run.mFirstCue;
+ while (cue != null) {
+ mCues.remove(cue);
+ Cue nextCue = cue.mNextInRun;
+ cue.mNextInRun = null;
+ cue = nextCue;
+ }
+ mRunsByID.remove(run.mRunID);
+ Run nextRun = run.mNextRunAtEndTimeMs;
+ run.mPrevRunAtEndTimeMs = null;
+ run.mNextRunAtEndTimeMs = null;
+ run = nextRun;
+ }
+ mRunsByEndTime.removeAt(ix);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ /* remove all cues (untangle all cross-links) */
+ int size = mRunsByEndTime.size();
+ for (int ix = size - 1; ix >= 0; ix--) {
+ removeRunsByEndTimeIndex(ix);
+ }
+
+ super.finalize();
+ }
+
+ private synchronized void takeTime(long timeMs) {
+ mLastTimeMs = timeMs;
+ }
+
+ protected synchronized void clearActiveCues() {
+ if (DEBUG) Log.v(TAG, "Clearing " + mActiveCues.size() + " active cues");
+ mActiveCues.clear();
+ mLastUpdateTimeMs = -1;
+ }
+
+ protected void scheduleTimedEvents() {
+ /* get times for the next event */
+ if (mTimeProvider != null) {
+ mNextScheduledTimeMs = mCues.nextTimeAfter(mLastTimeMs);
+ if (DEBUG) Log.d(TAG, "sched @" + mNextScheduledTimeMs + " after " + mLastTimeMs);
+ mTimeProvider.notifyAt(mNextScheduledTimeMs >= 0
+ ? (mNextScheduledTimeMs * 1000) : MediaTimeProvider.NO_TIME, this);
+ }
+ }
+
+ @Override
+ public void onTimedEvent(long timeUs) {
+ if (DEBUG) Log.d(TAG, "onTimedEvent " + timeUs);
+ synchronized (this) {
+ long timeMs = timeUs / 1000;
+ updateActiveCues(false, timeMs);
+ takeTime(timeMs);
+ }
+ updateView(mActiveCues);
+ scheduleTimedEvents();
+ }
+
+ @Override
+ public void onSeek(long timeUs) {
+ if (DEBUG) Log.d(TAG, "onSeek " + timeUs);
+ synchronized (this) {
+ long timeMs = timeUs / 1000;
+ updateActiveCues(true, timeMs);
+ takeTime(timeMs);
+ }
+ updateView(mActiveCues);
+ scheduleTimedEvents();
+ }
+
+ @Override
+ public void onStop() {
+ synchronized (this) {
+ if (DEBUG) Log.d(TAG, "onStop");
+ clearActiveCues();
+ mLastTimeMs = -1;
+ }
+ updateView(mActiveCues);
+ mNextScheduledTimeMs = -1;
+ mTimeProvider.notifyAt(MediaTimeProvider.NO_TIME, this);
+ }
+
+ protected MediaTimeProvider mTimeProvider;
+
+ /**
+ * Shows subtitle rendering widget
+ */
+ public void show() {
+ if (mVisible) {
+ return;
+ }
+
+ mVisible = true;
+ RenderingWidget renderingWidget = getRenderingWidget();
+ if (renderingWidget != null) {
+ renderingWidget.setVisible(true);
+ }
+ if (mTimeProvider != null) {
+ mTimeProvider.scheduleUpdate(this);
+ }
+ }
+
+ /**
+ * Hides subtitle rendering widget
+ */
+ public void hide() {
+ if (!mVisible) {
+ return;
+ }
+
+ if (mTimeProvider != null) {
+ mTimeProvider.cancelNotifications(this);
+ }
+ RenderingWidget renderingWidget = getRenderingWidget();
+ if (renderingWidget != null) {
+ renderingWidget.setVisible(false);
+ }
+ mVisible = false;
+ }
+
+ protected synchronized boolean addCue(Cue cue) {
+ mCues.add(cue);
+
+ if (cue.mRunID != 0) {
+ Run run = mRunsByID.get(cue.mRunID);
+ if (run == null) {
+ run = new Run();
+ mRunsByID.put(cue.mRunID, run);
+ run.mEndTimeMs = cue.mEndTimeMs;
+ } else if (run.mEndTimeMs < cue.mEndTimeMs) {
+ run.mEndTimeMs = cue.mEndTimeMs;
+ }
+
+ // link-up cues in the same run
+ cue.mNextInRun = run.mFirstCue;
+ run.mFirstCue = cue;
+ }
+
+ // if a cue is added that should be visible, need to refresh view
+ long nowMs = -1;
+ if (mTimeProvider != null) {
+ try {
+ nowMs = mTimeProvider.getCurrentTimeUs(
+ false /* precise */, true /* monotonic */) / 1000;
+ } catch (IllegalStateException e) {
+ // handle as it we are not playing
+ }
+ }
+
+ if (DEBUG) {
+ Log.v(TAG, "mVisible=" + mVisible + ", "
+ + cue.mStartTimeMs + " <= " + nowMs + ", "
+ + cue.mEndTimeMs + " >= " + mLastTimeMs);
+ }
+
+ if (mVisible && cue.mStartTimeMs <= nowMs
+ // we don't trust nowMs, so check any cue since last callback
+ && cue.mEndTimeMs >= mLastTimeMs) {
+ if (mRunnable != null) {
+ mHandler.removeCallbacks(mRunnable);
+ }
+ final SubtitleTrack track = this;
+ final long thenMs = nowMs;
+ mRunnable = new Runnable() {
+ @Override
+ public void run() {
+ // even with synchronized, it is possible that we are going
+ // to do multiple updates as the runnable could be already
+ // running.
+ synchronized (track) {
+ mRunnable = null;
+ updateActiveCues(true, thenMs);
+ updateView(mActiveCues);
+ }
+ }
+ };
+ // delay update so we don't update view on every cue. TODO why 10?
+ if (mHandler.postDelayed(mRunnable, 10 /* delay */)) {
+ if (DEBUG) Log.v(TAG, "scheduling update");
+ } else {
+ if (DEBUG) Log.w(TAG, "failed to schedule subtitle view update");
+ }
+ return true;
+ }
+
+ if (mVisible && cue.mEndTimeMs >= mLastTimeMs
+ && (cue.mStartTimeMs < mNextScheduledTimeMs || mNextScheduledTimeMs < 0)) {
+ scheduleTimedEvents();
+ }
+
+ return false;
+ }
+
+ /**
+ * Sets MediaTimeProvider
+ */
+ public synchronized void setTimeProvider(MediaTimeProvider timeProvider) {
+ if (mTimeProvider == timeProvider) {
+ return;
+ }
+ if (mTimeProvider != null) {
+ mTimeProvider.cancelNotifications(this);
+ }
+ mTimeProvider = timeProvider;
+ if (mTimeProvider != null) {
+ mTimeProvider.scheduleUpdate(this);
+ }
+ }
+
+
+ static class CueList {
+ private static final String TAG = "CueList";
+ // simplistic, inefficient implementation
+ private SortedMap<Long, ArrayList<Cue>> mCues;
+ public boolean DEBUG = false;
+
+ private boolean addEvent(Cue cue, long timeMs) {
+ ArrayList<Cue> cues = mCues.get(timeMs);
+ if (cues == null) {
+ cues = new ArrayList<Cue>(2);
+ mCues.put(timeMs, cues);
+ } else if (cues.contains(cue)) {
+ // do not duplicate cues
+ return false;
+ }
+
+ cues.add(cue);
+ return true;
+ }
+
+ private void removeEvent(Cue cue, long timeMs) {
+ ArrayList<Cue> cues = mCues.get(timeMs);
+ if (cues != null) {
+ cues.remove(cue);
+ if (cues.size() == 0) {
+ mCues.remove(timeMs);
+ }
+ }
+ }
+
+ public void add(Cue cue) {
+ // ignore non-positive-duration cues
+ if (cue.mStartTimeMs >= cue.mEndTimeMs) return;
+
+ if (!addEvent(cue, cue.mStartTimeMs)) {
+ return;
+ }
+
+ long lastTimeMs = cue.mStartTimeMs;
+ if (cue.mInnerTimesMs != null) {
+ for (long timeMs: cue.mInnerTimesMs) {
+ if (timeMs > lastTimeMs && timeMs < cue.mEndTimeMs) {
+ addEvent(cue, timeMs);
+ lastTimeMs = timeMs;
+ }
+ }
+ }
+
+ addEvent(cue, cue.mEndTimeMs);
+ }
+
+ public void remove(Cue cue) {
+ removeEvent(cue, cue.mStartTimeMs);
+ if (cue.mInnerTimesMs != null) {
+ for (long timeMs: cue.mInnerTimesMs) {
+ removeEvent(cue, timeMs);
+ }
+ }
+ removeEvent(cue, cue.mEndTimeMs);
+ }
+
+ public Iterable<Pair<Long, Cue>> entriesBetween(
+ final long lastTimeMs, final long timeMs) {
+ return new Iterable<Pair<Long, Cue>>() {
+ @Override
+ public Iterator<Pair<Long, Cue>> iterator() {
+ if (DEBUG) Log.d(TAG, "slice (" + lastTimeMs + ", " + timeMs + "]=");
+ try {
+ return new EntryIterator(
+ mCues.subMap(lastTimeMs + 1, timeMs + 1));
+ } catch (IllegalArgumentException e) {
+ return new EntryIterator(null);
+ }
+ }
+ };
+ }
+
+ public long nextTimeAfter(long timeMs) {
+ SortedMap<Long, ArrayList<Cue>> tail = null;
+ try {
+ tail = mCues.tailMap(timeMs + 1);
+ if (tail != null) {
+ return tail.firstKey();
+ } else {
+ return -1;
+ }
+ } catch (IllegalArgumentException e) {
+ return -1;
+ } catch (NoSuchElementException e) {
+ return -1;
+ }
+ }
+
+ class EntryIterator implements Iterator<Pair<Long, Cue>> {
+ @Override
+ public boolean hasNext() {
+ return !mDone;
+ }
+
+ @Override
+ public Pair<Long, Cue> next() {
+ if (mDone) {
+ throw new NoSuchElementException("");
+ }
+ mLastEntry = new Pair<Long, Cue>(
+ mCurrentTimeMs, mListIterator.next());
+ mLastListIterator = mListIterator;
+ if (!mListIterator.hasNext()) {
+ nextKey();
+ }
+ return mLastEntry;
+ }
+
+ @Override
+ public void remove() {
+ // only allow removing end tags
+ if (mLastListIterator == null
+ || mLastEntry.second.mEndTimeMs != mLastEntry.first) {
+ throw new IllegalStateException("");
+ }
+
+ // remove end-cue
+ mLastListIterator.remove();
+ mLastListIterator = null;
+ if (mCues.get(mLastEntry.first).size() == 0) {
+ mCues.remove(mLastEntry.first);
+ }
+
+ // remove rest of the cues
+ Cue cue = mLastEntry.second;
+ removeEvent(cue, cue.mStartTimeMs);
+ if (cue.mInnerTimesMs != null) {
+ for (long timeMs: cue.mInnerTimesMs) {
+ removeEvent(cue, timeMs);
+ }
+ }
+ }
+
+ EntryIterator(SortedMap<Long, ArrayList<Cue>> cues) {
+ if (DEBUG) Log.v(TAG, cues + "");
+ mRemainingCues = cues;
+ mLastListIterator = null;
+ nextKey();
+ }
+
+ private void nextKey() {
+ do {
+ try {
+ if (mRemainingCues == null) {
+ throw new NoSuchElementException("");
+ }
+ mCurrentTimeMs = mRemainingCues.firstKey();
+ mListIterator =
+ mRemainingCues.get(mCurrentTimeMs).iterator();
+ try {
+ mRemainingCues =
+ mRemainingCues.tailMap(mCurrentTimeMs + 1);
+ } catch (IllegalArgumentException e) {
+ mRemainingCues = null;
+ }
+ mDone = false;
+ } catch (NoSuchElementException e) {
+ mDone = true;
+ mRemainingCues = null;
+ mListIterator = null;
+ return;
+ }
+ } while (!mListIterator.hasNext());
+ }
+
+ private long mCurrentTimeMs;
+ private Iterator<Cue> mListIterator;
+ private boolean mDone;
+ private SortedMap<Long, ArrayList<Cue>> mRemainingCues;
+ private Iterator<Cue> mLastListIterator;
+ private Pair<Long, Cue> mLastEntry;
+ }
+
+ CueList() {
+ mCues = new TreeMap<Long, ArrayList<Cue>>();
+ }
+ }
+
+ static class Cue {
+ public long mStartTimeMs;
+ public long mEndTimeMs;
+ public long[] mInnerTimesMs;
+ public long mRunID;
+
+ public Cue mNextInRun;
+
+ /**
+ * Called to inform current timeMs to the cue
+ */
+ public void onTime(long timeMs) { }
+ }
+
+ /** update mRunsByEndTime (with default end time) */
+ protected void finishedRun(long runID) {
+ if (runID != 0 && runID != ~0) {
+ Run run = mRunsByID.get(runID);
+ if (run != null) {
+ run.storeByEndTimeMs(mRunsByEndTime);
+ }
+ }
+ }
+
+ /** update mRunsByEndTime with given end time */
+ public void setRunDiscardTimeMs(long runID, long timeMs) {
+ if (runID != 0 && runID != ~0) {
+ Run run = mRunsByID.get(runID);
+ if (run != null) {
+ run.mEndTimeMs = timeMs;
+ run.storeByEndTimeMs(mRunsByEndTime);
+ }
+ }
+ }
+
+ /** whether this is a text track who fires events instead getting rendered */
+ public int getTrackType() {
+ return getRenderingWidget() == null
+ ? TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT
+ : TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE;
+ }
+
+
+ private static class Run {
+ public Cue mFirstCue;
+ public Run mNextRunAtEndTimeMs;
+ public Run mPrevRunAtEndTimeMs;
+ public long mEndTimeMs = -1;
+ public long mRunID = 0;
+ private long mStoredEndTimeMs = -1;
+
+ public void storeByEndTimeMs(LongSparseArray<Run> runsByEndTime) {
+ // remove old value if any
+ int ix = runsByEndTime.indexOfKey(mStoredEndTimeMs);
+ if (ix >= 0) {
+ if (mPrevRunAtEndTimeMs == null) {
+ assert (this == runsByEndTime.valueAt(ix));
+ if (mNextRunAtEndTimeMs == null) {
+ runsByEndTime.removeAt(ix);
+ } else {
+ runsByEndTime.setValueAt(ix, mNextRunAtEndTimeMs);
+ }
+ }
+ removeAtEndTimeMs();
+ }
+
+ // add new value
+ if (mEndTimeMs >= 0) {
+ mPrevRunAtEndTimeMs = null;
+ mNextRunAtEndTimeMs = runsByEndTime.get(mEndTimeMs);
+ if (mNextRunAtEndTimeMs != null) {
+ mNextRunAtEndTimeMs.mPrevRunAtEndTimeMs = this;
+ }
+ runsByEndTime.put(mEndTimeMs, this);
+ mStoredEndTimeMs = mEndTimeMs;
+ }
+ }
+
+ public void removeAtEndTimeMs() {
+ Run prev = mPrevRunAtEndTimeMs;
+
+ if (mPrevRunAtEndTimeMs != null) {
+ mPrevRunAtEndTimeMs.mNextRunAtEndTimeMs = mNextRunAtEndTimeMs;
+ mPrevRunAtEndTimeMs = null;
+ }
+ if (mNextRunAtEndTimeMs != null) {
+ mNextRunAtEndTimeMs.mPrevRunAtEndTimeMs = prev;
+ mNextRunAtEndTimeMs = null;
+ }
+ }
+ }
+
+ /**
+ * Interface for rendering subtitles onto a Canvas.
+ */
+ public interface RenderingWidget {
+ /**
+ * Sets the widget's callback, which is used to send updates when the
+ * rendered data has changed.
+ *
+ * @param callback update callback
+ */
+ void setOnChangedListener(OnChangedListener callback);
+
+ /**
+ * Sets the widget's size.
+ *
+ * @param width width in pixels
+ * @param height height in pixels
+ */
+ void setSize(int width, int height);
+
+ /**
+ * Sets whether the widget should draw subtitles.
+ *
+ * @param visible true if subtitles should be drawn, false otherwise
+ */
+ void setVisible(boolean visible);
+
+ /**
+ * Renders subtitles onto a {@link Canvas}.
+ *
+ * @param c canvas on which to render subtitles
+ */
+ void draw(Canvas c);
+
+ /**
+ * Called when the widget is attached to a window.
+ */
+ void onAttachedToWindow();
+
+ /**
+ * Called when the widget is detached from a window.
+ */
+ void onDetachedFromWindow();
+
+ /**
+ * Callback used to send updates about changes to rendering data.
+ */
+ public interface OnChangedListener {
+ /**
+ * Called when the rendering data has changed.
+ *
+ * @param renderingWidget the widget whose data has changed
+ */
+ void onChanged(RenderingWidget renderingWidget);
+ }
+ }
+}
diff --git a/media/src/main/res/values/dimens.xml b/media/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..be50378
--- /dev/null
+++ b/media/src/main/res/values/dimens.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <!-- Shadow radius for video subtitles. -->
+ <dimen name="subtitle_shadow_radius">2dp</dimen>
+
+ <!-- Shadow offset for video subtitles. -->
+ <dimen name="subtitle_shadow_offset">2dp</dimen>
+
+ <!-- Outline width for video subtitles. -->
+ <dimen name="subtitle_outline_width">2dp</dimen>
+</resources>
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MediaController2Test_copied.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MediaController2Test_copied.java
new file mode 100644
index 0000000..a096160
--- /dev/null
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MediaController2Test_copied.java
@@ -0,0 +1,1512 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.test.client;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.os.ResultReceiver;
+import android.support.mediacompat.testlib.util.PollingCheck;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.annotation.NonNull;
+import androidx.media.AudioAttributesCompat;
+import androidx.media.MediaController2;
+import androidx.media.MediaController2.ControllerCallback;
+import androidx.media.MediaController2.PlaybackInfo;
+import androidx.media.MediaItem2;
+import androidx.media.MediaMetadata2;
+import androidx.media.MediaPlayerInterface;
+import androidx.media.MediaPlaylistAgent;
+import androidx.media.MediaSession2;
+import androidx.media.MediaSession2.ControllerInfo;
+import androidx.media.MediaSession2.SessionCallback;
+import androidx.media.Rating2;
+import androidx.media.SessionCommand2;
+import androidx.media.SessionCommandGroup2;
+import androidx.media.VolumeProviderCompat;
+import androidx.media.test.client.TestUtils.SyncHandler;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Tests {@link MediaController2}.
+ */
+// TODO(jaewan): Implement host-side test so controller and session can run in different processes.
+// TODO(jaewan): Fix flaky failure -- see MediaController2Impl.getController()
+// TODO(jaeawn): Revisit create/close session in the sHandler. It's no longer necessary.
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@FlakyTest
+@Ignore
+public class MediaController2Test_copied extends MediaSession2TestBase {
+ private static final String TAG = "MediaController2Test_copied";
+
+ PendingIntent mIntent;
+ MediaSession2 mSession;
+ MediaController2 mController;
+ MockPlayer mPlayer;
+ MockPlaylistAgent mMockAgent;
+ AudioManager mAudioManager;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ final Intent sessionActivity = new Intent(mContext, MockActivity.class);
+ // Create this test specific MediaSession2 to use our own Handler.
+ mIntent = PendingIntent.getActivity(mContext, 0, sessionActivity, 0);
+
+ mPlayer = new MockPlayer(1);
+ mMockAgent = new MockPlaylistAgent();
+ mSession = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setPlaylistAgent(mMockAgent)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+ @Override
+ public SessionCommandGroup2 onConnect(MediaSession2 session,
+ ControllerInfo controller) {
+ if (Process.myUid() == controller.getUid()) {
+ return super.onConnect(session, controller);
+ }
+ return null;
+ }
+
+ @Override
+ public void onPlaylistMetadataChanged(MediaSession2 session,
+ MediaPlaylistAgent playlistAgent,
+ MediaMetadata2 metadata) {
+ super.onPlaylistMetadataChanged(session, playlistAgent, metadata);
+ }
+ })
+ .setSessionActivity(mIntent)
+ .setId(TAG).build();
+ mController = createController(mSession.getToken());
+ mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ TestServiceRegistry.getInstance().setHandler(sHandler);
+ }
+
+ @After
+ @Override
+ public void cleanUp() throws Exception {
+ super.cleanUp();
+ if (mSession != null) {
+ mSession.close();
+ }
+ TestServiceRegistry.getInstance().cleanUp();
+ }
+
+ /**
+ * Test if the {@link MediaSession2TestBase.TestControllerCallback} wraps the callback proxy
+ * without missing any method.
+ */
+ @Test
+ public void testTestControllerCallback() {
+ prepareLooper();
+ Method[] methods = TestControllerCallback.class.getMethods();
+ assertNotNull(methods);
+ for (int i = 0; i < methods.length; i++) {
+ // For any methods in the controller callback, TestControllerCallback should have
+ // overriden the method and call matching API in the callback proxy.
+ assertNotEquals("TestControllerCallback should override " + methods[i]
+ + " and call callback proxy",
+ ControllerCallback.class, methods[i].getDeclaringClass());
+ }
+ }
+
+ @Test
+ public void testPlay() {
+ prepareLooper();
+ mController.play();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mPlayCalled);
+ }
+
+ @Test
+ public void testPause() {
+ prepareLooper();
+ mController.pause();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mPauseCalled);
+ }
+
+ @Test
+ public void testReset() {
+ prepareLooper();
+ mController.reset();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mResetCalled);
+ }
+
+ @Test
+ public void testPrepare() {
+ prepareLooper();
+ mController.prepare();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mPrepareCalled);
+ }
+
+ @Test
+ public void testSeekTo() {
+ prepareLooper();
+ final long seekPosition = 12125L;
+ mController.seekTo(seekPosition);
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mSeekToCalled);
+ assertEquals(seekPosition, mPlayer.mSeekPosition);
+ }
+
+ @Test
+ public void testGettersAfterConnected() throws InterruptedException {
+ prepareLooper();
+ final int state = MediaPlayerInterface.PLAYER_STATE_PLAYING;
+ final int bufferingState = MediaPlayerInterface.BUFFERING_STATE_BUFFERING_COMPLETE;
+ final long position = 150000;
+ final long bufferedPosition = 900000;
+ final float speed = 0.5f;
+ final long timeDiff = 102;
+ final MediaItem2 currentMediaItem = TestUtils.createMediaItemWithMetadata();
+
+ mPlayer.mLastPlayerState = state;
+ mPlayer.mLastBufferingState = bufferingState;
+ mPlayer.mCurrentPosition = position;
+ mPlayer.mBufferedPosition = bufferedPosition;
+ mPlayer.mPlaybackSpeed = speed;
+ mMockAgent.mCurrentMediaItem = currentMediaItem;
+
+ MediaController2 controller = createController(mSession.getToken());
+ // setTimeDiff is package-private, so cannot be called here.
+// controller.setTimeDiff(timeDiff);
+ assertEquals(state, controller.getPlayerState());
+ assertEquals(bufferedPosition, controller.getBufferedPosition());
+ assertEquals(speed, controller.getPlaybackSpeed(), 0.0f);
+ assertEquals(position + (long) (speed * timeDiff), controller.getCurrentPosition());
+ assertEquals(currentMediaItem, controller.getCurrentMediaItem());
+ }
+
+ @Test
+ public void testUpdatePlayer() throws InterruptedException {
+ prepareLooper();
+ final int testState = MediaPlayerInterface.PLAYER_STATE_PLAYING;
+ final List<MediaItem2> testPlaylist = TestUtils.createPlaylist(3);
+ final AudioAttributesCompat testAudioAttributes = new AudioAttributesCompat.Builder()
+ .setLegacyStreamType(AudioManager.STREAM_RING).build();
+ final CountDownLatch latch = new CountDownLatch(3);
+ mController = createController(mSession.getToken(), true, new ControllerCallback() {
+ @Override
+ public void onPlayerStateChanged(MediaController2 controller, int state) {
+ assertEquals(mController, controller);
+ assertEquals(testState, state);
+ latch.countDown();
+ }
+
+ @Override
+ public void onPlaylistChanged(MediaController2 controller, List<MediaItem2> list,
+ MediaMetadata2 metadata) {
+ assertEquals(mController, controller);
+ assertEquals(testPlaylist, list);
+ assertNull(metadata);
+ latch.countDown();
+ }
+
+ @Override
+ public void onPlaybackInfoChanged(MediaController2 controller, PlaybackInfo info) {
+ assertEquals(mController, controller);
+ assertEquals(testAudioAttributes, info.getAudioAttributes());
+ latch.countDown();
+ }
+ });
+
+ MockPlayer player = new MockPlayer(0);
+ player.mLastPlayerState = testState;
+ player.setAudioAttributes(testAudioAttributes);
+
+ MockPlaylistAgent agent = new MockPlaylistAgent();
+ agent.mPlaylist = testPlaylist;
+
+ mSession.updatePlayer(player, agent, null);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testGetSessionActivity() {
+ prepareLooper();
+ PendingIntent sessionActivity = mController.getSessionActivity();
+ assertEquals(mContext.getPackageName(), sessionActivity.getCreatorPackage());
+ assertEquals(Process.myUid(), sessionActivity.getCreatorUid());
+ }
+
+ @Test
+ public void testSetPlaylist() throws InterruptedException {
+ prepareLooper();
+ final List<MediaItem2> list = TestUtils.createPlaylist(2);
+ mController.setPlaylist(list, null /* Metadata */);
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ assertTrue(mMockAgent.mSetPlaylistCalled);
+ assertNull(mMockAgent.mMetadata);
+
+ assertNotNull(mMockAgent.mPlaylist);
+ assertEquals(list.size(), mMockAgent.mPlaylist.size());
+ for (int i = 0; i < list.size(); i++) {
+ // MediaController2.setPlaylist does not ensure the equality of the items.
+ assertEquals(list.get(i).getMediaId(), mMockAgent.mPlaylist.get(i).getMediaId());
+ }
+ }
+
+ /**
+ * This also tests {@link ControllerCallback#onPlaylistChanged(
+ * MediaController2, List, MediaMetadata2)}.
+ */
+ @Test
+ public void testGetPlaylist() throws InterruptedException {
+ prepareLooper();
+ final List<MediaItem2> testList = TestUtils.createPlaylist(2);
+ final AtomicReference<List<MediaItem2>> listFromCallback = new AtomicReference<>();
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onPlaylistChanged(MediaController2 controller,
+ List<MediaItem2> playlist, MediaMetadata2 metadata) {
+ assertNotNull(playlist);
+ assertEquals(testList.size(), playlist.size());
+ for (int i = 0; i < playlist.size(); i++) {
+ assertEquals(testList.get(i).getMediaId(), playlist.get(i).getMediaId());
+ }
+ listFromCallback.set(playlist);
+ latch.countDown();
+ }
+ };
+ final MediaPlaylistAgent agent = new MockPlaylistAgent() {
+ @Override
+ public List<MediaItem2> getPlaylist() {
+ return testList;
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setId("testControllerCallback_onPlaylistChanged")
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {})
+ .setPlaylistAgent(agent)
+ .build()) {
+ MediaController2 controller = createController(
+ session.getToken(), true, callback);
+ agent.notifyPlaylistChanged();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertEquals(listFromCallback.get(), controller.getPlaylist());
+ }
+ }
+
+ @Test
+ public void testUpdatePlaylistMetadata() throws InterruptedException {
+ prepareLooper();
+ final MediaMetadata2 testMetadata = TestUtils.createMetadata();
+ mController.updatePlaylistMetadata(testMetadata);
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ assertTrue(mMockAgent.mUpdatePlaylistMetadataCalled);
+ assertNotNull(mMockAgent.mMetadata);
+ assertEquals(testMetadata.getMediaId(), mMockAgent.mMetadata.getMediaId());
+ }
+
+ @Test
+ public void testGetPlaylistMetadata() throws InterruptedException {
+ prepareLooper();
+ final MediaMetadata2 testMetadata = TestUtils.createMetadata();
+ final AtomicReference<MediaMetadata2> metadataFromCallback = new AtomicReference<>();
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onPlaylistMetadataChanged(MediaController2 controller,
+ MediaMetadata2 metadata) {
+ assertNotNull(testMetadata);
+ assertEquals(testMetadata.getMediaId(), metadata.getMediaId());
+ metadataFromCallback.set(metadata);
+ latch.countDown();
+ }
+ };
+ final MediaPlaylistAgent agent = new MockPlaylistAgent() {
+ @Override
+ public MediaMetadata2 getPlaylistMetadata() {
+ return testMetadata;
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setId("testGetPlaylistMetadata")
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {})
+ .setPlaylistAgent(agent)
+ .build()) {
+ MediaController2 controller = createController(session.getToken(), true, callback);
+ agent.notifyPlaylistMetadataChanged();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertEquals(metadataFromCallback.get().getMediaId(),
+ controller.getPlaylistMetadata().getMediaId());
+ }
+ }
+
+ @Test
+ public void testSetPlaybackSpeed() throws Exception {
+ prepareLooper();
+ final float speed = 1.5f;
+ mController.setPlaybackSpeed(speed);
+ assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertEquals(speed, mPlayer.mPlaybackSpeed, 0.0f);
+ }
+
+ /**
+ * Test whether {@link MediaSession2#setPlaylist(List, MediaMetadata2)} is notified
+ * through the
+ * {@link ControllerCallback#onPlaylistMetadataChanged(MediaController2, MediaMetadata2)}
+ * if the controller doesn't have {@link SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST} but
+ * {@link SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST_METADATA}.
+ */
+ @Test
+ public void testControllerCallback_onPlaylistMetadataChanged() throws InterruptedException {
+ prepareLooper();
+ final MediaItem2 item = TestUtils.createMediaItemWithMetadata();
+ final List<MediaItem2> list = TestUtils.createPlaylist(2);
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onPlaylistMetadataChanged(MediaController2 controller,
+ MediaMetadata2 metadata) {
+ assertNotNull(metadata);
+ assertEquals(item.getMediaId(), metadata.getMediaId());
+ latch.countDown();
+ }
+ };
+ final SessionCallback sessionCallback = new SessionCallback() {
+ @Override
+ public SessionCommandGroup2 onConnect(MediaSession2 session,
+ ControllerInfo controller) {
+ if (Process.myUid() == controller.getUid()) {
+ SessionCommandGroup2 commands = new SessionCommandGroup2();
+ commands.addCommand(new SessionCommand2(
+ SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST_METADATA));
+ return commands;
+ }
+ return super.onConnect(session, controller);
+ }
+ };
+ final MediaPlaylistAgent agent = new MockPlaylistAgent() {
+ @Override
+ public MediaMetadata2 getPlaylistMetadata() {
+ return item.getMetadata();
+ }
+
+ @Override
+ public List<MediaItem2> getPlaylist() {
+ return list;
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setId("testControllerCallback_onPlaylistMetadataChanged")
+ .setSessionCallback(sHandlerExecutor, sessionCallback)
+ .setPlaylistAgent(agent)
+ .build()) {
+ MediaController2 controller = createController(session.getToken(), true, callback);
+ agent.notifyPlaylistMetadataChanged();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+
+ @Test
+ public void testControllerCallback_onSeekCompleted() throws InterruptedException {
+ prepareLooper();
+ final long testSeekPosition = 400;
+ final long testPosition = 500;
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onSeekCompleted(MediaController2 controller, long position) {
+ // setTimeDiff is package-private, so cannot be called here.
+// controller.setTimeDiff(Long.valueOf(0));
+ assertEquals(testSeekPosition, position);
+ assertEquals(testPosition, controller.getCurrentPosition());
+ latch.countDown();
+ }
+ };
+ final MediaController2 controller = createController(mSession.getToken(), true, callback);
+ mPlayer.mCurrentPosition = testPosition;
+ mPlayer.notifySeekCompleted(testSeekPosition);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testControllerCallback_onBufferingStateChanged() throws InterruptedException {
+ prepareLooper();
+ final List<MediaItem2> testPlaylist = TestUtils.createPlaylist(3);
+ final MediaItem2 testItem = testPlaylist.get(0);
+ final int testBufferingState = MediaPlayerInterface.BUFFERING_STATE_BUFFERING_AND_PLAYABLE;
+ final long testBufferingPosition = 500;
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onBufferingStateChanged(MediaController2 controller, MediaItem2 item,
+ int state) {
+ // setTimeDiff is package-private, so cannot be called here.
+// controller.setTimeDiff(Long.valueOf(0));
+ assertEquals(testItem, item);
+ assertEquals(testBufferingState, state);
+ assertEquals(testBufferingState, controller.getBufferingState());
+ assertEquals(testBufferingPosition, controller.getBufferedPosition());
+ latch.countDown();
+ }
+ };
+ final MediaController2 controller = createController(mSession.getToken(), true, callback);
+ mSession.setPlaylist(testPlaylist, null);
+ mPlayer.mBufferedPosition = testBufferingPosition;
+ mPlayer.notifyBufferingStateChanged(testItem.getDataSourceDesc(), testBufferingState);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testControllerCallback_onPlayerStateChanged() throws InterruptedException {
+ prepareLooper();
+ final int testPlayerState = MediaPlayerInterface.PLAYER_STATE_PLAYING;
+ final long testPosition = 500;
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onPlayerStateChanged(MediaController2 controller, int state) {
+ // setTimeDiff is package-private, so cannot be called here.
+// controller.setTimeDiff(Long.valueOf(0));
+ assertEquals(testPlayerState, state);
+ assertEquals(testPlayerState, controller.getPlayerState());
+ assertEquals(testPosition, controller.getCurrentPosition());
+ latch.countDown();
+ }
+ };
+ final MediaController2 controller = createController(mSession.getToken(), true, callback);
+ mPlayer.mCurrentPosition = testPosition;
+ mPlayer.notifyPlaybackState(testPlayerState);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testAddPlaylistItem() throws InterruptedException {
+ prepareLooper();
+ final int testIndex = 12;
+ final MediaItem2 testMediaItem = TestUtils.createMediaItemWithMetadata();
+ mController.addPlaylistItem(testIndex, testMediaItem);
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ assertTrue(mMockAgent.mAddPlaylistItemCalled);
+ assertEquals(testIndex, mMockAgent.mIndex);
+ // MediaController2.addPlaylistItem does not ensure the equality of the items.
+ assertEquals(testMediaItem.getMediaId(), mMockAgent.mItem.getMediaId());
+ }
+
+ @Test
+ public void testRemovePlaylistItem() throws InterruptedException {
+ prepareLooper();
+ mMockAgent.mPlaylist = TestUtils.createPlaylist(2);
+
+ // Recreate controller for sending removePlaylistItem.
+ // It's easier to ensure that MediaController2.getPlaylist() returns the playlist from the
+ // agent.
+ MediaController2 controller = createController(mSession.getToken());
+ MediaItem2 targetItem = controller.getPlaylist().get(0);
+ controller.removePlaylistItem(targetItem);
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ assertTrue(mMockAgent.mRemovePlaylistItemCalled);
+ assertEquals(targetItem, mMockAgent.mItem);
+ }
+
+ @Test
+ public void testReplacePlaylistItem() throws InterruptedException {
+ prepareLooper();
+ final int testIndex = 12;
+ final MediaItem2 testMediaItem = TestUtils.createMediaItemWithMetadata();
+ mController.replacePlaylistItem(testIndex, testMediaItem);
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ assertTrue(mMockAgent.mReplacePlaylistItemCalled);
+ // MediaController2.replacePlaylistItem does not ensure the equality of the items.
+ assertEquals(testMediaItem.getMediaId(), mMockAgent.mItem.getMediaId());
+ }
+
+ @Test
+ public void testSkipToPreviousItem() throws InterruptedException {
+ prepareLooper();
+ mController.skipToPreviousItem();
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mMockAgent.mSkipToPreviousItemCalled);
+ }
+
+ @Test
+ public void testSkipToNextItem() throws InterruptedException {
+ prepareLooper();
+ mController.skipToNextItem();
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mMockAgent.mSkipToNextItemCalled);
+ }
+
+ @Test
+ public void testSkipToPlaylistItem() throws InterruptedException {
+ prepareLooper();
+ MediaController2 controller = createController(mSession.getToken());
+ MediaItem2 targetItem = TestUtils.createMediaItemWithMetadata();
+ controller.skipToPlaylistItem(targetItem);
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ assertTrue(mMockAgent.mSkipToPlaylistItemCalled);
+ assertEquals(targetItem, mMockAgent.mItem);
+ }
+
+ /**
+ * This also tests {@link ControllerCallback#onShuffleModeChanged(MediaController2, int)}.
+ */
+ @Test
+ public void testGetShuffleMode() throws InterruptedException {
+ prepareLooper();
+ final int testShuffleMode = MediaPlaylistAgent.SHUFFLE_MODE_GROUP;
+ final MediaPlaylistAgent agent = new MockPlaylistAgent() {
+ @Override
+ public int getShuffleMode() {
+ return testShuffleMode;
+ }
+ };
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onShuffleModeChanged(MediaController2 controller, int shuffleMode) {
+ assertEquals(testShuffleMode, shuffleMode);
+ latch.countDown();
+ }
+ };
+ mSession.updatePlayer(mPlayer, agent, null);
+ MediaController2 controller = createController(mSession.getToken(), true, callback);
+ agent.notifyShuffleModeChanged();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertEquals(testShuffleMode, controller.getShuffleMode());
+ }
+
+ @Test
+ public void testSetShuffleMode() throws InterruptedException {
+ prepareLooper();
+ final int testShuffleMode = MediaPlaylistAgent.SHUFFLE_MODE_GROUP;
+ mController.setShuffleMode(testShuffleMode);
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ assertTrue(mMockAgent.mSetShuffleModeCalled);
+ assertEquals(testShuffleMode, mMockAgent.mShuffleMode);
+ }
+
+ /**
+ * This also tests {@link ControllerCallback#onRepeatModeChanged(MediaController2, int)}.
+ */
+ @Test
+ public void testGetRepeatMode() throws InterruptedException {
+ prepareLooper();
+ final int testRepeatMode = MediaPlaylistAgent.REPEAT_MODE_GROUP;
+ final MediaPlaylistAgent agent = new MockPlaylistAgent() {
+ @Override
+ public int getRepeatMode() {
+ return testRepeatMode;
+ }
+ };
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onRepeatModeChanged(MediaController2 controller, int repeatMode) {
+ assertEquals(testRepeatMode, repeatMode);
+ latch.countDown();
+ }
+ };
+ mSession.updatePlayer(mPlayer, agent, null);
+ MediaController2 controller = createController(mSession.getToken(), true, callback);
+ agent.notifyRepeatModeChanged();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertEquals(testRepeatMode, controller.getRepeatMode());
+ }
+
+ @Test
+ public void testSetRepeatMode() throws InterruptedException {
+ prepareLooper();
+ final int testRepeatMode = MediaPlaylistAgent.REPEAT_MODE_GROUP;
+ mController.setRepeatMode(testRepeatMode);
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ assertTrue(mMockAgent.mSetRepeatModeCalled);
+ assertEquals(testRepeatMode, mMockAgent.mRepeatMode);
+ }
+
+ @Test
+ public void testSetVolumeTo() throws Exception {
+ prepareLooper();
+ final int maxVolume = 100;
+ final int currentVolume = 23;
+ final int volumeControlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
+ TestVolumeProvider volumeProvider =
+ new TestVolumeProvider(volumeControlType, maxVolume, currentVolume);
+
+ mSession.updatePlayer(new MockPlayer(0), null, volumeProvider);
+ final MediaController2 controller = createController(mSession.getToken(), true, null);
+
+ final int targetVolume = 50;
+ controller.setVolumeTo(targetVolume, 0 /* flags */);
+ assertTrue(volumeProvider.mLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertTrue(volumeProvider.mSetVolumeToCalled);
+ assertEquals(targetVolume, volumeProvider.mVolume);
+ }
+
+ @Test
+ public void testAdjustVolume() throws Exception {
+ prepareLooper();
+ final int maxVolume = 100;
+ final int currentVolume = 23;
+ final int volumeControlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
+ TestVolumeProvider volumeProvider =
+ new TestVolumeProvider(volumeControlType, maxVolume, currentVolume);
+
+ mSession.updatePlayer(new MockPlayer(0), null, volumeProvider);
+ final MediaController2 controller = createController(mSession.getToken(), true, null);
+
+ final int direction = AudioManager.ADJUST_RAISE;
+ controller.adjustVolume(direction, 0 /* flags */);
+ assertTrue(volumeProvider.mLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertTrue(volumeProvider.mAdjustVolumeCalled);
+ assertEquals(direction, volumeProvider.mDirection);
+ }
+
+ @Test
+ public void testSetVolumeWithLocalVolume() throws Exception {
+ prepareLooper();
+ if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
+ // This test is not eligible for this device.
+ return;
+ }
+
+ // Here, we intentionally choose STREAM_ALARM in order not to consider
+ // 'Do Not Disturb' or 'Volume limit'.
+ final int stream = AudioManager.STREAM_ALARM;
+ final int maxVolume = mAudioManager.getStreamMaxVolume(stream);
+ final int minVolume = 0;
+ if (maxVolume <= minVolume) {
+ return;
+ }
+
+ // Set stream of the session.
+ AudioAttributesCompat attrs = new AudioAttributesCompat.Builder()
+ .setLegacyStreamType(stream)
+ .build();
+ mPlayer.setAudioAttributes(attrs);
+ mSession.updatePlayer(mPlayer, null, null);
+
+ final int originalVolume = mAudioManager.getStreamVolume(stream);
+ final int targetVolume = originalVolume == minVolume
+ ? originalVolume + 1 : originalVolume - 1;
+
+ mController.setVolumeTo(targetVolume, AudioManager.FLAG_SHOW_UI);
+ new PollingCheck(WAIT_TIME_MS) {
+ @Override
+ protected boolean check() {
+ return targetVolume == mAudioManager.getStreamVolume(stream);
+ }
+ }.run();
+
+ // Set back to original volume.
+ mAudioManager.setStreamVolume(stream, originalVolume, 0 /* flags */);
+ }
+
+ @Test
+ public void testAdjustVolumeWithLocalVolume() throws Exception {
+ prepareLooper();
+ if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
+ // This test is not eligible for this device.
+ return;
+ }
+
+ // Here, we intentionally choose STREAM_ALARM in order not to consider
+ // 'Do Not Disturb' or 'Volume limit'.
+ final int stream = AudioManager.STREAM_ALARM;
+ final int maxVolume = mAudioManager.getStreamMaxVolume(stream);
+ final int minVolume = 0;
+ if (maxVolume <= minVolume) {
+ return;
+ }
+
+ // Set stream of the session.
+ AudioAttributesCompat attrs = new AudioAttributesCompat.Builder()
+ .setLegacyStreamType(stream)
+ .build();
+ mPlayer.setAudioAttributes(attrs);
+ mSession.updatePlayer(mPlayer, null, null);
+
+ final int originalVolume = mAudioManager.getStreamVolume(stream);
+ final int direction = originalVolume == minVolume
+ ? AudioManager.ADJUST_RAISE : AudioManager.ADJUST_LOWER;
+ final int targetVolume = originalVolume + direction;
+
+ mController.adjustVolume(direction, AudioManager.FLAG_SHOW_UI);
+ new PollingCheck(WAIT_TIME_MS) {
+ @Override
+ protected boolean check() {
+ return targetVolume == mAudioManager.getStreamVolume(stream);
+ }
+ }.run();
+
+ // Set back to original volume.
+ mAudioManager.setStreamVolume(stream, originalVolume, 0 /* flags */);
+ }
+
+ @Test
+ public void testGetPackageName() {
+ prepareLooper();
+ assertEquals(mContext.getPackageName(), mController.getSessionToken().getPackageName());
+ }
+
+ @Test
+ public void testSendCustomCommand() throws InterruptedException {
+ prepareLooper();
+ // TODO(jaewan): Need to revisit with the permission.
+ final SessionCommand2 testCommand =
+ new SessionCommand2(SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE);
+ final Bundle testArgs = new Bundle();
+ testArgs.putString("args", "testSendCustomCommand");
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onCustomCommand(MediaSession2 session, ControllerInfo controller,
+ SessionCommand2 customCommand, Bundle args, ResultReceiver cb) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(testCommand, customCommand);
+ assertTrue(TestUtils.equals(testArgs, args));
+ assertNull(cb);
+ latch.countDown();
+ }
+ };
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback).setId(TAG).build();
+ final MediaController2 controller = createController(mSession.getToken());
+ controller.sendCustomCommand(testCommand, testArgs, null);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testControllerCallback_onConnected() throws InterruptedException {
+ prepareLooper();
+ // createController() uses controller callback to wait until the controller becomes
+ // available.
+ MediaController2 controller = createController(mSession.getToken());
+ assertNotNull(controller);
+ }
+
+ @Test
+ public void testControllerCallback_sessionRejects() throws InterruptedException {
+ prepareLooper();
+ final MediaSession2.SessionCallback sessionCallback = new SessionCallback() {
+ @Override
+ public SessionCommandGroup2 onConnect(MediaSession2 session,
+ ControllerInfo controller) {
+ return null;
+ }
+ };
+ sHandler.postAndSync(new Runnable() {
+ @Override
+ public void run() {
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, sessionCallback).build();
+ }
+ });
+ MediaController2 controller =
+ createController(mSession.getToken(), false, null);
+ assertNotNull(controller);
+ waitForConnect(controller, false);
+ waitForDisconnect(controller, true);
+ }
+
+ @Test
+ public void testControllerCallback_releaseSession() throws InterruptedException {
+ prepareLooper();
+ mSession.close();
+ waitForDisconnect(mController, true);
+ }
+
+ @Test
+ public void testControllerCallback_close() throws InterruptedException {
+ prepareLooper();
+ mController.close();
+ waitForDisconnect(mController, true);
+ }
+
+ @Test
+ public void testFastForward() throws InterruptedException {
+ prepareLooper();
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onFastForward(MediaSession2 session, ControllerInfo controller) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testFastForward").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.fastForward();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testRewind() throws InterruptedException {
+ prepareLooper();
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onRewind(MediaSession2 session, ControllerInfo controller) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testRewind").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.rewind();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testPlayFromSearch() throws InterruptedException {
+ prepareLooper();
+ final String request = "random query";
+ final Bundle bundle = new Bundle();
+ bundle.putString("key", "value");
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onPlayFromSearch(MediaSession2 session, ControllerInfo controller,
+ String query, Bundle extras) {
+ super.onPlayFromSearch(session, controller, query, extras);
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(request, query);
+ assertTrue(TestUtils.equals(bundle, extras));
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testPlayFromSearch").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.playFromSearch(request, bundle);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testPlayFromUri() throws InterruptedException {
+ prepareLooper();
+ final Uri request = Uri.parse("foo://boo");
+ final Bundle bundle = new Bundle();
+ bundle.putString("key", "value");
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onPlayFromUri(MediaSession2 session, ControllerInfo controller, Uri uri,
+ Bundle extras) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(request, uri);
+ assertTrue(TestUtils.equals(bundle, extras));
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testPlayFromUri").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.playFromUri(request, bundle);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testPlayFromMediaId() throws InterruptedException {
+ prepareLooper();
+ final String request = "media_id";
+ final Bundle bundle = new Bundle();
+ bundle.putString("key", "value");
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onPlayFromMediaId(MediaSession2 session, ControllerInfo controller,
+ String mediaId, Bundle extras) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(request, mediaId);
+ assertTrue(TestUtils.equals(bundle, extras));
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testPlayFromMediaId").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.playFromMediaId(request, bundle);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testPrepareFromSearch() throws InterruptedException {
+ prepareLooper();
+ final String request = "random query";
+ final Bundle bundle = new Bundle();
+ bundle.putString("key", "value");
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onPrepareFromSearch(MediaSession2 session, ControllerInfo controller,
+ String query, Bundle extras) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(request, query);
+ assertTrue(TestUtils.equals(bundle, extras));
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testPrepareFromSearch").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.prepareFromSearch(request, bundle);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testPrepareFromUri() throws InterruptedException {
+ prepareLooper();
+ final Uri request = Uri.parse("foo://boo");
+ final Bundle bundle = new Bundle();
+ bundle.putString("key", "value");
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onPrepareFromUri(MediaSession2 session, ControllerInfo controller, Uri uri,
+ Bundle extras) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(request, uri);
+ assertTrue(TestUtils.equals(bundle, extras));
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testPrepareFromUri").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.prepareFromUri(request, bundle);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testPrepareFromMediaId() throws InterruptedException {
+ prepareLooper();
+ final String request = "media_id";
+ final Bundle bundle = new Bundle();
+ bundle.putString("key", "value");
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onPrepareFromMediaId(MediaSession2 session, ControllerInfo controller,
+ String mediaId, Bundle extras) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(request, mediaId);
+ assertTrue(TestUtils.equals(bundle, extras));
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testPrepareFromMediaId").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.prepareFromMediaId(request, bundle);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testSetRating() throws InterruptedException {
+ prepareLooper();
+ final int ratingType = Rating2.RATING_5_STARS;
+ final float ratingValue = 3.5f;
+ final Rating2 rating = Rating2.newStarRating(ratingType, ratingValue);
+ final String mediaId = "media_id";
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onSetRating(MediaSession2 session, ControllerInfo controller,
+ String mediaIdOut, Rating2 ratingOut) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(mediaId, mediaIdOut);
+ assertEquals(rating, ratingOut);
+ latch.countDown();
+ }
+ };
+
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testSetRating").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.setRating(mediaId, rating);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testIsConnected() throws InterruptedException {
+ prepareLooper();
+ assertTrue(mController.isConnected());
+ sHandler.postAndSync(new Runnable() {
+ @Override
+ public void run() {
+ mSession.close();
+ }
+ });
+ waitForDisconnect(mController, true);
+ assertFalse(mController.isConnected());
+ }
+
+ /**
+ * Test potential deadlock for calls between controller and session.
+ */
+ @Test
+ public void testDeadlock() throws InterruptedException {
+ prepareLooper();
+ sHandler.postAndSync(new Runnable() {
+ @Override
+ public void run() {
+ mSession.close();
+ mSession = null;
+ }
+ });
+
+ // Two more threads are needed not to block test thread nor test wide thread (sHandler).
+ final HandlerThread sessionThread = new HandlerThread("testDeadlock_session");
+ final HandlerThread testThread = new HandlerThread("testDeadlock_test");
+ sessionThread.start();
+ testThread.start();
+ final SyncHandler sessionHandler = new SyncHandler(sessionThread.getLooper());
+ final Handler testHandler = new Handler(testThread.getLooper());
+ final CountDownLatch latch = new CountDownLatch(1);
+ try {
+ final MockPlayer player = new MockPlayer(0);
+ sessionHandler.postAndSync(new Runnable() {
+ @Override
+ public void run() {
+ mSession = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {})
+ .setId("testDeadlock").build();
+ }
+ });
+ final MediaController2 controller = createController(mSession.getToken());
+ testHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ final int state = MediaPlayerInterface.PLAYER_STATE_ERROR;
+ for (int i = 0; i < 100; i++) {
+ // triggers call from session to controller.
+ player.notifyPlaybackState(state);
+ // triggers call from controller to session.
+ controller.play();
+
+ // Repeat above
+ player.notifyPlaybackState(state);
+ controller.pause();
+ player.notifyPlaybackState(state);
+ controller.reset();
+ player.notifyPlaybackState(state);
+ controller.skipToNextItem();
+ player.notifyPlaybackState(state);
+ controller.skipToPreviousItem();
+ }
+ // This may hang if deadlock happens.
+ latch.countDown();
+ }
+ });
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } finally {
+ if (mSession != null) {
+ sessionHandler.postAndSync(new Runnable() {
+ @Override
+ public void run() {
+ // Clean up here because sessionHandler will be removed afterwards.
+ mSession.close();
+ mSession = null;
+ }
+ });
+ }
+
+ if (Build.VERSION.SDK_INT >= 18) {
+ sessionThread.quitSafely();
+ testThread.quitSafely();
+ } else {
+ sessionThread.quit();
+ testThread.quit();
+ }
+ }
+ }
+
+ // Temporaily commenting out, since we don't have the Mock services yet.
+// @Test
+// public void testGetServiceToken() {
+// prepareLooper();
+// SessionToken2 token = TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID);
+// assertNotNull(token);
+// assertEquals(mContext.getPackageName(), token.getPackageName());
+// assertEquals(MockMediaSessionService2.ID, token.getId());
+// assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
+// }
+//
+// @Test
+// public void testConnectToService_sessionService() throws InterruptedException {
+// prepareLooper();
+// testConnectToService(MockMediaSessionService2.ID);
+// }
+//
+// @Test
+// public void testConnectToService_libraryService() throws InterruptedException {
+// prepareLooper();
+// testConnectToService(MockMediaLibraryService2.ID);
+// }
+//
+// public void testConnectToService(String id) throws InterruptedException {
+// prepareLooper();
+// final CountDownLatch latch = new CountDownLatch(1);
+// final MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback() {
+// @Override
+// public SessionCommandGroup2 onConnect(@NonNull MediaSession2 session,
+// @NonNull ControllerInfo controller) {
+// if (Process.myUid() == controller.getUid()) {
+// if (mSession != null) {
+// mSession.close();
+// }
+// mSession = session;
+// mPlayer = (MockPlayer) session.getPlayer();
+// assertEquals(mContext.getPackageName(), controller.getPackageName());
+// assertFalse(controller.isTrusted());
+// latch.countDown();
+// }
+// return super.onConnect(session, controller);
+// }
+// };
+// TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
+//
+// final SessionCommand2 testCommand = new SessionCommand2("testConnectToService", null);
+// final CountDownLatch controllerLatch = new CountDownLatch(1);
+// mController = createController(TestUtils.getServiceToken(mContext, id), true,
+// new ControllerCallback() {
+// @Override
+// public void onCustomCommand(MediaController2 controller,
+// SessionCommand2 command, Bundle args, ResultReceiver receiver) {
+// if (testCommand.equals(command)) {
+// controllerLatch.countDown();
+// }
+// }
+// }
+// );
+// assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+//
+// // Test command from controller to session service.
+// mController.play();
+// assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+// assertTrue(mPlayer.mPlayCalled);
+//
+// // Test command from session service to controller.
+// mSession.sendCustomCommand(testCommand, null);
+// assertTrue(controllerLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+// }
+
+ @Test
+ public void testControllerAfterSessionIsGone_session() throws InterruptedException {
+ prepareLooper();
+ testControllerAfterSessionIsClosed(mSession.getToken().getId());
+ }
+
+ // Temporaily commenting out, since we don't have the Mock services yet.
+// @Test
+// public void testControllerAfterSessionIsClosed_sessionService() throws InterruptedException {
+// prepareLooper();
+// testConnectToService(MockMediaSessionService2.ID);
+// testControllerAfterSessionIsClosed(MockMediaSessionService2.ID);
+// }
+
+ @Test
+ public void testSubscribeRouteInfo() throws InterruptedException {
+ prepareLooper();
+ final TestSessionCallback callback = new TestSessionCallback() {
+ @Override
+ public void onSubscribeRoutesInfo(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ mLatch.countDown();
+ }
+
+ @Override
+ public void onUnsubscribeRoutesInfo(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ mLatch.countDown();
+ }
+ };
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback).setId(TAG).build();
+ final MediaController2 controller = createController(mSession.getToken());
+
+ callback.resetLatchCount(1);
+ controller.subscribeRoutesInfo();
+ assertTrue(callback.mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ callback.resetLatchCount(1);
+ controller.unsubscribeRoutesInfo();
+ assertTrue(callback.mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testSelectRouteInfo() throws InterruptedException {
+ prepareLooper();
+ final Bundle testRoute = new Bundle();
+ testRoute.putString("id", "testRoute");
+ final TestSessionCallback callback = new TestSessionCallback() {
+ @Override
+ public void onSelectRoute(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller, @NonNull Bundle route) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertTrue(TestUtils.equals(route, testRoute));
+ mLatch.countDown();
+ }
+ };
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback).setId(TAG).build();
+ final MediaController2 controller = createController(mSession.getToken());
+
+ callback.resetLatchCount(1);
+ controller.selectRoute(testRoute);
+ assertTrue(callback.mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testClose_beforeConnected() throws InterruptedException {
+ prepareLooper();
+ MediaController2 controller =
+ createController(mSession.getToken(), false, null);
+ controller.close();
+ }
+
+ @Test
+ public void testClose_twice() {
+ prepareLooper();
+ mController.close();
+ mController.close();
+ }
+
+ @Test
+ public void testClose_session() throws InterruptedException {
+ prepareLooper();
+ final String id = mSession.getToken().getId();
+ mController.close();
+ // close is done immediately for session.
+ testNoInteraction();
+
+ // Test whether the controller is notified about later close of the session or
+ // re-creation.
+ testControllerAfterSessionIsClosed(id);
+ }
+
+ // Temporaily commenting out, since we don't have the Mock services yet.
+// @Test
+// public void testClose_sessionService() throws InterruptedException {
+// prepareLooper();
+// testCloseFromService(MockMediaSessionService2.ID);
+// }
+//
+// @Test
+// public void testClose_libraryService() throws InterruptedException {
+// prepareLooper();
+// testCloseFromService(MockMediaLibraryService2.ID);
+// }
+//
+// private void testCloseFromService(String id) throws InterruptedException {
+// final CountDownLatch latch = new CountDownLatch(1);
+// TestServiceRegistry.getInstance().setSessionServiceCallback(new SessionServiceCallback() {
+// @Override
+// public void onCreated() {
+// // Do nothing.
+// }
+//
+// @Override
+// public void onDestroyed() {
+// latch.countDown();
+// }
+// });
+// mController = createController(TestUtils.getServiceToken(mContext, id));
+// mController.close();
+// // Wait until close triggers onDestroy() of the session service.
+// assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+// assertNull(TestServiceRegistry.getInstance().getServiceInstance());
+// testNoInteraction();
+//
+// // Test whether the controller is notified about later close of the session or
+// // re-creation.
+// testControllerAfterSessionIsClosed(id);
+// }
+
+ private void testControllerAfterSessionIsClosed(final String id) throws InterruptedException {
+ // This cause session service to be died.
+ mSession.close();
+ waitForDisconnect(mController, true);
+ testNoInteraction();
+
+ // Ensure that the controller cannot use newly create session with the same ID.
+ // Recreated session has different session stub, so previously created controller
+ // shouldn't be available.
+ mSession = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {})
+ .setId(id).build();
+ testNoInteraction();
+ }
+
+ // Test that mSession and mController doesn't interact.
+ // Note that this method can be called after the mSession is died, so mSession may not have
+ // valid player.
+ private void testNoInteraction() throws InterruptedException {
+ // TODO: check that calls from the controller to session shouldn't be delivered.
+
+ // Calls from the session to controller shouldn't be delivered.
+ final CountDownLatch latch = new CountDownLatch(1);
+ setRunnableForOnCustomCommand(mController, new Runnable() {
+ @Override
+ public void run() {
+ latch.countDown();
+ }
+ });
+ SessionCommand2 customCommand = new SessionCommand2("testNoInteraction", null);
+ mSession.sendCustomCommand(customCommand, null);
+ assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ setRunnableForOnCustomCommand(mController, null);
+ }
+
+ // TODO(jaewan): Add test for service connect rejection, when we differentiate session
+ // active/inactive and connection accept/refuse
+
+ class TestVolumeProvider extends VolumeProviderCompat {
+ final CountDownLatch mLatch = new CountDownLatch(1);
+ boolean mSetVolumeToCalled;
+ boolean mAdjustVolumeCalled;
+ int mVolume;
+ int mDirection;
+
+ TestVolumeProvider(int controlType, int maxVolume, int currentVolume) {
+ super(controlType, maxVolume, currentVolume);
+ }
+
+ @Override
+ public void onSetVolumeTo(int volume) {
+ mSetVolumeToCalled = true;
+ mVolume = volume;
+ mLatch.countDown();
+ }
+
+ @Override
+ public void onAdjustVolume(int direction) {
+ mAdjustVolumeCalled = true;
+ mDirection = direction;
+ mLatch.countDown();
+ }
+ }
+
+ class TestSessionCallback extends SessionCallback {
+ CountDownLatch mLatch;
+
+ void resetLatchCount(int count) {
+ mLatch = new CountDownLatch(count);
+ }
+ }
+}
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MediaSession2TestBase.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MediaSession2TestBase.java
new file mode 100644
index 0000000..a8da0b1
--- /dev/null
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MediaSession2TestBase.java
@@ -0,0 +1,375 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.test.client;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.support.test.InstrumentationRegistry;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.media.MediaController2;
+import androidx.media.MediaController2.ControllerCallback;
+import androidx.media.MediaItem2;
+import androidx.media.MediaMetadata2;
+import androidx.media.MediaSession2.CommandButton;
+import androidx.media.SessionCommand2;
+import androidx.media.SessionCommandGroup2;
+import androidx.media.SessionToken2;
+import androidx.media.test.client.TestUtils.SyncHandler;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Base class for session test.
+ * <p>
+ * For all subclasses, all individual tests should begin with the {@link #prepareLooper()}. See
+ * {@link #prepareLooper} for details.
+ */
+abstract class MediaSession2TestBase {
+ // Expected success
+ static final int WAIT_TIME_MS = 1000;
+
+ // Expected timeout
+ static final int TIMEOUT_MS = 500;
+
+ static SyncHandler sHandler;
+ static Executor sHandlerExecutor;
+
+ Context mContext;
+ private List<MediaController2> mControllers = new ArrayList<>();
+
+ interface TestControllerInterface {
+ ControllerCallback getCallback();
+ }
+
+ interface TestControllerCallbackInterface {
+ void waitForConnect(boolean expect) throws InterruptedException;
+ void waitForDisconnect(boolean expect) throws InterruptedException;
+ void setRunnableForOnCustomCommand(Runnable runnable);
+ }
+
+ /**
+ * All tests methods should start with this.
+ * <p>
+ * MediaControllerCompat, which is wrapped by the MediaSession2, can be only created by the
+ * thread whose Looper is prepared. However, when the presubmit tests runs on the server,
+ * test runs with the {@link org.junit.internal.runners.statements.FailOnTimeout} which creates
+ * dedicated thread for running test methods while methods annotated with @After or @Before
+ * runs on the different thread. This ensures that the current Looper is prepared.
+ * <p>
+ * To address the issue .
+ */
+ public static void prepareLooper() {
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+ }
+
+ @BeforeClass
+ public static void setUpThread() {
+ synchronized (MediaSession2TestBase.class) {
+ if (sHandler != null) {
+ return;
+ }
+ prepareLooper();
+ HandlerThread handlerThread = new HandlerThread("MediaSession2TestBase");
+ handlerThread.start();
+ sHandler = new SyncHandler(handlerThread.getLooper());
+ sHandlerExecutor = new Executor() {
+ @Override
+ public void execute(Runnable runnable) {
+ SyncHandler handler;
+ synchronized (MediaSession2TestBase.class) {
+ handler = sHandler;
+ }
+ if (handler != null) {
+ handler.post(runnable);
+ }
+ }
+ };
+ }
+ }
+
+ @AfterClass
+ public static void cleanUpThread() {
+ synchronized (MediaSession2TestBase.class) {
+ if (sHandler == null) {
+ return;
+ }
+ if (Build.VERSION.SDK_INT >= 18) {
+ sHandler.getLooper().quitSafely();
+ } else {
+ sHandler.getLooper().quit();
+ }
+ sHandler = null;
+ sHandlerExecutor = null;
+ }
+ }
+
+ @CallSuper
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getTargetContext();
+ }
+
+ @CallSuper
+ public void cleanUp() throws Exception {
+ for (int i = 0; i < mControllers.size(); i++) {
+ mControllers.get(i).close();
+ }
+ }
+
+ final MediaController2 createController(SessionToken2 token) throws InterruptedException {
+ return createController(token, true, null);
+ }
+
+ final MediaController2 createController(@NonNull SessionToken2 token,
+ boolean waitForConnect, @Nullable ControllerCallback callback)
+ throws InterruptedException {
+ TestControllerInterface instance = onCreateController(token, callback);
+ if (!(instance instanceof MediaController2)) {
+ throw new RuntimeException("Test has a bug. Expected MediaController2 but returned "
+ + instance);
+ }
+ MediaController2 controller = (MediaController2) instance;
+ mControllers.add(controller);
+ if (waitForConnect) {
+ waitForConnect(controller, true);
+ }
+ return controller;
+ }
+
+ private static TestControllerCallbackInterface getTestControllerCallbackInterface(
+ MediaController2 controller) {
+ if (!(controller instanceof TestControllerInterface)) {
+ throw new RuntimeException("Test has a bug. Expected controller implemented"
+ + " TestControllerInterface but got " + controller);
+ }
+ ControllerCallback callback = ((TestControllerInterface) controller).getCallback();
+ if (!(callback instanceof TestControllerCallbackInterface)) {
+ throw new RuntimeException("Test has a bug. Expected controller with callback "
+ + " implemented TestControllerCallbackInterface but got " + controller);
+ }
+ return (TestControllerCallbackInterface) callback;
+ }
+
+ public static void waitForConnect(MediaController2 controller, boolean expected)
+ throws InterruptedException {
+ getTestControllerCallbackInterface(controller).waitForConnect(expected);
+ }
+
+ public static void waitForDisconnect(MediaController2 controller, boolean expected)
+ throws InterruptedException {
+ getTestControllerCallbackInterface(controller).waitForDisconnect(expected);
+ }
+
+ public static void setRunnableForOnCustomCommand(MediaController2 controller,
+ Runnable runnable) {
+ getTestControllerCallbackInterface(controller).setRunnableForOnCustomCommand(runnable);
+ }
+
+ TestControllerInterface onCreateController(final @NonNull SessionToken2 token,
+ @Nullable ControllerCallback callback) throws InterruptedException {
+ final ControllerCallback controllerCallback =
+ callback != null ? callback : new ControllerCallback() {};
+ final AtomicReference<TestControllerInterface> controller = new AtomicReference<>();
+ sHandler.postAndSync(new Runnable() {
+ @Override
+ public void run() {
+ // Create controller on the test handler, for changing MediaBrowserCompat's Handler
+ // Looper. Otherwise, MediaBrowserCompat will post all the commands to the handler
+ // and commands wouldn't be run if tests codes waits on the test handler.
+ controller.set(new TestMediaController(
+ mContext, token, new TestControllerCallback(controllerCallback)));
+ }
+ });
+ return controller.get();
+ }
+
+ // TODO(jaewan): (Can be Post-P): Deprecate this
+ public static class TestControllerCallback extends MediaController2.ControllerCallback
+ implements TestControllerCallbackInterface {
+ public final ControllerCallback mCallbackProxy;
+ public final CountDownLatch connectLatch = new CountDownLatch(1);
+ public final CountDownLatch disconnectLatch = new CountDownLatch(1);
+ @GuardedBy("this")
+ private Runnable mOnCustomCommandRunnable;
+
+ TestControllerCallback(@NonNull ControllerCallback callbackProxy) {
+ if (callbackProxy == null) {
+ throw new IllegalArgumentException("Callback proxy shouldn't be null. Test bug");
+ }
+ mCallbackProxy = callbackProxy;
+ }
+
+ @CallSuper
+ @Override
+ public void onConnected(MediaController2 controller, SessionCommandGroup2 commands) {
+ connectLatch.countDown();
+ }
+
+ @CallSuper
+ @Override
+ public void onDisconnected(MediaController2 controller) {
+ disconnectLatch.countDown();
+ }
+
+ @Override
+ public void waitForConnect(boolean expect) throws InterruptedException {
+ if (expect) {
+ assertTrue(connectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } else {
+ assertFalse(connectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Override
+ public void waitForDisconnect(boolean expect) throws InterruptedException {
+ if (expect) {
+ assertTrue(disconnectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } else {
+ assertFalse(disconnectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Override
+ public void onCustomCommand(MediaController2 controller, SessionCommand2 command,
+ Bundle args, ResultReceiver receiver) {
+ mCallbackProxy.onCustomCommand(controller, command, args, receiver);
+ synchronized (this) {
+ if (mOnCustomCommandRunnable != null) {
+ mOnCustomCommandRunnable.run();
+ }
+ }
+ }
+
+ @Override
+ public void onPlaybackInfoChanged(MediaController2 controller,
+ MediaController2.PlaybackInfo info) {
+ mCallbackProxy.onPlaybackInfoChanged(controller, info);
+ }
+
+ @Override
+ public void onCustomLayoutChanged(MediaController2 controller, List<CommandButton> layout) {
+ mCallbackProxy.onCustomLayoutChanged(controller, layout);
+ }
+
+ @Override
+ public void onAllowedCommandsChanged(MediaController2 controller,
+ SessionCommandGroup2 commands) {
+ mCallbackProxy.onAllowedCommandsChanged(controller, commands);
+ }
+
+ @Override
+ public void onPlayerStateChanged(MediaController2 controller, int state) {
+ mCallbackProxy.onPlayerStateChanged(controller, state);
+ }
+
+ @Override
+ public void onSeekCompleted(MediaController2 controller, long position) {
+ mCallbackProxy.onSeekCompleted(controller, position);
+ }
+
+ @Override
+ public void onPlaybackSpeedChanged(MediaController2 controller, float speed) {
+ mCallbackProxy.onPlaybackSpeedChanged(controller, speed);
+ }
+
+ @Override
+ public void onBufferingStateChanged(MediaController2 controller, MediaItem2 item,
+ int state) {
+ mCallbackProxy.onBufferingStateChanged(controller, item, state);
+ }
+
+ @Override
+ public void onError(MediaController2 controller, int errorCode, Bundle extras) {
+ mCallbackProxy.onError(controller, errorCode, extras);
+ }
+
+ @Override
+ public void onCurrentMediaItemChanged(MediaController2 controller, MediaItem2 item) {
+ mCallbackProxy.onCurrentMediaItemChanged(controller, item);
+ }
+
+ @Override
+ public void onPlaylistChanged(MediaController2 controller,
+ List<MediaItem2> list, MediaMetadata2 metadata) {
+ mCallbackProxy.onPlaylistChanged(controller, list, metadata);
+ }
+
+ @Override
+ public void onPlaylistMetadataChanged(MediaController2 controller,
+ MediaMetadata2 metadata) {
+ mCallbackProxy.onPlaylistMetadataChanged(controller, metadata);
+ }
+
+ @Override
+ public void onShuffleModeChanged(MediaController2 controller, int shuffleMode) {
+ mCallbackProxy.onShuffleModeChanged(controller, shuffleMode);
+ }
+
+ @Override
+ public void onRepeatModeChanged(MediaController2 controller, int repeatMode) {
+ mCallbackProxy.onRepeatModeChanged(controller, repeatMode);
+ }
+
+ @Override
+ public void setRunnableForOnCustomCommand(Runnable runnable) {
+ synchronized (this) {
+ mOnCustomCommandRunnable = runnable;
+ }
+ }
+
+ @Override
+ public void onRoutesInfoChanged(@NonNull MediaController2 controller,
+ @Nullable List<Bundle> routes) {
+ mCallbackProxy.onRoutesInfoChanged(controller, routes);
+ }
+ }
+
+ public class TestMediaController extends MediaController2 implements TestControllerInterface {
+ private final ControllerCallback mCallback;
+
+ TestMediaController(@NonNull Context context, @NonNull SessionToken2 token,
+ @NonNull ControllerCallback callback) {
+ super(context, token, sHandlerExecutor, callback);
+ mCallback = callback;
+ }
+
+ @Override
+ public ControllerCallback getCallback() {
+ return mCallback;
+ }
+ }
+}
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MockActivity.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MockActivity.java
new file mode 100644
index 0000000..1bcb93a
--- /dev/null
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MockActivity.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.test.client;
+
+import android.app.Activity;
+
+public class MockActivity extends Activity {
+}
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MockPlayer.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MockPlayer.java
new file mode 100644
index 0000000..7a61184
--- /dev/null
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MockPlayer.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.test.client;
+
+import androidx.annotation.NonNull;
+import androidx.collection.ArrayMap;
+import androidx.media.AudioAttributesCompat;
+import androidx.media.DataSourceDesc;
+import androidx.media.MediaPlayerInterface;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+
+/**
+ * A mock implementation of {@link MediaPlayerInterface} for testing.
+ */
+public class MockPlayer extends MediaPlayerInterface {
+ public final CountDownLatch mCountDownLatch;
+
+ public boolean mPlayCalled;
+ public boolean mPauseCalled;
+ public boolean mResetCalled;
+ public boolean mPrepareCalled;
+ public boolean mSeekToCalled;
+ public boolean mSetPlaybackSpeedCalled;
+ public long mSeekPosition;
+ public long mCurrentPosition;
+ public long mBufferedPosition;
+ public float mPlaybackSpeed = 1.0f;
+ public @PlayerState int mLastPlayerState;
+ public @BuffState int mLastBufferingState;
+ public long mDuration;
+
+ public ArrayMap<PlayerEventCallback, Executor> mCallbacks = new ArrayMap<>();
+
+ private AudioAttributesCompat mAudioAttributes;
+
+ public MockPlayer(int count) {
+ mCountDownLatch = (count > 0) ? new CountDownLatch(count) : null;
+ }
+
+ @Override
+ public void close() {
+ // no-op
+ }
+
+ @Override
+ public void reset() {
+ mResetCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void play() {
+ mPlayCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void pause() {
+ mPauseCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void prepare() {
+ mPrepareCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void seekTo(long pos) {
+ mSeekToCalled = true;
+ mSeekPosition = pos;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void skipToNext() {
+ // No-op. This skipToNext() means 'skip to next item in the setNextDataSources()'
+ }
+
+ @Override
+ public int getPlayerState() {
+ return mLastPlayerState;
+ }
+
+ @Override
+ public long getCurrentPosition() {
+ return mCurrentPosition;
+ }
+
+ @Override
+ public long getBufferedPosition() {
+ return mBufferedPosition;
+ }
+
+ @Override
+ public float getPlaybackSpeed() {
+ return mPlaybackSpeed;
+ }
+
+ @Override
+ public int getBufferingState() {
+ return mLastBufferingState;
+ }
+
+ @Override
+ public long getDuration() {
+ return mDuration;
+ }
+
+ @Override
+ public void registerPlayerEventCallback(@NonNull Executor executor,
+ @NonNull PlayerEventCallback callback) {
+ if (callback == null || executor == null) {
+ throw new IllegalArgumentException("callback=" + callback + " executor=" + executor);
+ }
+ mCallbacks.put(callback, executor);
+ }
+
+ @Override
+ public void unregisterPlayerEventCallback(@NonNull PlayerEventCallback callback) {
+ mCallbacks.remove(callback);
+ }
+
+ public void notifyPlaybackState(final int state) {
+ mLastPlayerState = state;
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ final PlayerEventCallback callback = mCallbacks.keyAt(i);
+ final Executor executor = mCallbacks.valueAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ callback.onPlayerStateChanged(MockPlayer.this, state);
+ }
+ });
+ }
+ }
+
+ public void notifyCurrentDataSourceChanged(final DataSourceDesc dsd) {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ final PlayerEventCallback callback = mCallbacks.keyAt(i);
+ final Executor executor = mCallbacks.valueAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ callback.onCurrentDataSourceChanged(MockPlayer.this, dsd);
+ }
+ });
+ }
+ }
+
+ public void notifyMediaPrepared(final DataSourceDesc dsd) {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ final PlayerEventCallback callback = mCallbacks.keyAt(i);
+ final Executor executor = mCallbacks.valueAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ callback.onMediaPrepared(MockPlayer.this, dsd);
+ }
+ });
+ }
+ }
+
+ public void notifyBufferingStateChanged(final DataSourceDesc dsd,
+ final @BuffState int buffState) {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ final PlayerEventCallback callback = mCallbacks.keyAt(i);
+ final Executor executor = mCallbacks.valueAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ callback.onBufferingStateChanged(MockPlayer.this, dsd, buffState);
+ }
+ });
+ }
+ }
+
+ public void notifyPlaybackSpeedChanged(final float speed) {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ final PlayerEventCallback callback = mCallbacks.keyAt(i);
+ final Executor executor = mCallbacks.valueAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ callback.onPlaybackSpeedChanged(MockPlayer.this, speed);
+ }
+ });
+ }
+ }
+
+ public void notifySeekCompleted(final long position) {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ final PlayerEventCallback callback = mCallbacks.keyAt(i);
+ final Executor executor = mCallbacks.valueAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ callback.onSeekCompleted(MockPlayer.this, position);
+ }
+ });
+ }
+ }
+
+ public void notifyError(int what) {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ final PlayerEventCallback callback = mCallbacks.keyAt(i);
+ final Executor executor = mCallbacks.valueAt(i);
+ // TODO: Uncomment or remove
+ //executor.execute(() -> callback.onError(null, what, 0));
+ }
+ }
+
+ @Override
+ public void setAudioAttributes(AudioAttributesCompat attributes) {
+ mAudioAttributes = attributes;
+ }
+
+ @Override
+ public AudioAttributesCompat getAudioAttributes() {
+ return mAudioAttributes;
+ }
+
+ @Override
+ public void setDataSource(@NonNull DataSourceDesc dsd) {
+ // TODO: Implement this
+ }
+
+ @Override
+ public void setNextDataSource(@NonNull DataSourceDesc dsd) {
+ // TODO: Implement this
+ }
+
+ @Override
+ public void setNextDataSources(@NonNull List<DataSourceDesc> dsds) {
+ // TODO: Implement this
+ }
+
+ @Override
+ public DataSourceDesc getCurrentDataSource() {
+ // TODO: Implement this
+ return null;
+ }
+
+ @Override
+ public void loopCurrent(boolean loop) {
+ // TODO: implement this
+ }
+
+ @Override
+ public void setPlaybackSpeed(float speed) {
+ mSetPlaybackSpeedCalled = true;
+ mPlaybackSpeed = speed;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void setPlayerVolume(float volume) {
+ // TODO: implement this
+ }
+
+ @Override
+ public float getPlayerVolume() {
+ // TODO: implement this
+ return -1;
+ }
+}
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MockPlaylistAgent.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MockPlaylistAgent.java
new file mode 100644
index 0000000..1ed6cd7
--- /dev/null
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/MockPlaylistAgent.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.test.client;
+
+import androidx.media.MediaItem2;
+import androidx.media.MediaMetadata2;
+import androidx.media.MediaPlaylistAgent;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * A mock implementation of {@link MediaPlaylistAgent} for testing.
+ * <p>
+ * Do not use mockito for {@link MediaPlaylistAgent}. Instead, use this.
+ * Mocks created from mockito should not be shared across different threads.
+ */
+public class MockPlaylistAgent extends MediaPlaylistAgent {
+ public final CountDownLatch mCountDownLatch = new CountDownLatch(1);
+
+ public List<MediaItem2> mPlaylist;
+ public MediaMetadata2 mMetadata;
+ public MediaItem2 mCurrentMediaItem;
+ public MediaItem2 mItem;
+ public int mIndex = -1;
+ public @RepeatMode int mRepeatMode = -1;
+ public @ShuffleMode int mShuffleMode = -1;
+
+ public boolean mSetPlaylistCalled;
+ public boolean mUpdatePlaylistMetadataCalled;
+ public boolean mAddPlaylistItemCalled;
+ public boolean mRemovePlaylistItemCalled;
+ public boolean mReplacePlaylistItemCalled;
+ public boolean mSkipToPlaylistItemCalled;
+ public boolean mSkipToPreviousItemCalled;
+ public boolean mSkipToNextItemCalled;
+ public boolean mSetRepeatModeCalled;
+ public boolean mSetShuffleModeCalled;
+
+ @Override
+ public List<MediaItem2> getPlaylist() {
+ return mPlaylist;
+ }
+
+ @Override
+ public void setPlaylist(List<MediaItem2> list, MediaMetadata2 metadata) {
+ mSetPlaylistCalled = true;
+ mPlaylist = list;
+ mMetadata = metadata;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public MediaMetadata2 getPlaylistMetadata() {
+ return mMetadata;
+ }
+
+ @Override
+ public void updatePlaylistMetadata(MediaMetadata2 metadata) {
+ mUpdatePlaylistMetadataCalled = true;
+ mMetadata = metadata;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public MediaItem2 getCurrentMediaItem() {
+ return mCurrentMediaItem;
+ }
+
+ @Override
+ public void addPlaylistItem(int index, MediaItem2 item) {
+ mAddPlaylistItemCalled = true;
+ mIndex = index;
+ mItem = item;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void removePlaylistItem(MediaItem2 item) {
+ mRemovePlaylistItemCalled = true;
+ mItem = item;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void replacePlaylistItem(int index, MediaItem2 item) {
+ mReplacePlaylistItemCalled = true;
+ mIndex = index;
+ mItem = item;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void skipToPlaylistItem(MediaItem2 item) {
+ mSkipToPlaylistItemCalled = true;
+ mItem = item;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void skipToPreviousItem() {
+ mSkipToPreviousItemCalled = true;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void skipToNextItem() {
+ mSkipToNextItemCalled = true;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public int getRepeatMode() {
+ return mRepeatMode;
+ }
+
+ @Override
+ public void setRepeatMode(int repeatMode) {
+ mSetRepeatModeCalled = true;
+ mRepeatMode = repeatMode;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public int getShuffleMode() {
+ return mShuffleMode;
+ }
+
+ @Override
+ public void setShuffleMode(int shuffleMode) {
+ mSetShuffleModeCalled = true;
+ mShuffleMode = shuffleMode;
+ mCountDownLatch.countDown();
+ }
+}
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/TestHelper.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/TestHelper.java
new file mode 100644
index 0000000..76c7758
--- /dev/null
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/TestHelper.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.test.client;
+
+import static androidx.media.test.lib.TestHelperUtil.ACTION_TEST_HELPER;
+import static androidx.media.test.lib.TestHelperUtil.SERVICE_TEST_HELPER_COMPONENT_NAME;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.support.mediacompat.testlib.ITestHelperForServiceApp;
+import android.util.Log;
+
+import androidx.media.MediaSession2;
+import androidx.media.SessionToken2;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Interacts with service app's TestHelperService.
+ */
+public class TestHelper {
+
+ private static final String TAG = "TestHelper";
+
+ private Context mContext;
+ private ServiceConnection mServiceConnection;
+ private ITestHelperForServiceApp mBinder;
+
+ private CountDownLatch mCountDownLatch;
+
+ public TestHelper(Context context) {
+ mContext = context;
+ mCountDownLatch = new CountDownLatch(1);
+ }
+
+ /**
+ * Connects to service app's TestHelperService. Should NOT be called in main thread.
+ *
+ * @return true if connected successfully, false if failed to connect.
+ */
+ public boolean connect(int timeoutMs) {
+ mServiceConnection = new MyServiceConnection();
+
+ final Intent intent = new Intent(ACTION_TEST_HELPER);
+ intent.setComponent(SERVICE_TEST_HELPER_COMPONENT_NAME);
+
+ boolean bound = false;
+ try {
+ bound = mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
+ } catch (Exception ex) {
+ Log.e(TAG, "Failed binding to the test helper service of service app");
+ }
+
+ if (bound) {
+ try {
+ mCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException ex) {
+ Log.e(TAG, "InterruptedException while waiting for onServiceConnected.", ex);
+ }
+ }
+ return mBinder != null;
+ }
+
+ /**
+ * Create a session2 in the service app, and gets its token.
+ *
+ * @return A {@link SessionToken2} object if succeeded, {@code null} if failed.
+ */
+ public SessionToken2 getSessionToken2(String testName) {
+ SessionToken2 token = null;
+ try {
+ Bundle bundle = mBinder.getSessionToken2(testName);
+ if (bundle != null) {
+ bundle.setClassLoader(MediaSession2.class.getClassLoader());
+ }
+ token = SessionToken2.fromBundle(bundle);
+ } catch (RemoteException ex) {
+ Log.e(TAG, "Failed to get session token. testName=" + testName);
+ }
+ return token;
+ }
+
+ // These methods will run on main thread.
+ class MyServiceConnection implements ServiceConnection {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ Log.d(TAG, "Connected to service app's TestHelperService.");
+ mBinder = ITestHelperForServiceApp.Stub.asInterface(service);
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ Log.d(TAG, "Disconnected from the service.");
+ }
+ }
+}
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/TestHelperTest.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/TestHelperTest.java
new file mode 100644
index 0000000..af5b1ed
--- /dev/null
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/TestHelperTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.test.client;
+
+import static android.support.mediacompat.testlib.VersionConstants.KEY_SERVICE_VERSION;
+import static android.support.mediacompat.testlib.VersionConstants.VERSION_TOT;
+import static android.support.mediacompat.testlib.util.IntentUtil.SERVICE_PACKAGE_NAME;
+import static android.support.test.InstrumentationRegistry.getArguments;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.os.Looper;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.media.MediaController2;
+import androidx.media.SessionToken2;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.Executor;
+
+/** Test {@link TestHelper}. */
+@RunWith(AndroidJUnit4.class)
+public class TestHelperTest {
+ private static final int TIME_OUT_MS = 3000;
+
+ private Context mContext;
+ private TestHelper mTestHelper;
+ private String mServiceVersion;
+
+ @Before
+ public void setUp() {
+ // The version of the service app is provided through the instrumentation arguments.
+ mServiceVersion = getArguments().getString(KEY_SERVICE_VERSION, "");
+ if (!VERSION_TOT.equals(mServiceVersion)) {
+ return;
+ }
+
+ mContext = InstrumentationRegistry.getTargetContext();
+ mTestHelper = new TestHelper(mContext);
+ boolean connected = mTestHelper.connect(TIME_OUT_MS);
+ if (!connected) {
+ fail("Failed to connect to Test helper service.");
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testGettingToken() {
+ if (!VERSION_TOT.equals(mServiceVersion)) {
+ return;
+ }
+ SessionToken2 token = mTestHelper.getSessionToken2("testGettingToken");
+ assertNotNull(token);
+ assertEquals(SERVICE_PACKAGE_NAME, token.getPackageName());
+ }
+
+ @Test
+ @SmallTest
+ public void testCreatingController() {
+ if (!VERSION_TOT.equals(mServiceVersion)) {
+ return;
+ }
+ Looper.prepare();
+ SessionToken2 token = mTestHelper.getSessionToken2("testCreatingController");
+ assertNotNull(token);
+ MediaController2 controller = new MediaController2(mContext, token, new Executor() {
+ @Override
+ public void execute(Runnable command) {
+ command.run();
+ }
+ }, new MediaController2.ControllerCallback() {});
+ }
+}
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/TestServiceRegistry.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/TestServiceRegistry.java
new file mode 100644
index 0000000..8c87073
--- /dev/null
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/TestServiceRegistry.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.test.client;
+
+import static org.junit.Assert.fail;
+
+import android.os.Handler;
+
+import androidx.annotation.GuardedBy;
+import androidx.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback;
+import androidx.media.MediaSessionService2;
+import androidx.media.test.client.TestUtils.SyncHandler;
+
+/**
+ * Keeps the instance of currently running {@link MockMediaSessionService2}. And also provides
+ * a way to control them in one place.
+ * <p>
+ * It only support only one service at a time.
+ */
+public class TestServiceRegistry {
+ @GuardedBy("TestServiceRegistry.class")
+ private static TestServiceRegistry sInstance;
+ @GuardedBy("TestServiceRegistry.class")
+ private MediaSessionService2 mService;
+ @GuardedBy("TestServiceRegistry.class")
+ private SyncHandler mHandler;
+ @GuardedBy("TestServiceRegistry.class")
+ private MediaLibrarySessionCallback mSessionCallback;
+ @GuardedBy("TestServiceRegistry.class")
+ private SessionServiceCallback mSessionServiceCallback;
+
+ /**
+ * Callback for session service's lifecyle (onCreate() / onDestroy())
+ */
+ public interface SessionServiceCallback {
+ void onCreated();
+ void onDestroyed();
+ }
+
+ public static TestServiceRegistry getInstance() {
+ synchronized (TestServiceRegistry.class) {
+ if (sInstance == null) {
+ sInstance = new TestServiceRegistry();
+ }
+ return sInstance;
+ }
+ }
+
+ public void setHandler(Handler handler) {
+ synchronized (TestServiceRegistry.class) {
+ mHandler = new SyncHandler(handler.getLooper());
+ }
+ }
+
+ public Handler getHandler() {
+ synchronized (TestServiceRegistry.class) {
+ return mHandler;
+ }
+ }
+
+ public void setSessionServiceCallback(SessionServiceCallback sessionServiceCallback) {
+ synchronized (TestServiceRegistry.class) {
+ mSessionServiceCallback = sessionServiceCallback;
+ }
+ }
+
+ public void setSessionCallback(MediaLibrarySessionCallback sessionCallback) {
+ synchronized (TestServiceRegistry.class) {
+ mSessionCallback = sessionCallback;
+ }
+ }
+
+ public MediaLibrarySessionCallback getSessionCallback() {
+ synchronized (TestServiceRegistry.class) {
+ return mSessionCallback;
+ }
+ }
+
+ public void setServiceInstance(MediaSessionService2 service) {
+ synchronized (TestServiceRegistry.class) {
+ if (mService != null) {
+ fail("Previous service instance is still running. Clean up manually to ensure"
+ + " previoulsy running service doesn't break current test");
+ }
+ mService = service;
+ if (mSessionServiceCallback != null) {
+ mSessionServiceCallback.onCreated();
+ }
+ }
+ }
+
+ public MediaSessionService2 getServiceInstance() {
+ synchronized (TestServiceRegistry.class) {
+ return mService;
+ }
+ }
+
+ public void cleanUp() {
+ synchronized (TestServiceRegistry.class) {
+ if (mService != null) {
+ // TODO(jaewan): Remove this, and override SessionService#onDestroy() to do this
+ mService.getSession().close();
+ // stopSelf() would not kill service while the binder connection established by
+ // bindService() exists, and close() above will do the job instead.
+ // So stopSelf() isn't really needed, but just for sure.
+ mService.stopSelf();
+ mService = null;
+ }
+ if (mHandler != null) {
+ mHandler.removeCallbacksAndMessages(null);
+ }
+ mSessionCallback = null;
+ if (mSessionServiceCallback != null) {
+ mSessionServiceCallback.onDestroyed();
+ mSessionServiceCallback = null;
+ }
+ }
+ }
+}
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/TestUtils.java b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/TestUtils.java
new file mode 100644
index 0000000..3cd7f68
--- /dev/null
+++ b/media/version-compat-tests/current/client/src/androidTest/java/androidx/media/test/client/TestUtils.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.test.client;
+
+import static org.junit.Assert.assertTrue;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+
+import androidx.core.util.ObjectsCompat;
+import androidx.media.DataSourceDesc;
+import androidx.media.MediaItem2;
+import androidx.media.MediaMetadata2;
+
+import java.io.FileDescriptor;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Utilities for tests.
+ */
+public final class TestUtils {
+ private static final int WAIT_TIME_MS = 1000;
+ private static final int WAIT_SERVICE_TIME_MS = 5000;
+
+ // Temporaily commenting out, since we don't have the Mock services yet.
+// /**
+// * Finds the session with id in this test package.
+// *
+// * @param context
+// * @param id
+// * @return
+// */
+// public static SessionToken2 getServiceToken(Context context, String id) {
+// switch (id) {
+// case MockMediaSessionService2.ID:
+// return new SessionToken2(context, new ComponentName(
+// context.getPackageName(), MockMediaSessionService2.class.getName()));
+// case MockMediaLibraryService2.ID:
+// return new SessionToken2(context, new ComponentName(
+// context.getPackageName(), MockMediaLibraryService2.class.getName()));
+// }
+// fail("Unknown id=" + id);
+// return null;
+// }
+
+ /**
+ * Compares contents of two bundles.
+ *
+ * @param a a bundle
+ * @param b another bundle
+ * @return {@code true} if two bundles are the same. {@code false} otherwise. This may be
+ * incorrect if any bundle contains a bundle.
+ */
+ public static boolean equals(Bundle a, Bundle b) {
+ return contains(a, b) && contains(b, a);
+ }
+
+ /**
+ * Checks whether a Bundle contains another bundle.
+ *
+ * @param a a bundle
+ * @param b another bundle
+ * @return {@code true} if a contains b. {@code false} otherwise. This may be incorrect if any
+ * bundle contains a bundle.
+ */
+ public static boolean contains(Bundle a, Bundle b) {
+ if (a == b) {
+ return true;
+ }
+ if (a == null || b == null) {
+ return b == null;
+ }
+ if (!a.keySet().containsAll(b.keySet())) {
+ return false;
+ }
+ for (String key : b.keySet()) {
+ if (!ObjectsCompat.equals(a.get(key), b.get(key))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Create a playlist for testing purpose
+ * <p>
+ * Caller's method name will be used for prefix of each media item's media id.
+ *
+ * @param size list size
+ * @return the newly created playlist
+ */
+ public static List<MediaItem2> createPlaylist(int size) {
+ final List<MediaItem2> list = new ArrayList<>();
+ String caller = Thread.currentThread().getStackTrace()[1].getMethodName();
+ for (int i = 0; i < size; i++) {
+ list.add(new MediaItem2.Builder(MediaItem2.FLAG_PLAYABLE)
+ .setMediaId(caller + "_item_" + (size + 1))
+ .setDataSourceDesc(createDSD()).build());
+ }
+ return list;
+ }
+
+ /**
+ * Create a media item with the metadata for testing purpose.
+ *
+ * @return the newly created media item
+ * @see #createMetadata()
+ */
+ public static MediaItem2 createMediaItemWithMetadata() {
+ return new MediaItem2.Builder(MediaItem2.FLAG_PLAYABLE)
+ .setMetadata(createMetadata()).setDataSourceDesc(createDSD()).build();
+ }
+
+ /**
+ * Create a media metadata for testing purpose.
+ * <p>
+ * Caller's method name will be used for the media id.
+ *
+ * @return the newly created media item
+ */
+ public static MediaMetadata2 createMetadata() {
+ String mediaId = Thread.currentThread().getStackTrace()[1].getMethodName();
+ return new MediaMetadata2.Builder()
+ .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID, mediaId).build();
+ }
+
+ private static DataSourceDesc createDSD() {
+ return new DataSourceDesc.Builder().setDataSource(new FileDescriptor()).build();
+ }
+
+ /**
+ * Create a bundle for testing purpose.
+ *
+ * @return the newly created bundle.
+ */
+ public static Bundle createTestBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putString("test_key", "test_value");
+ return bundle;
+ }
+
+ /**
+ * Handler that always waits until the Runnable finishes.
+ */
+ public static class SyncHandler extends Handler {
+ public SyncHandler(Looper looper) {
+ super(looper);
+ }
+
+ public void postAndSync(final Runnable runnable) throws InterruptedException {
+ if (getLooper() == Looper.myLooper()) {
+ runnable.run();
+ } else {
+ final CountDownLatch latch = new CountDownLatch(1);
+ post(new Runnable() {
+ @Override
+ public void run() {
+ runnable.run();
+ latch.countDown();
+ }
+ });
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+ }
+}
diff --git a/media/version-compat-tests/current/service/src/androidTest/AndroidManifest.xml b/media/version-compat-tests/current/service/src/androidTest/AndroidManifest.xml
index 13c22ae..0ca78fc 100644
--- a/media/version-compat-tests/current/service/src/androidTest/AndroidManifest.xml
+++ b/media/version-compat-tests/current/service/src/androidTest/AndroidManifest.xml
@@ -41,5 +41,12 @@
<action android:name="android.media.browse.MediaBrowserService"/>
</intent-filter>
</service>
+
+ <service android:name="androidx.media.test.service.TestHelperService">
+ <intent-filter>
+ <!-- Keep sync with TestHelperUtil.java -->
+ <action android:name="androidx.media.test.action.TEST_HELPER"/>
+ </intent-filter>
+ </service>
</application>
</manifest>
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java b/media/version-compat-tests/current/service/src/androidTest/java/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java
index 3519f2f..82e55ed 100644
--- a/media/version-compat-tests/current/service/src/androidTest/java/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java
+++ b/media/version-compat-tests/current/service/src/androidTest/java/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java
@@ -85,6 +85,7 @@
import android.os.Parcel;
import android.os.ResultReceiver;
import android.os.SystemClock;
+import android.support.mediacompat.testlib.util.PollingCheck;
import android.support.test.filters.MediumTest;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
@@ -722,7 +723,80 @@
@Test
@SmallTest
- public void testVolumeControl() throws Exception {
+ public void testSetVolumeWithLocalVolume() throws Exception {
+ if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
+ // This test is not eligible for this device.
+ return;
+ }
+
+ // Here, we intentionally choose STREAM_ALARM in order not to consider
+ // 'Do Not Disturb' or 'Volume limit'.
+ final int stream = AudioManager.STREAM_ALARM;
+ final int maxVolume = mAudioManager.getStreamMaxVolume(stream);
+ final int minVolume = 0;
+ if (maxVolume <= minVolume) {
+ return;
+ }
+
+ mSession.setPlaybackToLocal(stream);
+
+ final int originalVolume = mAudioManager.getStreamVolume(stream);
+ final int targetVolume = originalVolume == minVolume
+ ? originalVolume + 1 : originalVolume - 1;
+
+ callMediaControllerMethod(SET_VOLUME_TO, targetVolume, getContext(),
+ mSession.getSessionToken());
+ new PollingCheck(TIME_OUT_MS) {
+ @Override
+ protected boolean check() {
+ return targetVolume == mAudioManager.getStreamVolume(stream);
+ }
+ }.run();
+
+ // Set back to original volume.
+ mAudioManager.setStreamVolume(stream, originalVolume, 0 /* flags */);
+ }
+
+ @Test
+ @SmallTest
+ public void testAdjustVolumeWithLocalVolume() throws Exception {
+ if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
+ // This test is not eligible for this device.
+ return;
+ }
+
+ // Here, we intentionally choose STREAM_ALARM in order not to consider
+ // 'Do Not Disturb' or 'Volume limit'.
+ final int stream = AudioManager.STREAM_ALARM;
+ final int maxVolume = mAudioManager.getStreamMaxVolume(stream);
+ final int minVolume = 0;
+ if (maxVolume <= minVolume) {
+ return;
+ }
+
+ mSession.setPlaybackToLocal(stream);
+
+ final int originalVolume = mAudioManager.getStreamVolume(stream);
+ final int direction = originalVolume == minVolume
+ ? AudioManager.ADJUST_RAISE : AudioManager.ADJUST_LOWER;
+ final int targetVolume = originalVolume + direction;
+
+ callMediaControllerMethod(ADJUST_VOLUME, direction, getContext(),
+ mSession.getSessionToken());
+ new PollingCheck(TIME_OUT_MS) {
+ @Override
+ protected boolean check() {
+ return targetVolume == mAudioManager.getStreamVolume(stream);
+ }
+ }.run();
+
+ // Set back to original volume.
+ mAudioManager.setStreamVolume(stream, originalVolume, 0 /* flags */);
+ }
+
+ @Test
+ @SmallTest
+ public void testRemoteVolumeControl() throws Exception {
if (android.os.Build.VERSION.SDK_INT < 27) {
// This test causes an Exception on System UI in API < 27.
return;
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/MockPlayer.java b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/MockPlayer.java
new file mode 100644
index 0000000..14f1a96
--- /dev/null
+++ b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/MockPlayer.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.test.service;
+
+import androidx.annotation.NonNull;
+import androidx.collection.ArrayMap;
+import androidx.media.AudioAttributesCompat;
+import androidx.media.DataSourceDesc;
+import androidx.media.MediaPlayerInterface;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+
+/**
+ * A mock implementation of {@link MediaPlayerInterface} for testing.
+ */
+public class MockPlayer extends MediaPlayerInterface {
+ public final CountDownLatch mCountDownLatch;
+
+ public boolean mPlayCalled;
+ public boolean mPauseCalled;
+ public boolean mResetCalled;
+ public boolean mPrepareCalled;
+ public boolean mSeekToCalled;
+ public boolean mSetPlaybackSpeedCalled;
+ public long mSeekPosition;
+ public long mCurrentPosition;
+ public long mBufferedPosition;
+ public float mPlaybackSpeed = 1.0f;
+ public @PlayerState int mLastPlayerState;
+ public @BuffState int mLastBufferingState;
+ public long mDuration;
+
+ public ArrayMap<PlayerEventCallback, Executor> mCallbacks = new ArrayMap<>();
+
+ private AudioAttributesCompat mAudioAttributes;
+
+ public MockPlayer(int count) {
+ mCountDownLatch = (count > 0) ? new CountDownLatch(count) : null;
+ }
+
+ @Override
+ public void close() {
+ // no-op
+ }
+
+ @Override
+ public void reset() {
+ mResetCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void play() {
+ mPlayCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void pause() {
+ mPauseCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void prepare() {
+ mPrepareCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void seekTo(long pos) {
+ mSeekToCalled = true;
+ mSeekPosition = pos;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void skipToNext() {
+ // No-op. This skipToNext() means 'skip to next item in the setNextDataSources()'
+ }
+
+ @Override
+ public int getPlayerState() {
+ return mLastPlayerState;
+ }
+
+ @Override
+ public long getCurrentPosition() {
+ return mCurrentPosition;
+ }
+
+ @Override
+ public long getBufferedPosition() {
+ return mBufferedPosition;
+ }
+
+ @Override
+ public float getPlaybackSpeed() {
+ return mPlaybackSpeed;
+ }
+
+ @Override
+ public int getBufferingState() {
+ return mLastBufferingState;
+ }
+
+ @Override
+ public long getDuration() {
+ return mDuration;
+ }
+
+ @Override
+ public void registerPlayerEventCallback(@NonNull Executor executor,
+ @NonNull PlayerEventCallback callback) {
+ if (callback == null || executor == null) {
+ throw new IllegalArgumentException("callback=" + callback + " executor=" + executor);
+ }
+ mCallbacks.put(callback, executor);
+ }
+
+ @Override
+ public void unregisterPlayerEventCallback(@NonNull PlayerEventCallback callback) {
+ mCallbacks.remove(callback);
+ }
+
+ public void notifyPlaybackState(final int state) {
+ mLastPlayerState = state;
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ final PlayerEventCallback callback = mCallbacks.keyAt(i);
+ final Executor executor = mCallbacks.valueAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ callback.onPlayerStateChanged(MockPlayer.this, state);
+ }
+ });
+ }
+ }
+
+ public void notifyCurrentDataSourceChanged(final DataSourceDesc dsd) {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ final PlayerEventCallback callback = mCallbacks.keyAt(i);
+ final Executor executor = mCallbacks.valueAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ callback.onCurrentDataSourceChanged(MockPlayer.this, dsd);
+ }
+ });
+ }
+ }
+
+ public void notifyMediaPrepared(final DataSourceDesc dsd) {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ final PlayerEventCallback callback = mCallbacks.keyAt(i);
+ final Executor executor = mCallbacks.valueAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ callback.onMediaPrepared(MockPlayer.this, dsd);
+ }
+ });
+ }
+ }
+
+ public void notifyBufferingStateChanged(final DataSourceDesc dsd,
+ final @BuffState int buffState) {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ final PlayerEventCallback callback = mCallbacks.keyAt(i);
+ final Executor executor = mCallbacks.valueAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ callback.onBufferingStateChanged(MockPlayer.this, dsd, buffState);
+ }
+ });
+ }
+ }
+
+ public void notifyPlaybackSpeedChanged(final float speed) {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ final PlayerEventCallback callback = mCallbacks.keyAt(i);
+ final Executor executor = mCallbacks.valueAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ callback.onPlaybackSpeedChanged(MockPlayer.this, speed);
+ }
+ });
+ }
+ }
+
+ public void notifySeekCompleted(final long position) {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ final PlayerEventCallback callback = mCallbacks.keyAt(i);
+ final Executor executor = mCallbacks.valueAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ callback.onSeekCompleted(MockPlayer.this, position);
+ }
+ });
+ }
+ }
+
+ public void notifyError(int what) {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ final PlayerEventCallback callback = mCallbacks.keyAt(i);
+ final Executor executor = mCallbacks.valueAt(i);
+ // TODO: Uncomment or remove
+ //executor.execute(() -> callback.onError(null, what, 0));
+ }
+ }
+
+ @Override
+ public void setAudioAttributes(AudioAttributesCompat attributes) {
+ mAudioAttributes = attributes;
+ }
+
+ @Override
+ public AudioAttributesCompat getAudioAttributes() {
+ return mAudioAttributes;
+ }
+
+ @Override
+ public void setDataSource(@NonNull DataSourceDesc dsd) {
+ // TODO: Implement this
+ }
+
+ @Override
+ public void setNextDataSource(@NonNull DataSourceDesc dsd) {
+ // TODO: Implement this
+ }
+
+ @Override
+ public void setNextDataSources(@NonNull List<DataSourceDesc> dsds) {
+ // TODO: Implement this
+ }
+
+ @Override
+ public DataSourceDesc getCurrentDataSource() {
+ // TODO: Implement this
+ return null;
+ }
+
+ @Override
+ public void loopCurrent(boolean loop) {
+ // TODO: implement this
+ }
+
+ @Override
+ public void setPlaybackSpeed(float speed) {
+ mSetPlaybackSpeedCalled = true;
+ mPlaybackSpeed = speed;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void setPlayerVolume(float volume) {
+ // TODO: implement this
+ }
+
+ @Override
+ public float getPlayerVolume() {
+ // TODO: implement this
+ return -1;
+ }
+}
diff --git a/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/TestHelperService.java b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/TestHelperService.java
new file mode 100644
index 0000000..8df40b9
--- /dev/null
+++ b/media/version-compat-tests/current/service/src/androidTest/java/androidx/media/test/service/TestHelperService.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.test.service;
+
+import static androidx.media.test.lib.TestHelperUtil.ACTION_TEST_HELPER;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.support.mediacompat.testlib.ITestHelperForServiceApp;
+
+import androidx.media.MediaSession2;
+
+/**
+ * A Service that creates {@link MediaSession2} and calls its methods according to the client app's
+ * requests.
+ */
+public class TestHelperService extends Service {
+
+ MediaSession2 mSession2;
+ ServiceBinder mBinder;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mBinder = new ServiceBinder();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (ACTION_TEST_HELPER.equals(intent.getAction())) {
+ return mBinder;
+ }
+ return null;
+ }
+
+ private class ServiceBinder extends ITestHelperForServiceApp.Stub {
+ @Override
+ public Bundle getSessionToken2(String testName) throws RemoteException {
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+
+ // TODO: Create the right session according to testName, and return its token here.
+ mSession2 = new MediaSession2.Builder(TestHelperService.this)
+ .setPlayer(new MockPlayer(0))
+ .build();
+ return mSession2.getToken().toBundle();
+ }
+
+ @Override
+ public void callMediaSession2Method(int method, Bundle args) throws RemoteException {
+ // TODO: Call appropriate method (mSession2.~~~)
+ }
+
+ // TODO: call(MediaPlayerBase/Agent)Method may be also needed.
+ // If so, do not create mPlayer/mAgent, but get them from mSession.getPlayer()/getAgent().
+ }
+}
diff --git a/media/version-compat-tests/lib/src/main/aidl/android/support/mediacompat/testlib/ITestHelperForServiceApp.aidl b/media/version-compat-tests/lib/src/main/aidl/android/support/mediacompat/testlib/ITestHelperForServiceApp.aidl
new file mode 100644
index 0000000..d12e8ca
--- /dev/null
+++ b/media/version-compat-tests/lib/src/main/aidl/android/support/mediacompat/testlib/ITestHelperForServiceApp.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.mediacompat.testlib;
+
+import android.os.Bundle;
+
+interface ITestHelperForServiceApp {
+
+ Bundle getSessionToken2(String testName);
+ void callMediaSession2Method(int method, in Bundle args);
+}
diff --git a/media/version-compat-tests/lib/src/main/java/androidx/media/test/lib/TestHelperUtil.java b/media/version-compat-tests/lib/src/main/java/androidx/media/test/lib/TestHelperUtil.java
new file mode 100644
index 0000000..9cc68b5
--- /dev/null
+++ b/media/version-compat-tests/lib/src/main/java/androidx/media/test/lib/TestHelperUtil.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.test.lib;
+
+import android.content.ComponentName;
+import android.support.mediacompat.testlib.util.IntentUtil;
+
+/**
+ * Methods and constants used for calling methods between client and service apps by using
+ * TestHelper/TestHelperService.
+ */
+public class TestHelperUtil {
+ public static final ComponentName SERVICE_TEST_HELPER_COMPONENT_NAME = new ComponentName(
+ IntentUtil.SERVICE_PACKAGE_NAME, "androidx.media.test.service.TestHelperService");
+
+ public static final String ACTION_TEST_HELPER = "androidx.media.action.test.TEST_HELPER";
+
+ private TestHelperUtil() {
+ }
+}
diff --git a/paging/runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.java b/paging/runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.java
index f30163d..0033665 100644
--- a/paging/runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.java
+++ b/paging/runtime/src/main/java/androidx/paging/AsyncPagedListDiffer.java
@@ -150,7 +150,7 @@
public AsyncPagedListDiffer(@NonNull RecyclerView.Adapter adapter,
@NonNull DiffUtil.ItemCallback<T> diffCallback) {
mUpdateCallback = new AdapterListUpdateCallback(adapter);
- mConfig = new AsyncDifferConfig.Builder<T>(diffCallback).build();
+ mConfig = new AsyncDifferConfig.Builder<>(diffCallback).build();
}
@SuppressWarnings("WeakerAccess")
@@ -227,6 +227,7 @@
*
* @param pagedList The new PagedList.
*/
+ @SuppressWarnings("ReferenceEquality")
public void submitList(final PagedList<T> pagedList) {
if (pagedList != null) {
if (mPagedList == null && mSnapshot == null) {
diff --git a/palette/ktx/OWNERS b/palette/ktx/OWNERS
new file mode 100644
index 0000000..e450f4c
--- /dev/null
+++ b/palette/ktx/OWNERS
@@ -0,0 +1 @@
+jakew@google.com
diff --git a/palette/ktx/build.gradle b/palette/ktx/build.gradle
new file mode 100644
index 0000000..e5d5d43
--- /dev/null
+++ b/palette/ktx/build.gradle
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ buildTypes {
+ debug {
+ testCoverageEnabled = false // Breaks Kotlin compiler.
+ }
+ }
+}
+
+dependencies {
+ api(project(":palette"))
+ api(KOTLIN_STDLIB)
+ androidTestImplementation(JUNIT)
+ androidTestImplementation(TEST_RUNNER_TMP, libs.exclude_for_espresso)
+}
+
+supportLibrary {
+ name = "Palette Kotlin Extensions"
+ publish = true
+ mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+ mavenGroup = LibraryGroups.PALETTE
+ inceptionYear = "2018"
+ description = "Kotlin extensions for 'palette' artifact"
+}
diff --git a/palette/ktx/src/androidTest/java/androidx/palette/graphics/PaletteTest.kt b/palette/ktx/src/androidTest/java/androidx/palette/graphics/PaletteTest.kt
new file mode 100644
index 0000000..c108493
--- /dev/null
+++ b/palette/ktx/src/androidTest/java/androidx/palette/graphics/PaletteTest.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.palette.graphics
+
+import android.graphics.Bitmap
+import android.graphics.Bitmap.Config.ARGB_8888
+import android.graphics.Canvas
+import android.graphics.Color.RED
+import androidx.palette.graphics.Target.VIBRANT
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertSame
+import org.junit.Test
+
+class PaletteTest {
+ @Test fun bitmapBuild() {
+ val bitmap = Bitmap.createBitmap(10, 10, ARGB_8888)
+ // There's no easy way to test that the palette was created from our Bitmap.
+ assertNotNull(bitmap.buildPalette())
+ }
+
+ @Test fun operatorGet() {
+ val bitmap = Bitmap.createBitmap(10, 10, ARGB_8888).apply {
+ Canvas(this).drawColor(RED)
+ }
+ val palette = Palette.from(bitmap).generate()
+ assertSame(palette.getSwatchForTarget(VIBRANT), palette[VIBRANT])
+ }
+}
diff --git a/palette/ktx/src/main/AndroidManifest.xml b/palette/ktx/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..f25fda5
--- /dev/null
+++ b/palette/ktx/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2018 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest package="androidx.palette.ktx"/>
diff --git a/palette/ktx/src/main/java/androidx/palette/graphics/Palette.kt b/palette/ktx/src/main/java/androidx/palette/graphics/Palette.kt
new file mode 100644
index 0000000..58da09a
--- /dev/null
+++ b/palette/ktx/src/main/java/androidx/palette/graphics/Palette.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package androidx.palette.graphics
+
+import android.graphics.Bitmap
+
+/**
+ * Create a [Palette.Builder] from this bitmap.
+ *
+ * @see Palette.from
+ */
+inline fun Bitmap.buildPalette() = Palette.Builder(this)
+
+/**
+ * Returns the selected swatch for the given target from the palette, or `null` if one
+ * could not be found.
+ *
+ * @see Palette.getSwatchForTarget
+ */
+inline operator fun Palette.get(target: Target): Palette.Swatch? = getSwatchForTarget(target)
diff --git a/persistence/db/ktx/OWNERS b/persistence/db/ktx/OWNERS
new file mode 100644
index 0000000..e450f4c
--- /dev/null
+++ b/persistence/db/ktx/OWNERS
@@ -0,0 +1 @@
+jakew@google.com
diff --git a/persistence/db/ktx/build.gradle b/persistence/db/ktx/build.gradle
new file mode 100644
index 0000000..676445b
--- /dev/null
+++ b/persistence/db/ktx/build.gradle
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.SupportLibraryExtension
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ api(project(":sqlite:sqlite"))
+ api(KOTLIN_STDLIB)
+
+ testImplementation(JUNIT)
+ testImplementation(MOCKITO_CORE)
+}
+
+supportLibrary {
+ name = "Android DB KTX"
+ publish = true
+ mavenVersion = LibraryVersions.ROOM
+ mavenGroup = LibraryGroups.PERSISTENCE
+ inceptionYear = "2018"
+ description = "Kotlin extensions for DB"
+ url = SupportLibraryExtension.ARCHITECTURE_URL
+}
diff --git a/persistence/db/ktx/src/main/AndroidManifest.xml b/persistence/db/ktx/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..f4d66ba
--- /dev/null
+++ b/persistence/db/ktx/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<!--
+ ~ Copyright (C) 2018 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest package="androidx.sqlite.db.ktx"/>
diff --git a/persistence/db/ktx/src/main/java/androidx/sqlite/db/SupportSQLiteDatabase.kt b/persistence/db/ktx/src/main/java/androidx/sqlite/db/SupportSQLiteDatabase.kt
new file mode 100644
index 0000000..ba7b338
--- /dev/null
+++ b/persistence/db/ktx/src/main/java/androidx/sqlite/db/SupportSQLiteDatabase.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.sqlite.db
+
+/**
+ * Run [body] in a transaction marking it as successful if it completes without exception.
+ *
+ * @param exclusive Run in `EXCLUSIVE` mode when true, `IMMEDIATE` mode otherwise.
+ */
+inline fun <T> SupportSQLiteDatabase.transaction(
+ exclusive: Boolean = true,
+ body: SupportSQLiteDatabase.() -> T
+): T {
+ if (exclusive) {
+ beginTransaction()
+ } else {
+ beginTransactionNonExclusive()
+ }
+ try {
+ val result = body()
+ setTransactionSuccessful()
+ return result
+ } finally {
+ endTransaction()
+ }
+}
diff --git a/persistence/db/ktx/src/test/java/androidx/sqlite/db/SupportSQLiteDatabaseTest.kt b/persistence/db/ktx/src/test/java/androidx/sqlite/db/SupportSQLiteDatabaseTest.kt
new file mode 100644
index 0000000..76698d3
--- /dev/null
+++ b/persistence/db/ktx/src/test/java/androidx/sqlite/db/SupportSQLiteDatabaseTest.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.sqlite.db
+
+import org.junit.Assert.fail
+import org.junit.Test
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+class SupportSQLiteDatabaseTest {
+ @Test fun exclusiveDefault() {
+ val db = mock(SupportSQLiteDatabase::class.java)
+ db.transaction {}
+ verify(db).beginTransaction()
+ }
+
+ @Test fun exclusiveFalse() {
+ val db = mock(SupportSQLiteDatabase::class.java)
+ db.transaction(exclusive = false) {}
+ verify(db).beginTransactionNonExclusive()
+ }
+
+ @Test fun exclusiveTrue() {
+ val db = mock(SupportSQLiteDatabase::class.java)
+ db.transaction(exclusive = true) {}
+ verify(db).beginTransaction()
+ }
+
+ @Test fun bodyNormalCallsSuccessAndEnd() {
+ val db = mock(SupportSQLiteDatabase::class.java)
+ db.transaction {}
+ verify(db).setTransactionSuccessful()
+ verify(db).endTransaction()
+ }
+
+ @Suppress("UNREACHABLE_CODE") // A programming error might not invoke the lambda.
+ @Test fun bodyThrowsDoesNotCallSuccess() {
+ val db = mock(SupportSQLiteDatabase::class.java)
+ try {
+ db.transaction {
+ throw IllegalStateException()
+ }
+ fail()
+ } catch (e: IllegalStateException) {
+ }
+ verify(db, times(0)).setTransactionSuccessful()
+ verify(db).endTransaction()
+ }
+}
diff --git a/preference/src/main/java/androidx/preference/PreferenceCategory.java b/preference/src/main/java/androidx/preference/PreferenceCategory.java
index 3585bd9..d00d959 100644
--- a/preference/src/main/java/androidx/preference/PreferenceCategory.java
+++ b/preference/src/main/java/androidx/preference/PreferenceCategory.java
@@ -20,6 +20,7 @@
import android.util.AttributeSet;
import androidx.core.content.res.TypedArrayUtils;
+import androidx.core.os.BuildCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat;
@@ -66,21 +67,30 @@
}
@Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+ if (BuildCompat.isAtLeastP()) {
+ holder.itemView.setAccessibilityHeading(true);
+ }
+ }
+
+ @Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(info);
+ if (!BuildCompat.isAtLeastP()) {
+ CollectionItemInfoCompat existingItemInfo = info.getCollectionItemInfo();
+ if (existingItemInfo == null) {
+ return;
+ }
- CollectionItemInfoCompat existingItemInfo = info.getCollectionItemInfo();
- if (existingItemInfo == null) {
- return;
+ final CollectionItemInfoCompat newItemInfo = CollectionItemInfoCompat.obtain(
+ existingItemInfo.getRowIndex(),
+ existingItemInfo.getRowSpan(),
+ existingItemInfo.getColumnIndex(),
+ existingItemInfo.getColumnSpan(),
+ true /* heading */,
+ existingItemInfo.isSelected());
+ info.setCollectionItemInfo(newItemInfo);
}
-
- final CollectionItemInfoCompat newItemInfo = CollectionItemInfoCompat.obtain(
- existingItemInfo.getRowIndex(),
- existingItemInfo.getRowSpan(),
- existingItemInfo.getColumnIndex(),
- existingItemInfo.getColumnSpan(),
- true /* heading */,
- existingItemInfo.isSelected());
- info.setCollectionItemInfo(newItemInfo);
}
}
diff --git a/room/common/src/main/java/androidx/room/ForeignKey.java b/room/common/src/main/java/androidx/room/ForeignKey.java
index 847941a..54b9252 100644
--- a/room/common/src/main/java/androidx/room/ForeignKey.java
+++ b/room/common/src/main/java/androidx/room/ForeignKey.java
@@ -13,11 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package androidx.room;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
import androidx.annotation.IntDef;
+import java.lang.annotation.Retention;
+
/**
* Declares a foreign key on another {@link Entity}.
* <p>
@@ -161,6 +164,7 @@
* {@link #onUpdate()}.
*/
@IntDef({NO_ACTION, RESTRICT, SET_NULL, SET_DEFAULT, CASCADE})
+ @Retention(SOURCE)
@interface Action {
}
}
diff --git a/samples/SupportCarDemos/src/main/AndroidManifest.xml b/samples/SupportCarDemos/src/main/AndroidManifest.xml
index 3de2cac..c0c2d83 100644
--- a/samples/SupportCarDemos/src/main/AndroidManifest.xml
+++ b/samples/SupportCarDemos/src/main/AndroidManifest.xml
@@ -139,6 +139,17 @@
<meta-data android:name="android.support.PARENT_ACTIVITY"
android:value=".SupportCarDemoActivity" />
</activity>
+
+ <activity android:name=".AppBarActivity"
+ android:label="App Bar"
+ android:parentActivityName=".SupportCarDemoActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.SAMPLE_CODE" />
+ </intent-filter>
+ <meta-data android:name="android.support.PARENT_ACTIVITY"
+ android:value=".SupportCarDemoActivity" />
+ </activity>
</application>
</manifest>
diff --git a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/AppBarActivity.java b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/AppBarActivity.java
new file mode 100644
index 0000000..3919c5a
--- /dev/null
+++ b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/AppBarActivity.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.androidx.car;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.Menu;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import androidx.car.widget.ListItem;
+import androidx.car.widget.ListItemAdapter;
+import androidx.car.widget.ListItemProvider;
+import androidx.car.widget.PagedListView;
+import androidx.car.widget.TextListItem;
+
+/**
+ * Demo activity for creating {@code App Bar} with {@link Toolbar}.
+ */
+public class AppBarActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_app_bar);
+ Toolbar toolbar = findViewById(R.id.car_toolbar);
+ setSupportActionBar(toolbar);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setTitle("Title");
+ getSupportActionBar().setSubtitle("Subtitle");
+
+ setUpList();
+ }
+
+ private void setUpList() {
+ PagedListView list = findViewById(R.id.list);
+ list.setAdapter(new ListItemAdapter(this, new SampleProvider(this, 50)));
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.demo_menu, menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ private static class SampleProvider extends ListItemProvider {
+
+ private int mCount;
+ private Context mContext;
+
+ SampleProvider(Context context, int count) {
+ mContext = context;
+ mCount = count;
+ }
+
+ @Override
+ public ListItem get(int position) {
+ if (position < 0 || position >= mCount) {
+ throw new IndexOutOfBoundsException();
+ }
+ TextListItem item = new TextListItem(mContext);
+ item.setPrimaryActionIcon(android.R.drawable.sym_def_app_icon, false);
+ item.setTitle("title");
+ return item;
+ }
+
+ @Override
+ public int size() {
+ return mCount;
+ }
+ }
+}
diff --git a/samples/SupportCarDemos/src/main/res/layout/activity_app_bar.xml b/samples/SupportCarDemos/src/main/res/layout/activity_app_bar.xml
new file mode 100644
index 0000000..74dcbc3
--- /dev/null
+++ b/samples/SupportCarDemos/src/main/res/layout/activity_app_bar.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2018 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <androidx.appcompat.widget.Toolbar
+ android:id="@+id/car_toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/car_card" />
+
+ <androidx.car.widget.PagedListView
+ android:id="@+id/list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:gutter="both" />
+</LinearLayout>
diff --git a/samples/SupportCarDemos/src/main/res/menu/demo_menu.xml b/samples/SupportCarDemos/src/main/res/menu/demo_menu.xml
new file mode 100644
index 0000000..4b52bd9
--- /dev/null
+++ b/samples/SupportCarDemos/src/main/res/menu/demo_menu.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2018 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/action_icon"
+ android:icon="@drawable/pressed_icon"
+ android:title="Action1 with icon"
+ app:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/action_overflow"
+ android:title="Action2"
+ app:showAsAction="always|withText"/>
+
+ <item
+ android:id="@+id/action_overflow"
+ android:title="Action3"
+ app:showAsAction="never"/>
+</menu>
\ No newline at end of file
diff --git a/samples/SupportMediaDemos/build.gradle b/samples/SupportMediaDemos/build.gradle
index bf23177..fecf2a5 100644
--- a/samples/SupportMediaDemos/build.gradle
+++ b/samples/SupportMediaDemos/build.gradle
@@ -25,7 +25,7 @@
android {
defaultConfig {
- minSdkVersion 19
- targetSdkVersion 26
+ minSdkVersion = 19
+ targetSdkVersion = 26
}
}
diff --git a/samples/SupportMediaDemos/src/main/AndroidManifest.xml b/samples/SupportMediaDemos/src/main/AndroidManifest.xml
index 267eafd..b686fcd 100644
--- a/samples/SupportMediaDemos/src/main/AndroidManifest.xml
+++ b/samples/SupportMediaDemos/src/main/AndroidManifest.xml
@@ -15,14 +15,17 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
package="com.example.androidx.media">
+
+ <uses-sdk tools:overrideLibrary="androidx.media.widget" />
+
<application android:label="Video View Test">
<!-- Video Selection Activity -->
<activity android:name="VideoSelector"
- android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
- android:configChanges="orientation|screenSize"
- >
+ android:theme="@style/Theme.AppCompat"
+ android:configChanges="orientation|screenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
@@ -31,8 +34,8 @@
<!-- Video playback activity -->
<activity android:name="VideoViewTest"
- android:configChanges="orientation|screenSize"
- >
+ android:theme="@style/Theme.AppCompat"
+ android:configChanges="orientation|screenSize">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
</intent-filter>
diff --git a/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoViewTest.java b/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoViewTest.java
index d640303..2289769 100644
--- a/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoViewTest.java
+++ b/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoViewTest.java
@@ -29,6 +29,7 @@
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.AttributeSet;
import android.util.Log;
+import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
@@ -36,6 +37,7 @@
import android.view.WindowManager;
import android.widget.Toast;
+import androidx.fragment.app.FragmentActivity;
import androidx.media.widget.MediaControlView2;
import androidx.media.widget.VideoView2;
@@ -43,7 +45,7 @@
* Test application for VideoView2/MediaControlView2
*/
@SuppressLint("NewApi")
-public class VideoViewTest extends Activity {
+public class VideoViewTest extends FragmentActivity {
public static final String LOOPING_EXTRA_NAME =
"com.example.androidx.media.VideoViewTest.IsLooping";
public static final String USE_TEXTURE_VIEW_EXTRA_NAME =
@@ -71,6 +73,7 @@
setContentView(R.layout.video_activity);
mVideoView = findViewById(R.id.video_view);
+ mVideoView.setActivity(this);
String errorString = null;
Intent intent = getIntent();
@@ -82,12 +85,11 @@
if (mUseTextureView) {
mVideoView.setViewType(VideoView2.VIEW_TYPE_TEXTUREVIEW);
}
-
- mVideoView.setFullScreenRequestListener(new FullScreenRequestListener());
mVideoView.setVideoUri(contentUri);
mMediaControlView = new MediaControlView2(this);
mVideoView.setMediaControlView2(mMediaControlView, 2000);
+ mMediaControlView.setOnFullScreenListener(new FullScreenListener());
}
if (errorString != null) {
showErrorDialog(errorString);
@@ -179,9 +181,10 @@
}
};
- private class FullScreenRequestListener implements VideoView2.OnFullScreenRequestListener {
+ private class FullScreenListener
+ implements MediaControlView2.OnFullScreenListener {
@Override
- public void onFullScreenRequest(View view, boolean fullScreen) {
+ public void onFullScreen(View view, boolean fullScreen) {
// TODO: Remove bottom controls after adding back button functionality.
if (mPrevHeight == 0 && mPrevWidth == 0) {
ViewGroup.LayoutParams params = mVideoView.getLayoutParams();
@@ -223,6 +226,7 @@
public static class MyVideoView extends VideoView2 {
private float mDX;
private float mDY;
+ private Activity mActivity;
public MyVideoView(Context context) {
super(context);
@@ -255,6 +259,18 @@
}
return super.onTouchEvent(ev);
}
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
+ mActivity.finish();
+ }
+ return true;
+ }
+
+ public void setActivity(Activity activity) {
+ mActivity = activity;
+ }
}
@Override
diff --git a/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SampleSliceProvider.java b/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SampleSliceProvider.java
index 0beb745..afa5415 100644
--- a/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SampleSliceProvider.java
+++ b/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SampleSliceProvider.java
@@ -65,9 +65,27 @@
public static final String ACTION_TOAST_RANGE_VALUE =
"com.example.androidx.slice.action.TOAST_RANGE_VALUE";
- public static final String[] URI_PATHS = {"message", "wifi", "note", "ride", "toggle",
- "toggle2", "contact", "gallery", "weather", "reservation", "loadlist", "loadgrid",
- "inputrange", "range", "contact2", "subscription"};
+ public static final String[] URI_PATHS = {
+ "message",
+ "wifi",
+ "note",
+ "ride",
+ "toggle",
+ "toggle2",
+ "toggletester",
+ "contact",
+ "contact2",
+ "gallery",
+ "gallery2",
+ "weather",
+ "reservation",
+ "loadlist",
+ "loadgrid",
+ "inputrange",
+ "range",
+ "subscription",
+ "singleitems",
+ };
/**
* @return Uri with the provided path
@@ -116,12 +134,16 @@
return createCustomToggleSlice(sliceUri);
case "/toggle2":
return createTwoCustomToggleSlices(sliceUri);
+ case "/toggletester":
+ return createdToggleTesterSlice(sliceUri);
case "/contact":
return createContact(sliceUri);
case "/contact2":
return createContact2(sliceUri);
case "/gallery":
- return createGallery(sliceUri);
+ return createGallery(sliceUri, true /* showHeader */);
+ case "/gallery2":
+ return createGallery(sliceUri, false /* showHeader */);
case "/weather":
return createWeather(sliceUri);
case "/reservation":
@@ -136,6 +158,8 @@
return createDownloadProgressRange(sliceUri);
case "/subscription":
return createCatSlice(sliceUri, false /* customSeeMore */);
+ case "/singleitems":
+ return createSingleSlice(sliceUri);
}
throw new IllegalArgumentException("Unknown uri " + sliceUri);
}
@@ -181,58 +205,41 @@
.build();
}
- private Slice createGallery(Uri sliceUri) {
+ private Slice createGallery(Uri sliceUri, boolean showHeader) {
SliceAction primaryAction = new SliceAction(
getBroadcastIntent(ACTION_TOAST, "open photo album"),
IconCompat.createWithResource(getContext(), R.drawable.slices_1),
LARGE_IMAGE,
"Open photo album");
- return new ListBuilder(getContext(), sliceUri, INFINITY)
- .setColor(0xff4285F4)
- .addRow(b -> b
- .setTitle("Family trip to Hawaii")
- .setSubtitle("Sep 30, 2017 - Oct 2, 2017")
- .setPrimaryAction(primaryAction))
- .addAction(new SliceAction(
- getBroadcastIntent(ACTION_TOAST, "cast photo album"),
- IconCompat.createWithResource(getContext(), R.drawable.ic_cast),
- "Cast photo album"))
- .addAction(new SliceAction(
- getBroadcastIntent(ACTION_TOAST, "share photo album"),
- IconCompat.createWithResource(getContext(), R.drawable.ic_share),
- "Share photo album"))
- .addGridRow(b -> b
- .addCell(cb -> cb
- .addImage(IconCompat.createWithResource(getContext(),
- R.drawable.slices_1),
- LARGE_IMAGE))
- .addCell(cb -> cb
- .addImage(IconCompat.createWithResource(getContext(),
- R.drawable.slices_2),
- LARGE_IMAGE))
- .addCell(cb -> cb
- .addImage(IconCompat.createWithResource(getContext(),
- R.drawable.slices_3),
- LARGE_IMAGE))
- .addCell(cb -> cb
- .addImage(IconCompat.createWithResource(getContext(),
- R.drawable.slices_4),
- LARGE_IMAGE))
- .addCell(cb -> cb
- .addImage(IconCompat.createWithResource(getContext(),
- R.drawable.slices_2),
- LARGE_IMAGE))
- .addCell(cb -> cb
- .addImage(IconCompat.createWithResource(getContext(),
- R.drawable.slices_3),
- LARGE_IMAGE))
- .addCell(cb -> cb
- .addImage(IconCompat.createWithResource(getContext(),
- R.drawable.slices_4),
- LARGE_IMAGE))
- .setSeeMoreAction(getBroadcastIntent(ACTION_TOAST, "see your gallery"))
- .setContentDescription("Images from your trip to Hawaii"))
- .build();
+ ListBuilder lb = new ListBuilder(getContext(), sliceUri, INFINITY)
+ .setAccentColor(0xff4285F4);
+ if (showHeader) {
+ lb.addRow(b -> b
+ .setTitle("Family trip to Hawaii")
+ .setSubtitle("Sep 30, 2017 - Oct 2, 2017")
+ .setPrimaryAction(primaryAction))
+ .addAction(new SliceAction(
+ getBroadcastIntent(ACTION_TOAST, "cast photo album"),
+ IconCompat.createWithResource(getContext(), R.drawable.ic_cast),
+ "Cast photo album"))
+ .addAction(new SliceAction(
+ getBroadcastIntent(ACTION_TOAST, "share photo album"),
+ IconCompat.createWithResource(getContext(), R.drawable.ic_share),
+ "Share photo album"));
+ }
+ int[] galleryResId = new int[] {R.drawable.slices_1, R.drawable.slices_2,
+ R.drawable.slices_3, R.drawable.slices_4};
+ int imageCount = 7;
+ GridRowBuilder grb = new GridRowBuilder(lb);
+ for (int i = 0; i < imageCount; i++) {
+ IconCompat ic = IconCompat.createWithResource(getContext(),
+ galleryResId[i % galleryResId.length]);
+ grb.addCell(cb -> cb.addImage(ic, LARGE_IMAGE));
+ }
+ grb.setPrimaryAction(primaryAction)
+ .setSeeMoreAction(getBroadcastIntent(ACTION_TOAST, "see your gallery"))
+ .setContentDescription("Images from your trip to Hawaii");
+ return lb.addGridRow(grb).build();
}
private Slice createCatSlice(Uri sliceUri, boolean customSeeMore) {
@@ -280,7 +287,7 @@
ListBuilder b = new ListBuilder(getContext(), sliceUri, INFINITY);
ListBuilder.RowBuilder rb = new ListBuilder.RowBuilder(b);
GridRowBuilder gb = new GridRowBuilder(b);
- return b.setColor(0xff3949ab)
+ return b.setAccentColor(0xff3949ab)
.addRow(rb
.setTitle("Mady Pitza")
.setSubtitle("Frequently contacted contact")
@@ -322,7 +329,7 @@
R.drawable.mady), SMALL_IMAGE, "Mady");
return new ListBuilder(getContext(), sliceUri, INFINITY)
- .setColor(0xff3949ab)
+ .setAccentColor(0xff3949ab)
.setHeader(b -> b
.setTitle("Mady Pitza")
.setSummary("Called " + lastCalledString)
@@ -377,7 +384,7 @@
private Slice createNoteSlice(Uri sliceUri) {
// TODO: Remote input.
return new ListBuilder(getContext(), sliceUri, INFINITY)
- .setColor(0xfff4b400)
+ .setAccentColor(0xfff4b400)
.addRow(b -> b.setTitle("Create new note"))
.addAction(new SliceAction(getBroadcastIntent(ACTION_TOAST, "create note"),
IconCompat.createWithResource(getContext(), R.drawable.ic_create),
@@ -393,7 +400,7 @@
private Slice createReservationSlice(Uri sliceUri) {
return new ListBuilder(getContext(), sliceUri, INFINITY)
- .setColor(0xffFF5252)
+ .setAccentColor(0xffFF5252)
.setHeader(b -> b
.setTitle("Upcoming trip to Seattle")
.setSubtitle("Feb 1 - 19 | 2 guests"))
@@ -431,8 +438,8 @@
SliceAction primaryAction = new SliceAction(getBroadcastIntent(ACTION_TOAST, "get ride"),
IconCompat.createWithResource(getContext(), R.drawable.ic_car), "Get Ride");
- return new ListBuilder(getContext(), sliceUri, -TimeUnit.MINUTES.toMillis(2))
- .setColor(0xff0F9D58)
+ return new ListBuilder(getContext(), sliceUri, TimeUnit.SECONDS.toMillis(10))
+ .setAccentColor(0xff0F9D58)
.setHeader(b -> b
.setTitle("Get ride")
.setSubtitle(headerSubtitle)
@@ -455,7 +462,7 @@
private Slice createCustomToggleSlice(Uri sliceUri) {
return new ListBuilder(getContext(), sliceUri, INFINITY)
- .setColor(0xffff4081)
+ .setAccentColor(0xffff4081)
.addRow(b -> b
.setTitle("Custom toggle")
.setSubtitle("It can support two states")
@@ -468,7 +475,7 @@
private Slice createTwoCustomToggleSlices(Uri sliceUri) {
return new ListBuilder(getContext(), sliceUri, INFINITY)
- .setColor(0xffff4081)
+ .setAccentColor(0xffff4081)
.addRow(b -> b
.setTitle("2 toggles")
.setSubtitle("each supports two states")
@@ -514,7 +521,7 @@
String sliceCDString = wifiEnabled ? "Wifi connected to " + state
: "Wifi disconnected, 10 networks available";
ListBuilder lb = new ListBuilder(getContext(), sliceUri, INFINITY)
- .setColor(0xff4285f4)
+ .setAccentColor(0xff4285f4)
.setHeader(b -> b
.setTitle("Wi-fi")
.setSubtitle(state)
@@ -570,7 +577,7 @@
new SliceAction(getBroadcastIntent(ACTION_TOAST, "open star rating"),
icon, "Rate");
return new ListBuilder(getContext(), sliceUri, INFINITY)
- .setColor(0xffff4081)
+ .setAccentColor(0xffff4081)
.addInputRange(c -> c
.setTitle("Star rating")
.setSubtitle("Rate from 5 to 10 because it's weird")
@@ -590,7 +597,7 @@
new SliceAction(
getBroadcastIntent(ACTION_TOAST, "open download"), icon, "Download");
return new ListBuilder(getContext(), sliceUri, INFINITY)
- .setColor(0xffff4081)
+ .setAccentColor(0xffff4081)
.addRange(c -> c
.setTitle("Download progress")
.setSubtitle("Download is happening")
@@ -600,6 +607,116 @@
.build();
}
+ private Slice createdToggleTesterSlice(Uri uri) {
+ IconCompat star = IconCompat.createWithResource(getContext(), R.drawable.toggle_star);
+ IconCompat icon = IconCompat.createWithResource(getContext(), R.drawable.ic_star_on);
+
+ SliceAction primaryAction = new SliceAction(
+ getBroadcastIntent(ACTION_TOAST, "primary action"), icon, "Primary action");
+ SliceAction toggleAction = new SliceAction(
+ getBroadcastIntent(ACTION_TOAST, "star note"), star, "Star note", false);
+ SliceAction toggleAction2 = new SliceAction(
+ getBroadcastIntent(ACTION_TOAST, "star note 2"), star, "Star note 2", true);
+ SliceAction toggleAction3 = new SliceAction(
+ getBroadcastIntent(ACTION_TOAST, "star note 3"), star, "Star note 3", false);
+
+ ListBuilder lb = new ListBuilder(getContext(), uri, INFINITY);
+
+ // Primary action toggle
+ ListBuilder.RowBuilder primaryToggle = new ListBuilder.RowBuilder(lb);
+ primaryToggle.setTitle("Primary action is a toggle")
+ .setPrimaryAction(toggleAction);
+
+ // End toggle + normal primary action
+ ListBuilder.RowBuilder endToggle = new ListBuilder.RowBuilder(lb);
+ endToggle.setTitle("Only end toggles")
+ .setSubtitle("Normal primary action")
+ .setPrimaryAction(primaryAction)
+ .addEndItem(toggleAction)
+ .addEndItem(toggleAction2);
+
+ // Start toggle + normal primary
+ ListBuilder.RowBuilder startToggle = new ListBuilder.RowBuilder(lb);
+ startToggle.setTitle("One start toggle")
+ .setTitleItem(toggleAction)
+ .setSubtitle("Normal primary action")
+ .setPrimaryAction(primaryAction);
+
+ // Start + end toggles + normal primary action
+ ListBuilder.RowBuilder someToggles = new ListBuilder.RowBuilder(lb);
+ someToggles.setTitleItem(toggleAction)
+ .setPrimaryAction(primaryAction)
+ .setTitle("Start & end toggles")
+ .setSubtitle("Normal primary action")
+ .addEndItem(toggleAction2)
+ .addEndItem(toggleAction3);
+
+ // Start toggle ONLY
+ ListBuilder.RowBuilder startToggleOnly = new ListBuilder.RowBuilder(lb);
+ startToggleOnly.setTitle("Start action is a toggle")
+ .setSubtitle("No other actions")
+ .setTitleItem(toggleAction);
+
+ // End toggle ONLY
+ ListBuilder.RowBuilder endToggleOnly = new ListBuilder.RowBuilder(lb);
+ endToggleOnly.setTitle("End action is a toggle")
+ .setSubtitle("No other actions")
+ .addEndItem(toggleAction);
+
+ // All toggles: end item should be ignored / replaced with primary action
+ ListBuilder.RowBuilder muchToggles = new ListBuilder.RowBuilder(lb);
+ muchToggles.setTitleItem(toggleAction)
+ .setTitle("All toggles")
+ .setSubtitle("Even the primary action")
+ .setPrimaryAction(toggleAction2)
+ .addEndItem(toggleAction3);
+
+ lb.addRow(primaryToggle);
+ lb.addRow(endToggleOnly);
+ lb.addRow(endToggle);
+ lb.addRow(startToggleOnly);
+ lb.addRow(startToggle);
+ lb.addRow(someToggles);
+ lb.addRow(muchToggles);
+ return lb.build();
+ }
+
+ private Slice createSingleSlice(Uri uri) {
+ IconCompat ic2 = IconCompat.createWithResource(getContext(), R.drawable.ic_create);
+ IconCompat image = IconCompat.createWithResource(getContext(), R.drawable.cat_3);
+ IconCompat toggle = IconCompat.createWithResource(getContext(), R.drawable.toggle_star);
+ SliceAction toggleAction = new SliceAction(
+ getBroadcastIntent(ACTION_TOAST, "toggle action"), toggle, "toggle", false);
+ SliceAction simpleAction = new SliceAction(
+ getBroadcastIntent(ACTION_TOAST, "icon action"), ic2, "icon");
+ ListBuilder lb = new ListBuilder(getContext(), uri, INFINITY);
+ return lb.addRow(new ListBuilder.RowBuilder(lb)
+ .setTitle("Single title"))
+ .addRow(new ListBuilder.RowBuilder(lb)
+ .setSubtitle("Single subtitle"))
+ //Time stamps
+ .addRow(new ListBuilder.RowBuilder(lb)
+ .setTitleItem(System.currentTimeMillis()))
+ .addRow(new ListBuilder.RowBuilder(lb)
+ .addEndItem(System.currentTimeMillis()))
+ // Toggle actions
+ .addRow(new ListBuilder.RowBuilder(lb)
+ .setTitleItem(toggleAction))
+ .addRow(new ListBuilder.RowBuilder(lb)
+ .addEndItem(toggleAction))
+ // Icon actions
+ .addRow(new ListBuilder.RowBuilder(lb)
+ .setTitleItem(simpleAction))
+ .addRow(new ListBuilder.RowBuilder(lb)
+ .addEndItem(simpleAction))
+ // Images
+ .addRow(new ListBuilder.RowBuilder(lb)
+ .setTitleItem(image, SMALL_IMAGE))
+ .addRow(new ListBuilder.RowBuilder(lb)
+ .addEndItem(image, SMALL_IMAGE))
+ .build();
+ }
+
private Handler mHandler = new Handler();
private SparseArray<String> mListSummaries = new SparseArray<>();
private long mListLastUpdate;
diff --git a/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceBrowser.java b/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceBrowser.java
index 0a855a5..7b9d3d6 100644
--- a/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceBrowser.java
+++ b/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceBrowser.java
@@ -16,6 +16,8 @@
package com.example.androidx.slice.demos;
+import static androidx.slice.core.SliceHints.INFINITY;
+
import static com.example.androidx.slice.demos.SampleSliceProvider.URI_PATHS;
import static com.example.androidx.slice.demos.SampleSliceProvider.getUri;
@@ -47,6 +49,7 @@
import androidx.lifecycle.LiveData;
import androidx.slice.Slice;
import androidx.slice.SliceItem;
+import androidx.slice.SliceMetadata;
import androidx.slice.widget.EventInfo;
import androidx.slice.widget.SliceLiveData;
import androidx.slice.widget.SliceView;
@@ -65,7 +68,7 @@
private static final String SLICE_METADATA_KEY = "android.metadata.SLICE_URI";
private static final boolean TEST_INTENT = false;
- private static final boolean TEST_THEMES = false;
+ private static final boolean TEST_THEMES = true;
private static final boolean SCROLLING_ENABLED = true;
private ArrayList<Uri> mSliceUris = new ArrayList<Uri>();
@@ -218,7 +221,17 @@
mContainer.addView(v);
mSliceLiveData = SliceLiveData.fromUri(this, uri);
v.setMode(mSelectedMode);
- mSliceLiveData.observe(this, v);
+ mSliceLiveData.observe(this, slice -> {
+ v.setSlice(slice);
+ SliceMetadata metadata = SliceMetadata.from(this, slice);
+ long expiry = metadata.getExpiry();
+ if (expiry != INFINITY) {
+ // Shows the updated text after the TTL expires.
+ v.postDelayed(() -> v.setSlice(slice),
+ expiry - System.currentTimeMillis() + 15);
+ }
+ });
+ mSliceLiveData.observe(this, slice -> Log.d(TAG, "Slice: " + slice));
} else {
Log.w(TAG, "Invalid uri, skipping slice: " + uri);
}
diff --git a/samples/SupportSliceDemos/src/main/res/values/styles.xml b/samples/SupportSliceDemos/src/main/res/values/styles.xml
index d276a41..fcc4b1f 100644
--- a/samples/SupportSliceDemos/src/main/res/values/styles.xml
+++ b/samples/SupportSliceDemos/src/main/res/values/styles.xml
@@ -34,21 +34,7 @@
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light"/>
<style name="CustomSliceView" parent="Widget.SliceView">
- <item name="tintColor">@color/tintColor</item>
-
- <item name="titleColor">@color/textColor</item>
- <item name="subtitleColor">@color/textColor</item>
-
- <item name="headerTitleSize">@dimen/textSize</item>
- <item name="headerSubtitleSize">@dimen/textSize</item>
- <item name="titleSize">@dimen/textSize</item>
- <item name="subtitleSize">@dimen/textSize</item>
- <item name="gridTitleSize">@dimen/textSize</item>
- <item name="gridSubtitleSize">@dimen/smallTextSize</item>
-
- <item name="android:paddingLeft">8dp</item>
- <item name="android:paddingTop">8dp</item>
- <item name="android:paddingRight">8dp</item>
- <item name="android:paddingBottom">8dp</item>
+ <item name="gridTopPadding">4dp</item>
+ <item name="gridBottomPadding">4dp</item>
</style>
</resources>
diff --git a/settings.gradle b/settings.gradle
index 8a3a7fe..d969d88 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -40,9 +40,11 @@
includeProject(":car", "car")
includeProject(":cardview", "cardview")
includeProject(":collection", "collection")
+includeProject(":collection-ktx", "collection/ktx")
includeProject(":contentpaging", "content")
includeProject(":coordinatorlayout", "coordinatorlayout")
includeProject(":core", "compat")
+includeProject(":core-ktx", "core/ktx")
includeProject(":cursoradapter", "cursoradapter")
includeProject(":customview", "customview")
includeProject(":documentfile", "documentfile")
@@ -69,6 +71,7 @@
includeProject(":media-widget", "media-widget")
includeProject(":mediarouter", "mediarouter")
includeProject(":palette", "palette")
+includeProject(":palette-ktx", "palette/ktx")
includeProject(":percentlayout", "percent")
includeProject(":preference", "preference")
includeProject(":print", "print")
@@ -79,6 +82,7 @@
includeProject(":slice-view", "slices/view")
includeProject(":slice-builders", "slices/builders")
includeProject(":slidingpanelayout", "slidingpanelayout")
+includeProject(":fragment-ktx", "fragment/ktx")
includeProject(":swiperefreshlayout", "swiperefreshlayout")
includeProject(":textclassifier", "textclassifier")
includeProject(":transition", "transition")
@@ -138,6 +142,7 @@
/////////////////////////////
includeProject(":internal-testutils", "testutils")
+includeProject(":internal-testutils-ktx", "testutils-ktx")
/////////////////////////////
//
diff --git a/slices/builders/api/current.txt b/slices/builders/api/current.txt
index 1041eb9..f191aed 100644
--- a/slices/builders/api/current.txt
+++ b/slices/builders/api/current.txt
@@ -77,7 +77,8 @@
method public deprecated androidx.slice.builders.ListBuilder addSeeMoreAction(android.app.PendingIntent);
method public deprecated androidx.slice.builders.ListBuilder addSeeMoreRow(androidx.slice.builders.ListBuilder.RowBuilder);
method public deprecated androidx.slice.builders.ListBuilder addSeeMoreRow(androidx.core.util.Consumer<androidx.slice.builders.ListBuilder.RowBuilder>);
- method public androidx.slice.builders.ListBuilder setColor(int);
+ method public androidx.slice.builders.ListBuilder setAccentColor(int);
+ method public deprecated androidx.slice.builders.ListBuilder setColor(int);
method public androidx.slice.builders.ListBuilder setHeader(androidx.slice.builders.ListBuilder.HeaderBuilder);
method public androidx.slice.builders.ListBuilder setHeader(androidx.core.util.Consumer<androidx.slice.builders.ListBuilder.HeaderBuilder>);
method public androidx.slice.builders.ListBuilder setKeywords(java.util.List<java.lang.String>);
diff --git a/slices/builders/build.gradle b/slices/builders/build.gradle
index ec85148..91c7599 100644
--- a/slices/builders/build.gradle
+++ b/slices/builders/build.gradle
@@ -37,4 +37,5 @@
description = "A set of builders to create templates using SliceProvider APIs"
minSdkVersion = 19
failOnUncheckedWarnings = false
+ failOnDeprecationWarnings = false
}
diff --git a/slices/builders/src/main/java/androidx/slice/builders/ListBuilder.java b/slices/builders/src/main/java/androidx/slice/builders/ListBuilder.java
index 1c81ebd..76b9e08 100644
--- a/slices/builders/src/main/java/androidx/slice/builders/ListBuilder.java
+++ b/slices/builders/src/main/java/androidx/slice/builders/ListBuilder.java
@@ -82,9 +82,10 @@
* <ul>
* <li>{@link androidx.slice.widget.SliceView#MODE_SHORTCUT} - The primary {@link SliceAction}
* of the slice is used your primary action should contain an image and title representative
- * of your slice. If providing a tintable icon, use {@link #setColor(int)} to specify the color.
- * If a header has been specified for the list, the primary action associated with it will be
- * used, otherwise it will be the primary action associated with the first row of the list.
+ * of your slice. If providing a tintable icon, use {@link #setAccentColor(int)} to specify the
+ * color. If a header has been specified for the list, the primary action associated with it
+ * will be used, otherwise it will be the primary action associated with the first row of the
+ * list.
* </li>
* <li>{@link androidx.slice.widget.SliceView#MODE_SMALL} - Only a single row of content is
* displayed in small format. If a header has been specified it will be displayed. If no header
@@ -310,6 +311,15 @@
}
/**
+ * @deprecated TO BE REMOVED; use {@link #setAccentColor(int)} instead.
+ */
+ @Deprecated
+ @NonNull
+ public ListBuilder setColor(@ColorInt int color) {
+ return setAccentColor(color);
+ }
+
+ /**
* Sets the color to use on tintable items within the list builder.
* Things that might be tinted are:
* <ul>
@@ -322,7 +332,7 @@
* </ul>
*/
@NonNull
- public ListBuilder setColor(@ColorInt int color) {
+ public ListBuilder setAccentColor(@ColorInt int color) {
mImpl.setColor(color);
return this;
}
diff --git a/slices/builders/src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java b/slices/builders/src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java
index 714b570..16471ed 100644
--- a/slices/builders/src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java
+++ b/slices/builders/src/main/java/androidx/slice/builders/impl/GridRowBuilderListV1Impl.java
@@ -18,11 +18,9 @@
import static android.app.slice.Slice.HINT_HORIZONTAL;
import static android.app.slice.Slice.HINT_LARGE;
-import static android.app.slice.Slice.HINT_LIST_ITEM;
import static android.app.slice.Slice.HINT_NO_TINT;
import static android.app.slice.Slice.HINT_PARTIAL;
import static android.app.slice.Slice.HINT_SEE_MORE;
-import static android.app.slice.Slice.HINT_SHORTCUT;
import static android.app.slice.Slice.HINT_TITLE;
import static android.app.slice.Slice.SUBTYPE_CONTENT_DESCRIPTION;
@@ -59,23 +57,12 @@
/**
*/
@Override
- @NonNull
- public Slice build() {
- Slice.Builder sb = new Slice.Builder(getBuilder())
- .addHints(HINT_HORIZONTAL, HINT_LIST_ITEM);
- sb.addSubSlice(getBuilder().addHints(HINT_HORIZONTAL, HINT_LIST_ITEM).build());
- if (mPrimaryAction != null) {
- Slice.Builder actionBuilder = new Slice.Builder(getBuilder())
- .addHints(HINT_SHORTCUT, HINT_TITLE);
- sb.addSubSlice(mPrimaryAction.buildSlice(actionBuilder));
- }
- return sb.build();
- }
-
- /**
- */
- @Override
public void apply(Slice.Builder builder) {
+ builder.addHints(HINT_HORIZONTAL);
+ if (mPrimaryAction != null) {
+ Slice.Builder actionBuilder = new Slice.Builder(getBuilder()).addHints(HINT_TITLE);
+ builder.addSubSlice(mPrimaryAction.buildSlice(actionBuilder));
+ }
}
/**
@@ -96,7 +83,7 @@
*/
@Override
public void addCell(TemplateBuilderImpl builder) {
- getBuilder().addSubSlice(builder.getBuilder().addHints(HINT_LIST_ITEM).build());
+ builder.apply(getBuilder());
}
@@ -105,7 +92,7 @@
@Override
public void setSeeMoreCell(@NonNull TemplateBuilderImpl builder) {
builder.getBuilder().addHints(HINT_SEE_MORE);
- getBuilder().addSubSlice(builder.build());
+ builder.apply(getBuilder());
}
/**
@@ -184,8 +171,8 @@
@Override
public void addTitleText(@Nullable CharSequence text, boolean isLoading) {
@Slice.SliceHint String[] hints = isLoading
- ? new String[] {HINT_PARTIAL, HINT_LARGE}
- : new String[] {HINT_LARGE};
+ ? new String[] {HINT_PARTIAL, HINT_TITLE}
+ : new String[] {HINT_TITLE};
getBuilder().addText(text, null, hints);
}
@@ -236,20 +223,12 @@
@RestrictTo(LIBRARY)
@Override
public void apply(Slice.Builder b) {
- }
-
- /**
- */
- @Override
- @NonNull
- public Slice build() {
+ getBuilder().addHints(HINT_HORIZONTAL);
if (mContentIntent != null) {
- return new Slice.Builder(getBuilder())
- .addHints(HINT_HORIZONTAL)
- .addAction(mContentIntent, getBuilder().build(), null)
- .build();
+ b.addAction(mContentIntent, getBuilder().build(), null);
+ } else {
+ b.addSubSlice(getBuilder().build());
}
- return getBuilder().addHints(HINT_HORIZONTAL).build();
}
}
}
diff --git a/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilderV1Impl.java b/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilderV1Impl.java
index c771b62..8b16abe 100644
--- a/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilderV1Impl.java
+++ b/slices/builders/src/main/java/androidx/slice/builders/impl/ListBuilderV1Impl.java
@@ -76,7 +76,7 @@
*/
@Override
public void apply(Slice.Builder builder) {
- builder.addTimestamp(System.currentTimeMillis(), SUBTYPE_MILLIS, HINT_LAST_UPDATED);
+ builder.addLong(System.currentTimeMillis(), SUBTYPE_MILLIS, HINT_LAST_UPDATED);
if (mSliceHeader != null) {
builder.addSubSlice(mSliceHeader);
}
@@ -95,6 +95,7 @@
@NonNull
@Override
public void addRow(@NonNull TemplateBuilderImpl builder) {
+ builder.getBuilder().addHints(HINT_LIST_ITEM);
getBuilder().addSubSlice(builder.build());
}
@@ -103,6 +104,7 @@
@NonNull
@Override
public void addGridRow(@NonNull TemplateBuilderImpl builder) {
+ builder.getBuilder().addHints(HINT_LIST_ITEM);
getBuilder().addSubSlice(builder.build());
}
@@ -558,7 +560,6 @@
getBuilder()).addHints(HINT_TITLE, HINT_SHORTCUT);
b.addSubSlice(mPrimaryAction.buildSlice(sb), null);
}
- b.addHints(HINT_LIST_ITEM);
}
}
diff --git a/slices/core/api/current.txt b/slices/core/api/current.txt
index bbaa3a8..c6077bf 100644
--- a/slices/core/api/current.txt
+++ b/slices/core/api/current.txt
@@ -17,21 +17,34 @@
method public java.util.List<java.lang.String> getHints();
method public androidx.core.graphics.drawable.IconCompat getIcon();
method public int getInt();
+ method public long getLong();
method public androidx.slice.Slice getSlice();
method public java.lang.String getSubType();
method public java.lang.CharSequence getText();
- method public long getTimestamp();
+ method public deprecated long getTimestamp();
method public boolean hasHint(java.lang.String);
}
- public abstract class SliceProvider extends android.content.ContentProvider {
+ public abstract class SliceProvider extends android.content.ContentProvider implements androidx.core.app.CoreComponentFactory.CompatWrapped {
+ ctor public SliceProvider(java.lang.String...);
ctor public SliceProvider();
+ method public final int bulkInsert(android.net.Uri, android.content.ContentValues[]);
+ method public final android.net.Uri canonicalize(android.net.Uri);
+ method public final int delete(android.net.Uri, java.lang.String, java.lang.String[]);
+ method public final java.lang.String getType(android.net.Uri);
+ method public java.lang.Object getWrapper();
+ method public final android.net.Uri insert(android.net.Uri, android.content.ContentValues);
method public abstract androidx.slice.Slice onBindSlice(android.net.Uri);
+ method public final boolean onCreate();
method public abstract boolean onCreateSliceProvider();
method public java.util.Collection<android.net.Uri> onGetSliceDescendants(android.net.Uri);
method public android.net.Uri onMapIntentToUri(android.content.Intent);
method public void onSlicePinned(android.net.Uri);
method public void onSliceUnpinned(android.net.Uri);
+ method public final android.database.Cursor query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String);
+ method public final android.database.Cursor query(android.net.Uri, java.lang.String[], android.os.Bundle, android.os.CancellationSignal);
+ method public final android.database.Cursor query(android.net.Uri, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, android.os.CancellationSignal);
+ method public final int update(android.net.Uri, android.content.ContentValues, java.lang.String, java.lang.String[]);
}
}
diff --git a/slices/core/src/androidTest/java/androidx/slice/SliceTestProvider.java b/slices/core/src/androidTest/java/androidx/slice/SliceTestProvider.java
index d6f1628..519e5b8 100644
--- a/slices/core/src/androidTest/java/androidx/slice/SliceTestProvider.java
+++ b/slices/core/src/androidTest/java/androidx/slice/SliceTestProvider.java
@@ -33,6 +33,7 @@
@Override
public boolean onCreateSliceProvider() {
+ getContext().getPackageName();
return true;
}
diff --git a/slices/core/src/androidTest/java/androidx/slice/compat/CompatPermissionManagerTest.java b/slices/core/src/androidTest/java/androidx/slice/compat/CompatPermissionManagerTest.java
new file mode 100644
index 0000000..552df8c
--- /dev/null
+++ b/slices/core/src/androidTest/java/androidx/slice/compat/CompatPermissionManagerTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.slice.compat;
+
+import static androidx.core.content.PermissionChecker.PERMISSION_DENIED;
+import static androidx.core.content.PermissionChecker.PERMISSION_GRANTED;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Process;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CompatPermissionManagerTest {
+
+ private final Context mContext = InstrumentationRegistry.getContext();
+
+ @Test
+ public void testAutoGrant() {
+ final Uri uri = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority("my.authority")
+ .path("my_path")
+ .build();
+ final String testPermission = "android.permission.SOME_PERMISSION";
+
+ final int grantedPid = Process.myPid();
+ final int grantedUid = Process.myUid();
+
+ final int nonGrantedPid = grantedPid + 1;
+ final int nonGrantedUid = grantedUid + 1;
+
+ Context permContext = new ContextWrapper(mContext) {
+ @Override
+ public int checkPermission(String permission, int pid, int uid) {
+ if (testPermission.equals(permission)) {
+ if (grantedUid == uid) {
+ return PackageManager.PERMISSION_GRANTED;
+ } else if (nonGrantedUid == uid) {
+ return PackageManager.PERMISSION_DENIED;
+ }
+ }
+ return super.checkPermission(permission, pid, uid);
+ }
+
+ @Override
+ public PackageManager getPackageManager() {
+ PackageManager pm = spy(super.getPackageManager());
+ when(pm.getPackagesForUid(grantedUid)).thenReturn(new String[] { "grant_pkg"});
+ when(pm.getPackagesForUid(nonGrantedUid)).thenReturn(new String[] { "other_pkg"});
+ return pm;
+ }
+ };
+ CompatPermissionManager manager = spy(new CompatPermissionManager(permContext, "nothing", 0,
+ new String[] {testPermission}));
+
+ assertEquals(PERMISSION_DENIED, manager.checkSlicePermission(uri,
+ nonGrantedPid, nonGrantedUid));
+
+ assertEquals(PERMISSION_GRANTED, manager.checkSlicePermission(uri, grantedPid, grantedUid));
+ verify(manager).grantSlicePermission(eq(uri), eq("grant_pkg"));
+
+ }
+
+}
diff --git a/slices/core/src/androidTest/java/androidx/slice/compat/SliceProviderCompatTest.java b/slices/core/src/androidTest/java/androidx/slice/compat/SliceProviderCompatTest.java
new file mode 100644
index 0000000..cc4ba49
--- /dev/null
+++ b/slices/core/src/androidTest/java/androidx/slice/compat/SliceProviderCompatTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.slice.compat;
+
+import static androidx.core.content.PermissionChecker.PERMISSION_DENIED;
+import static androidx.core.content.PermissionChecker.PERMISSION_GRANTED;
+import static androidx.slice.compat.SliceProviderCompat.EXTRA_BIND_URI;
+import static androidx.slice.compat.SliceProviderCompat.EXTRA_SLICE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+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.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.slice.Slice;
+import androidx.slice.SliceProvider;
+import androidx.slice.SliceSpec;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SliceProviderCompatTest {
+
+ private final Context mContext = InstrumentationRegistry.getContext();
+
+ @Test
+ public void testBindWithPermission() {
+ Uri uri = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority("my.authority")
+ .path("my_path")
+ .build();
+ Slice s = new Slice.Builder(uri)
+ .addText("", null)
+ .build();
+
+ SliceProvider provider = spy(new SliceProviderImpl());
+ CompatPermissionManager permissions = mock(CompatPermissionManager.class);
+ when(permissions.checkSlicePermission(any(Uri.class), anyInt(), anyInt()))
+ .thenReturn(PERMISSION_GRANTED);
+
+ when(provider.onBindSlice(eq(uri))).thenReturn(s);
+ SliceProviderCompat compat = new SliceProviderCompat(provider, permissions,
+ mContext) {
+ @Override
+ public String getCallingPackage() {
+ return mContext.getPackageName();
+ }
+ };
+
+ Bundle b = new Bundle();
+ b.putParcelable(EXTRA_BIND_URI, uri);
+ SliceProviderCompat.addSpecs(b, Collections.<SliceSpec>emptySet());
+
+ Bundle result = compat.call(SliceProviderCompat.METHOD_SLICE, null, b);
+ assertEquals(s.toString(), new Slice(result.getBundle(EXTRA_SLICE)).toString());
+ }
+
+ @Test
+ public void testBindWithoutPermission() {
+ Uri uri = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority("my.authority")
+ .path("my_path")
+ .build();
+ Slice s = new Slice.Builder(uri)
+ .addText("", null)
+ .build();
+
+ SliceProvider provider = spy(new SliceProviderImpl());
+ CompatPermissionManager permissions = mock(CompatPermissionManager.class);
+ when(permissions.checkSlicePermission(any(Uri.class), anyInt(), anyInt()))
+ .thenReturn(PERMISSION_DENIED);
+
+ when(provider.onBindSlice(eq(uri))).thenReturn(s);
+ SliceProviderCompat compat = new SliceProviderCompat(provider, permissions,
+ mContext) {
+ @Override
+ public String getCallingPackage() {
+ return mContext.getPackageName();
+ }
+ };
+
+ Bundle b = new Bundle();
+ b.putParcelable(EXTRA_BIND_URI, uri);
+ SliceProviderCompat.addSpecs(b, Collections.<SliceSpec>emptySet());
+
+ Bundle result = compat.call(SliceProviderCompat.METHOD_SLICE, null, b);
+ assertNotEquals(s.toString(), new Slice(result.getBundle(EXTRA_SLICE)).toString());
+ }
+
+ public static class SliceProviderImpl extends SliceProvider {
+
+ @Override
+ public boolean onCreateSliceProvider() {
+ return true;
+ }
+
+ @Override
+ public Slice onBindSlice(Uri sliceUri) {
+ return null;
+ }
+ }
+}
diff --git a/slices/core/src/main/java/androidx/slice/Slice.java b/slices/core/src/main/java/androidx/slice/Slice.java
index d291ad2..8ce99da 100644
--- a/slices/core/src/main/java/androidx/slice/Slice.java
+++ b/slices/core/src/main/java/androidx/slice/Slice.java
@@ -31,6 +31,7 @@
import static android.app.slice.SliceItem.FORMAT_ACTION;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
import static android.app.slice.SliceItem.FORMAT_INT;
+import static android.app.slice.SliceItem.FORMAT_LONG;
import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TEXT;
@@ -58,6 +59,7 @@
import androidx.annotation.StringDef;
import androidx.core.graphics.drawable.IconCompat;
import androidx.core.os.BuildCompat;
+import androidx.core.util.Consumer;
import androidx.slice.compat.SliceProviderCompat;
import java.util.ArrayList;
@@ -285,6 +287,19 @@
}
/**
+ * Add an action to the slice being constructed
+ * @param subType Optional template-specific type information
+ * @see {@link SliceItem#getSubType()}
+ */
+ public Slice.Builder addAction(@NonNull Consumer<Uri> action,
+ @NonNull Slice s, @Nullable String subType) {
+ @SliceHint String[] hints = s != null
+ ? s.getHints().toArray(new String[s.getHints().size()]) : new String[0];
+ mItems.add(new SliceItem(action, s, FORMAT_ACTION, subType, hints));
+ return this;
+ }
+
+ /**
* Add text to the slice being constructed
* @param subType Optional template-specific type information
* @see {@link SliceItem#getSubType()}
@@ -377,6 +392,19 @@
* @param subType Optional template-specific type information
* @see {@link SliceItem#getSubType()}
*/
+ public Slice.Builder addLong(long time, @Nullable String subType,
+ @SliceHint String... hints) {
+ mItems.add(new SliceItem(time, FORMAT_LONG, subType, hints));
+ return this;
+ }
+
+ /**
+ * Add a timestamp to the slice being constructed
+ * @param subType Optional template-specific type information
+ * @see {@link SliceItem#getSubType()}
+ * @deprecated TO BE REMOVED
+ */
+ @Deprecated
public Slice.Builder addTimestamp(long time, @Nullable String subType,
@SliceHint String... hints) {
mItems.add(new SliceItem(time, FORMAT_TIMESTAMP, subType, hints));
@@ -427,20 +455,37 @@
public String toString(String indent) {
StringBuilder sb = new StringBuilder();
sb.append(indent);
- sb.append("slice: ");
- sb.append("\n");
- indent += " ";
+ sb.append("slice ");
+ addHints(sb, mHints);
+ sb.append("{\n");
+ String nextIndent = indent + " ";
for (int i = 0; i < mItems.length; i++) {
SliceItem item = mItems[i];
- sb.append(item.toString(indent));
- if (!FORMAT_SLICE.equals(item.getFormat())) {
- sb.append("\n");
- }
+ sb.append(item.toString(nextIndent));
}
+ sb.append(indent);
+ sb.append("}");
return sb.toString();
}
/**
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY)
+ public static void addHints(StringBuilder sb, String[] hints) {
+ if (hints == null || hints.length == 0) return;
+
+ sb.append("(");
+ int end = hints.length - 1;
+ for (int i = 0; i < end; i++) {
+ sb.append(hints[i]);
+ sb.append(", ");
+ }
+ sb.append(hints[end]);
+ sb.append(") ");
+ }
+
+ /**
* Turns a slice Uri into slice content.
*
* @hide
diff --git a/slices/core/src/main/java/androidx/slice/SliceItem.java b/slices/core/src/main/java/androidx/slice/SliceItem.java
index a5741f0..964c5df 100644
--- a/slices/core/src/main/java/androidx/slice/SliceItem.java
+++ b/slices/core/src/main/java/androidx/slice/SliceItem.java
@@ -19,13 +19,19 @@
import static android.app.slice.SliceItem.FORMAT_ACTION;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
import static android.app.slice.SliceItem.FORMAT_INT;
+import static android.app.slice.SliceItem.FORMAT_LONG;
import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TEXT;
import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
+import static androidx.slice.Slice.addHints;
+
import android.app.PendingIntent;
import android.app.RemoteInput;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.TextUtils;
@@ -36,6 +42,7 @@
import androidx.annotation.RestrictTo.Scope;
import androidx.annotation.StringDef;
import androidx.core.graphics.drawable.IconCompat;
+import androidx.core.util.Consumer;
import androidx.core.util.Pair;
import java.util.Arrays;
@@ -71,7 +78,7 @@
*/
@RestrictTo(Scope.LIBRARY)
@StringDef({FORMAT_SLICE, FORMAT_TEXT, FORMAT_IMAGE, FORMAT_ACTION, FORMAT_INT,
- FORMAT_TIMESTAMP, FORMAT_REMOTE_INPUT})
+ FORMAT_TIMESTAMP, FORMAT_REMOTE_INPUT, FORMAT_LONG})
public @interface SliceType {
}
@@ -111,7 +118,16 @@
@RestrictTo(Scope.LIBRARY)
public SliceItem(PendingIntent intent, Slice slice, String format, String subType,
@Slice.SliceHint String[] hints) {
- this(new Pair<>(intent, slice), format, subType, hints);
+ this(new Pair<Object, Slice>(intent, slice), format, subType, hints);
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY)
+ public SliceItem(Consumer<Uri> action, Slice slice, String format, String subType,
+ @Slice.SliceHint String[] hints) {
+ this(new Pair<Object, Slice>(action, slice), format, subType, hints);
}
/**
@@ -188,7 +204,20 @@
* SliceItem
*/
public PendingIntent getAction() {
- return ((Pair<PendingIntent, Slice>) mObj).first;
+ return (PendingIntent) ((Pair<Object, Slice>) mObj).first;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public void fireAction(Context context, Intent i) throws PendingIntent.CanceledException {
+ Object action = ((Pair<Object, Slice>) mObj).first;
+ if (action instanceof PendingIntent) {
+ ((PendingIntent) action).send(context, 0, i, null, null);
+ } else {
+ ((Consumer<Uri>) action).accept(getSlice().getUri());
+ }
}
/**
@@ -215,15 +244,23 @@
*/
public Slice getSlice() {
if (FORMAT_ACTION.equals(getFormat())) {
- return ((Pair<PendingIntent, Slice>) mObj).second;
+ return ((Pair<Object, Slice>) mObj).second;
}
return (Slice) mObj;
}
/**
- * @return The timestamp held by this {@link android.app.slice.SliceItem#FORMAT_TIMESTAMP}
+ * @return The long held by this {@link android.app.slice.SliceItem#FORMAT_LONG}
* SliceItem
*/
+ public long getLong() {
+ return (Long) mObj;
+ }
+
+ /**
+ * @deprecated TO BE REMOVED
+ */
+ @Deprecated
public long getTimestamp() {
return (Long) mObj;
}
@@ -301,8 +338,8 @@
dest.putParcelable(OBJ, ((Slice) obj).toBundle());
break;
case FORMAT_ACTION:
- dest.putParcelable(OBJ, ((Pair<PendingIntent, Slice>) obj).first);
- dest.putBundle(OBJ_2, ((Pair<PendingIntent, Slice>) obj).second.toBundle());
+ dest.putParcelable(OBJ, (PendingIntent) ((Pair<Object, Slice>) obj).first);
+ dest.putBundle(OBJ_2, ((Pair<Object, Slice>) obj).second.toBundle());
break;
case FORMAT_TEXT:
dest.putCharSequence(OBJ, (CharSequence) obj);
@@ -328,7 +365,7 @@
return in.getCharSequence(OBJ);
case FORMAT_ACTION:
return new Pair<>(
- (PendingIntent) in.getParcelable(OBJ),
+ in.getParcelable(OBJ),
new Slice(in.getBundle(OBJ_2)));
case FORMAT_INT:
return in.getInt(OBJ);
@@ -377,36 +414,35 @@
@RestrictTo(Scope.LIBRARY)
public String toString(String indent) {
StringBuilder sb = new StringBuilder();
- if (!FORMAT_SLICE.equals(getFormat())) {
- sb.append(indent);
- sb.append(getFormat());
- sb.append(": ");
- }
switch (getFormat()) {
case FORMAT_SLICE:
sb.append(getSlice().toString(indent));
break;
case FORMAT_ACTION:
- sb.append(getAction());
- sb.append("\n");
+ sb.append(indent).append(getAction()).append(",\n");
sb.append(getSlice().toString(indent));
break;
case FORMAT_TEXT:
- sb.append(getText());
+ sb.append(indent).append('"').append(getText()).append('"');
break;
case FORMAT_IMAGE:
- sb.append(getIcon());
+ sb.append(indent).append(getIcon());
break;
case FORMAT_INT:
- sb.append(getInt());
+ sb.append(indent).append(getInt());
break;
- case FORMAT_TIMESTAMP:
- sb.append(getTimestamp());
+ case FORMAT_LONG:
+ sb.append(indent).append(getLong());
break;
default:
- sb.append(SliceItem.typeToString(getFormat()));
+ sb.append(indent).append(SliceItem.typeToString(getFormat()));
break;
}
+ if (!FORMAT_SLICE.equals(getFormat())) {
+ sb.append(' ');
+ addHints(sb, mHints);
+ }
+ sb.append(",\n");
return sb.toString();
}
}
diff --git a/slices/core/src/main/java/androidx/slice/SliceProvider.java b/slices/core/src/main/java/androidx/slice/SliceProvider.java
index 8dc3b99..9c604eb 100644
--- a/slices/core/src/main/java/androidx/slice/SliceProvider.java
+++ b/slices/core/src/main/java/androidx/slice/SliceProvider.java
@@ -15,21 +15,44 @@
*/
package androidx.slice;
+import static android.app.slice.Slice.HINT_SHORTCUT;
+import static android.app.slice.Slice.HINT_TITLE;
+import static android.app.slice.SliceProvider.SLICE_TYPE;
+
+import static androidx.slice.compat.SliceProviderCompat.EXTRA_BIND_URI;
+import static androidx.slice.compat.SliceProviderCompat.EXTRA_PKG;
+import static androidx.slice.compat.SliceProviderCompat.EXTRA_PROVIDER_PKG;
+import static androidx.slice.compat.SliceProviderCompat.PERMS_PREFIX;
+import static androidx.slice.core.SliceHints.HINT_PERMISSION_REQUEST;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
import android.content.ContentProvider;
import android.content.ContentResolver;
+import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.content.pm.ProviderInfo;
+import android.content.pm.PackageManager;
import android.database.ContentObserver;
+import android.database.Cursor;
import android.net.Uri;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Process;
+import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.app.CoreComponentFactory;
import androidx.core.os.BuildCompat;
-import androidx.slice.compat.ContentProviderWrapper;
+import androidx.slice.compat.CompatPermissionManager;
import androidx.slice.compat.SliceProviderCompat;
import androidx.slice.compat.SliceProviderWrapperContainer;
+import androidx.slice.core.R;
import java.util.Collection;
import java.util.Collections;
@@ -74,19 +97,37 @@
*
* @see android.app.slice.Slice
*/
-public abstract class SliceProvider extends ContentProviderWrapper {
+public abstract class SliceProvider extends ContentProvider implements
+ CoreComponentFactory.CompatWrapped {
private static Set<SliceSpec> sSpecs;
- @Override
- public void attachInfo(Context context, ProviderInfo info) {
- ContentProvider impl;
- if (BuildCompat.isAtLeastP()) {
- impl = new SliceProviderWrapperContainer.SliceProviderWrapper(this);
- } else {
- impl = new SliceProviderCompat(this);
- }
- super.attachInfo(context, info, impl);
+ private static final String TAG = "SliceProvider";
+
+ private static final boolean DEBUG = false;
+ private final String[] mAutoGrantPermissions;
+
+ private SliceProviderCompat mCompat;
+
+
+ /**
+ * A version of constructing a SliceProvider that allows autogranting slice permissions
+ * to apps that hold specific platform permissions.
+ * <p>
+ * When an app tries to bind a slice from this provider that it does not have access to,
+ * This provider will check if the caller holds permissions to any of the autoGrantPermissions
+ * specified, if they do they will be granted persisted uri access to all slices of this
+ * provider.
+ *
+ * @param autoGrantPermissions List of permissions that holders are auto-granted access
+ * to slices.
+ */
+ public SliceProvider(@NonNull String... autoGrantPermissions) {
+ mAutoGrantPermissions = autoGrantPermissions;
+ }
+
+ public SliceProvider() {
+ mAutoGrantPermissions = new String[0];
}
/**
@@ -106,6 +147,106 @@
*/
public abstract boolean onCreateSliceProvider();
+ @Override
+ public Object getWrapper() {
+ if (BuildCompat.isAtLeastP()) {
+ return new SliceProviderWrapperContainer.SliceProviderWrapper(this,
+ mAutoGrantPermissions);
+ }
+ return null;
+ }
+
+ @Override
+ public final boolean onCreate() {
+ if (!BuildCompat.isAtLeastP()) {
+ mCompat = new SliceProviderCompat(this,
+ onCreatePermissionManager(mAutoGrantPermissions), getContext());
+ }
+ return onCreateSliceProvider();
+ }
+
+ /**
+ * @hide
+ * @param autoGrantPermissions
+ */
+ @VisibleForTesting
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ protected CompatPermissionManager onCreatePermissionManager(
+ String[] autoGrantPermissions) {
+ return new CompatPermissionManager(getContext(), PERMS_PREFIX + getClass().getName(),
+ Process.myUid(), autoGrantPermissions);
+ }
+
+ @Override
+ public final String getType(Uri uri) {
+ if (DEBUG) Log.d(TAG, "getFormat " + uri);
+ return SLICE_TYPE;
+ }
+
+ @Override
+ public Bundle call(String method, String arg, Bundle extras) {
+ return mCompat != null ? mCompat.call(method, arg, extras) : null;
+ }
+
+ /**
+ * Generate a slice that contains a permission request.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static Slice createPermissionSlice(Context context, Uri sliceUri,
+ String callingPackage) {
+ Slice.Builder parent = new Slice.Builder(sliceUri);
+
+ Slice.Builder action = new Slice.Builder(parent)
+ .addHints(HINT_TITLE, HINT_SHORTCUT)
+ .addAction(createPermissionIntent(context, sliceUri, callingPackage),
+ new Slice.Builder(parent).build(), null);
+
+ parent.addSubSlice(new Slice.Builder(sliceUri.buildUpon().appendPath("permission").build())
+ .addText(getPermissionString(context, callingPackage), null)
+ .addSubSlice(action.build())
+ .build());
+
+ return parent.addHints(HINT_PERMISSION_REQUEST).build();
+ }
+
+ /**
+ * Create a PendingIntent pointing at the permission dialog.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static PendingIntent createPermissionIntent(Context context, Uri sliceUri,
+ String callingPackage) {
+ Intent intent = new Intent();
+ intent.setComponent(new ComponentName(context.getPackageName(),
+ "androidx.slice.compat.SlicePermissionActivity"));
+ intent.putExtra(EXTRA_BIND_URI, sliceUri);
+ intent.putExtra(EXTRA_PKG, callingPackage);
+ intent.putExtra(EXTRA_PROVIDER_PKG, context.getPackageName());
+ // Unique pending intent.
+ intent.setData(sliceUri.buildUpon().appendQueryParameter("package", callingPackage)
+ .build());
+
+ return PendingIntent.getActivity(context, 0, intent, 0);
+ }
+
+ /**
+ * Get string describing permission request.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static CharSequence getPermissionString(Context context, String callingPackage) {
+ PackageManager pm = context.getPackageManager();
+ try {
+ return context.getString(R.string.abc_slices_permission_request,
+ pm.getApplicationInfo(callingPackage, 0).loadLabel(pm),
+ context.getApplicationInfo().loadLabel(pm));
+ } catch (PackageManager.NameNotFoundException e) {
+ // This shouldn't be possible since the caller is verified.
+ throw new RuntimeException("Unknown calling app", e);
+ }
+ }
+
/**
* Implemented to create a slice.
* <p>
@@ -181,6 +322,61 @@
return Collections.emptyList();
}
+ @Nullable
+ @Override
+ public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
+ @Nullable String selection, @Nullable String[] selectionArgs,
+ @Nullable String sortOrder) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ @RequiresApi(28)
+ public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
+ @Nullable Bundle queryArgs, @Nullable CancellationSignal cancellationSignal) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ @RequiresApi(16)
+ public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
+ @Nullable String selection, @Nullable String[] selectionArgs,
+ @Nullable String sortOrder, @Nullable CancellationSignal cancellationSignal) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public final Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
+ return null;
+ }
+
+ @Override
+ public final int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
+ return 0;
+ }
+
+ @Override
+ public final int delete(@NonNull Uri uri, @Nullable String selection,
+ @Nullable String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public final int update(@NonNull Uri uri, @Nullable ContentValues values,
+ @Nullable String selection, @Nullable String[] selectionArgs) {
+ return 0;
+ }
+
+ @Nullable
+ @Override
+ @RequiresApi(19)
+ public final Uri canonicalize(@NonNull Uri url) {
+ return null;
+ }
+
/**
* @hide
*/
diff --git a/slices/core/src/main/java/androidx/slice/compat/CompatPermissionManager.java b/slices/core/src/main/java/androidx/slice/compat/CompatPermissionManager.java
new file mode 100644
index 0000000..8321a05
--- /dev/null
+++ b/slices/core/src/main/java/androidx/slice/compat/CompatPermissionManager.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.slice.compat;
+
+import static androidx.core.content.PermissionChecker.PERMISSION_DENIED;
+import static androidx.core.content.PermissionChecker.PERMISSION_GRANTED;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.RestrictTo;
+import androidx.collection.ArraySet;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class CompatPermissionManager {
+
+ private static final String TAG = "CompatPermissionManager";
+ public static final String ALL_SUFFIX = "_all";
+
+ private final Context mContext;
+ private final String mPrefsName;
+ private final int mMyUid;
+ private final String[] mAutoGrantPermissions;
+
+ public CompatPermissionManager(Context context, String prefsName, int myUid,
+ String[] autoGrantPermissions) {
+ mContext = context;
+ mPrefsName = prefsName;
+ mMyUid = myUid;
+ mAutoGrantPermissions = autoGrantPermissions;
+ }
+
+ private SharedPreferences getPrefs() {
+ return mContext.getSharedPreferences(mPrefsName, Context.MODE_PRIVATE);
+ }
+
+ public int checkSlicePermission(Uri uri, int pid, int uid) {
+ if (uid == mMyUid) {
+ return PERMISSION_GRANTED;
+ }
+ String[] pkgs = mContext.getPackageManager().getPackagesForUid(uid);
+ for (String pkg : pkgs) {
+ if (checkSlicePermission(uri, pkg) == PERMISSION_GRANTED) {
+ return PERMISSION_GRANTED;
+ }
+ }
+ for (String autoGrantPermission : mAutoGrantPermissions) {
+ if (mContext.checkPermission(autoGrantPermission, pid, uid) == PERMISSION_GRANTED) {
+ for (String pkg : pkgs) {
+ grantSlicePermission(uri, pkg);
+ }
+ return PERMISSION_GRANTED;
+ }
+ }
+ // Fall back to allowing uri permissions through.
+ return mContext.checkUriPermission(uri, pid, uid, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ }
+
+ private int checkSlicePermission(Uri uri, String pkg) {
+ PermissionState state = getPermissionState(pkg, uri.getAuthority());
+ return state.hasAccess(uri.getPathSegments()) ? PERMISSION_GRANTED : PERMISSION_DENIED;
+ }
+
+ public void grantSlicePermission(Uri uri, String toPkg) {
+ PermissionState state = getPermissionState(toPkg, uri.getAuthority());
+ if (state.addPath(uri.getPathSegments())) {
+ persist(state);
+ }
+ }
+
+ public void revokeSlicePermission(Uri uri, String toPkg) {
+ PermissionState state = getPermissionState(toPkg, uri.getAuthority());
+ if (state.removePath(uri.getPathSegments())) {
+ persist(state);
+ }
+ }
+
+ private synchronized void persist(PermissionState state) {
+ if (!getPrefs().edit()
+ .putStringSet(state.getKey(), state.toPersistable())
+ .putBoolean(state.getKey() + ALL_SUFFIX, state.hasAllPermissions())
+ .commit()) {
+ Log.e(TAG, "Unable to persist permissions");
+ }
+ }
+
+ private PermissionState getPermissionState(String pkg, String authority) {
+ String key = pkg + "_" + authority;
+ Set<String> grant = getPrefs().getStringSet(key, Collections.<String>emptySet());
+ boolean hasAllPermissions = getPrefs().getBoolean(key + ALL_SUFFIX, false);
+ return new PermissionState(grant, key, hasAllPermissions);
+ }
+
+ public static class PermissionState {
+
+ private final ArraySet<String[]> mPaths = new ArraySet<>();
+ private final String mKey;
+
+ PermissionState(Set<String> grant, String key, boolean hasAllPermissions) {
+ if (hasAllPermissions) {
+ mPaths.add(new String[0]);
+ } else {
+ for (String g : grant) {
+ mPaths.add(decodeSegments(g));
+ }
+ }
+ mKey = key;
+ }
+
+ public boolean hasAllPermissions() {
+ return hasAccess(Collections.<String>emptyList());
+ }
+
+ public String getKey() {
+ return mKey;
+ }
+
+ public Set<String> toPersistable() {
+ ArraySet<String> ret = new ArraySet<>();
+ for (String[] path : mPaths) {
+ ret.add(encodeSegments(path));
+ }
+ return ret;
+ }
+
+ public boolean hasAccess(List<String> path) {
+ String[] inPath = path.toArray(new String[path.size()]);
+ for (String[] p : mPaths) {
+ if (isPathPrefixMatch(p, inPath)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean addPath(List<String> path) {
+ String[] pathSegs = path.toArray(new String[path.size()]);
+ for (int i = mPaths.size() - 1; i >= 0; i--) {
+ String[] existing = mPaths.valueAt(i);
+ if (isPathPrefixMatch(existing, pathSegs)) {
+ // Nothing to add here.
+ return false;
+ }
+ if (isPathPrefixMatch(pathSegs, existing)) {
+ mPaths.removeAt(i);
+ }
+ }
+ mPaths.add(pathSegs);
+ return true;
+ }
+
+ boolean removePath(List<String> path) {
+ boolean changed = false;
+ String[] pathSegs = path.toArray(new String[path.size()]);
+ for (int i = mPaths.size() - 1; i >= 0; i--) {
+ String[] existing = mPaths.valueAt(i);
+ if (isPathPrefixMatch(pathSegs, existing)) {
+ changed = true;
+ mPaths.removeAt(i);
+ }
+ }
+ return changed;
+ }
+
+ private boolean isPathPrefixMatch(String[] prefix, String[] path) {
+ final int prefixSize = prefix.length;
+ if (path.length < prefixSize) return false;
+
+ for (int i = 0; i < prefixSize; i++) {
+ if (!Objects.equals(path[i], prefix[i])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private String encodeSegments(String[] s) {
+ String[] out = new String[s.length];
+ for (int i = 0; i < s.length; i++) {
+ out[i] = Uri.encode(s[i]);
+ }
+ return TextUtils.join("/", out);
+ }
+
+ private String[] decodeSegments(String s) {
+ String[] sets = s.split("/", -1);
+ for (int i = 0; i < sets.length; i++) {
+ sets[i] = Uri.decode(sets[i]);
+ }
+ return sets;
+ }
+ }
+}
diff --git a/slices/core/src/main/java/androidx/slice/compat/CompatPinnedList.java b/slices/core/src/main/java/androidx/slice/compat/CompatPinnedList.java
index 56b8122..da602f5 100644
--- a/slices/core/src/main/java/androidx/slice/compat/CompatPinnedList.java
+++ b/slices/core/src/main/java/androidx/slice/compat/CompatPinnedList.java
@@ -28,6 +28,8 @@
import androidx.core.util.ObjectsCompat;
import androidx.slice.SliceSpec;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Set;
/**
@@ -70,6 +72,22 @@
return prefs;
}
+ /**
+ * Get pinned specs
+ */
+ public List<Uri> getPinnedSlices() {
+ List<Uri> pinned = new ArrayList<>();
+ for (String key : getPrefs().getAll().keySet()) {
+ if (key.startsWith(PIN_PREFIX)) {
+ Uri uri = Uri.parse(key.substring(PIN_PREFIX.length()));
+ if (!getPins(uri).isEmpty()) {
+ pinned.add(uri);
+ }
+ }
+ }
+ return pinned;
+ }
+
private Set<String> getPins(Uri uri) {
return getPrefs().getStringSet(PIN_PREFIX + uri.toString(), new ArraySet<String>());
}
@@ -85,8 +103,8 @@
if (TextUtils.isEmpty(specNamesStr) || TextUtils.isEmpty(specRevsStr)) {
return new ArraySet<>();
}
- String[] specNames = specNamesStr.split(",");
- String[] specRevs = specRevsStr.split(",");
+ String[] specNames = specNamesStr.split(",", -1);
+ String[] specRevs = specRevsStr.split(",", -1);
if (specNames.length != specRevs.length) {
return new ArraySet<>();
}
diff --git a/slices/core/src/main/java/androidx/slice/compat/ContentProviderWrapper.java b/slices/core/src/main/java/androidx/slice/compat/ContentProviderWrapper.java
deleted file mode 100644
index 45c6ffc..0000000
--- a/slices/core/src/main/java/androidx/slice/compat/ContentProviderWrapper.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.slice.compat;
-
-import android.content.ContentProvider;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.pm.ProviderInfo;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.CancellationSignal;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.RestrictTo.Scope;
-
-/**
- * @hide
- */
-// TODO: Remove as soon as we have better systems in place for this.
-@RestrictTo(Scope.LIBRARY)
-public class ContentProviderWrapper extends ContentProvider {
-
- private ContentProvider mImpl;
-
- /**
- * Triggers an attach with the object to wrap.
- */
- public void attachInfo(Context context, ProviderInfo info, ContentProvider impl) {
- mImpl = impl;
- super.attachInfo(context, info);
- mImpl.attachInfo(context, info);
- }
-
- @Override
- public final boolean onCreate() {
- return mImpl.onCreate();
- }
-
- @Nullable
- @Override
- public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
- @Nullable String selection, @Nullable String[] selectionArgs,
- @Nullable String sortOrder) {
- return mImpl.query(uri, projection, selection, selectionArgs, sortOrder);
- }
-
- @Nullable
- @Override
- @RequiresApi(28)
- public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
- @Nullable Bundle queryArgs, @Nullable CancellationSignal cancellationSignal) {
- return mImpl.query(uri, projection, queryArgs, cancellationSignal);
- }
-
- @Nullable
- @Override
- @RequiresApi(16)
- public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
- @Nullable String selection, @Nullable String[] selectionArgs,
- @Nullable String sortOrder, @Nullable CancellationSignal cancellationSignal) {
- return mImpl.query(uri, projection, selection, selectionArgs, sortOrder,
- cancellationSignal);
- }
-
- @Nullable
- @Override
- public final String getType(@NonNull Uri uri) {
- return mImpl.getType(uri);
- }
-
- @Nullable
- @Override
- public final Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
- return mImpl.insert(uri, values);
- }
-
- @Override
- public final int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
- return mImpl.bulkInsert(uri, values);
- }
-
- @Override
- public final int delete(@NonNull Uri uri, @Nullable String selection,
- @Nullable String[] selectionArgs) {
- return mImpl.delete(uri, selection, selectionArgs);
- }
-
- @Override
- public final int update(@NonNull Uri uri, @Nullable ContentValues values,
- @Nullable String selection, @Nullable String[] selectionArgs) {
- return mImpl.update(uri, values, selection, selectionArgs);
- }
-
- @Nullable
- @Override
- public final Bundle call(@NonNull String method, @Nullable String arg,
- @Nullable Bundle extras) {
- return mImpl.call(method, arg, extras);
- }
-
- @Nullable
- @Override
- @RequiresApi(19)
- public final Uri canonicalize(@NonNull Uri url) {
- return mImpl.canonicalize(url);
- }
-}
diff --git a/slices/core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java b/slices/core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java
index f3270f8..5fa33ae 100644
--- a/slices/core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java
+++ b/slices/core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java
@@ -20,16 +20,20 @@
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface.OnDismissListener;
-import android.content.Intent;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Uri;
import android.os.Bundle;
+import android.text.Html;
+import android.text.TextPaint;
+import android.text.TextUtils;
import android.util.Log;
import android.widget.TextView;
import androidx.annotation.RestrictTo;
import androidx.appcompat.app.AlertDialog;
+import androidx.core.text.BidiFormatter;
import androidx.slice.core.R;
/**
@@ -40,6 +44,8 @@
public class SlicePermissionActivity extends Activity implements OnClickListener,
OnDismissListener {
+ private static final float MAX_LABEL_SIZE_PX = 500f;
+
private static final String TAG = "SlicePermissionActivity";
private Uri mUri;
@@ -56,8 +62,12 @@
try {
PackageManager pm = getPackageManager();
- CharSequence app1 = pm.getApplicationInfo(mCallingPkg, 0).loadLabel(pm);
- CharSequence app2 = pm.getApplicationInfo(mProviderPkg, 0).loadLabel(pm);
+ CharSequence app1 = BidiFormatter.getInstance().unicodeWrap(
+ loadSafeLabel(pm, pm.getApplicationInfo(mCallingPkg, 0))
+ .toString());
+ CharSequence app2 = BidiFormatter.getInstance().unicodeWrap(
+ loadSafeLabel(pm, pm.getApplicationInfo(mProviderPkg, 0))
+ .toString());
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle(getString(R.string.abc_slice_permission_title, app1, app2))
.setView(R.layout.abc_slice_permission_request)
@@ -75,14 +85,50 @@
}
}
+ // Based on loadSafeLabel in PackageitemInfo
+ private CharSequence loadSafeLabel(PackageManager pm, ApplicationInfo appInfo) {
+ // loadLabel() always returns non-null
+ String label = appInfo.loadLabel(pm).toString();
+ // strip HTML tags to avoid <br> and other tags overwriting original message
+ String labelStr = Html.fromHtml(label).toString();
+
+ // If the label contains new line characters it may push the UI
+ // down to hide a part of it. Labels shouldn't have new line
+ // characters, so just truncate at the first time one is seen.
+ final int labelLength = labelStr.length();
+ int offset = 0;
+ while (offset < labelLength) {
+ final int codePoint = labelStr.codePointAt(offset);
+ final int type = Character.getType(codePoint);
+ if (type == Character.LINE_SEPARATOR
+ || type == Character.CONTROL
+ || type == Character.PARAGRAPH_SEPARATOR) {
+ labelStr = labelStr.substring(0, offset);
+ break;
+ }
+ // replace all non-break space to " " in order to be trimmed
+ if (type == Character.SPACE_SEPARATOR) {
+ labelStr = labelStr.substring(0, offset) + " " + labelStr.substring(offset
+ + Character.charCount(codePoint));
+ }
+ offset += Character.charCount(codePoint);
+ }
+
+ labelStr = labelStr.trim();
+ if (labelStr.isEmpty()) {
+ return appInfo.packageName;
+ }
+ TextPaint paint = new TextPaint();
+ paint.setTextSize(42);
+
+ return TextUtils.ellipsize(labelStr, paint, MAX_LABEL_SIZE_PX, TextUtils.TruncateAt.END);
+ }
+
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
- grantUriPermission(mCallingPkg, mUri.buildUpon().path("").build(),
- Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
- | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
- | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
- getContentResolver().notifyChange(mUri, null);
+ SliceProviderCompat.grantSlicePermission(this, getPackageName(), mCallingPkg,
+ mUri.buildUpon().path("").build());
}
finish();
}
diff --git a/slices/core/src/main/java/androidx/slice/compat/SliceProviderCompat.java b/slices/core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
index 57f070d..22e832e 100644
--- a/slices/core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
+++ b/slices/core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
@@ -15,35 +15,30 @@
*/
package androidx.slice.compat;
-import static android.app.slice.Slice.HINT_SHORTCUT;
-import static android.app.slice.Slice.HINT_TITLE;
+import static android.app.slice.SliceManager.CATEGORY_SLICE;
+import static android.app.slice.SliceManager.SLICE_METADATA_KEY;
import static android.app.slice.SliceProvider.SLICE_TYPE;
-import static androidx.slice.core.SliceHints.HINT_PERMISSION_REQUEST;
+import static androidx.core.content.PermissionChecker.PERMISSION_DENIED;
+import static androidx.core.content.PermissionChecker.PERMISSION_GRANTED;
-import android.app.PendingIntent;
-import android.content.ComponentName;
-import android.content.ContentProvider;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
-import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
+import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
-import android.database.Cursor;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
-import android.os.CancellationSignal;
import android.os.Handler;
import android.os.Looper;
import android.os.Parcelable;
import android.os.Process;
import android.os.RemoteException;
import android.os.StrictMode;
-import android.os.StrictMode.ThreadPolicy;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -54,8 +49,6 @@
import androidx.slice.Slice;
import androidx.slice.SliceProvider;
import androidx.slice.SliceSpec;
-import androidx.slice.core.R;
-import androidx.slice.core.SliceHints;
import java.util.ArrayList;
import java.util.Collection;
@@ -67,11 +60,14 @@
* @hide
*/
@RestrictTo(Scope.LIBRARY)
-public class SliceProviderCompat extends ContentProvider {
+public class SliceProviderCompat {
+ public static final String PERMS_PREFIX = "slice_perms_";
+ private static final String TAG = "SliceProviderCompat";
+ private static final String DATA_PREFIX = "slice_data_";
+ private static final String ALL_FILES = DATA_PREFIX + "all_slice_files";
- private static final String TAG = "SliceProvider";
+ private static final long SLICE_BIND_ANR = 2000;
- public static final String EXTRA_BIND_URI = "slice_uri";
public static final String METHOD_SLICE = "bind_slice";
public static final String METHOD_MAP_INTENT = "map_slice";
public static final String METHOD_PIN = "pin_slice";
@@ -79,7 +75,11 @@
public static final String METHOD_GET_PINNED_SPECS = "get_specs";
public static final String METHOD_MAP_ONLY_INTENT = "map_only";
public static final String METHOD_GET_DESCENDANTS = "get_descendants";
+ public static final String METHOD_CHECK_PERMISSION = "check_perms";
+ public static final String METHOD_GRANT_PERMISSION = "grant_perms";
+ public static final String METHOD_REVOKE_PERMISSION = "revoke_perms";
+ public static final String EXTRA_BIND_URI = "slice_uri";
public static final String EXTRA_INTENT = "slice_intent";
public static final String EXTRA_SLICE = "slice";
public static final String EXTRA_SUPPORTED_SPECS = "specs";
@@ -87,75 +87,48 @@
public static final String EXTRA_PKG = "pkg";
public static final String EXTRA_PROVIDER_PKG = "provider_pkg";
public static final String EXTRA_SLICE_DESCENDANTS = "slice_descendants";
- private static final String DATA_PREFIX = "slice_data_";
+ public static final String EXTRA_UID = "uid";
+ public static final String EXTRA_PID = "pid";
+ public static final String EXTRA_RESULT = "result";
- private static final long SLICE_BIND_ANR = 2000;
-
- private static final boolean DEBUG = false;
private final Handler mHandler = new Handler(Looper.getMainLooper());
- private SliceProvider mSliceProvider;
- private CompatPinnedList mPinnedList;
+ private final Context mContext;
private String mCallback;
+ private final SliceProvider mProvider;
+ private CompatPinnedList mPinnedList;
+ private CompatPermissionManager mPermissionManager;
- public SliceProviderCompat(SliceProvider provider) {
- mSliceProvider = provider;
+ public SliceProviderCompat(SliceProvider provider, CompatPermissionManager permissionManager,
+ Context context) {
+ mProvider = provider;
+ mContext = context;
+ String prefsFile = DATA_PREFIX + getClass().getName();
+ SharedPreferences allFiles = mContext.getSharedPreferences(ALL_FILES, 0);
+ Set<String> files = allFiles.getStringSet(ALL_FILES, Collections.<String>emptySet());
+ if (!files.contains(prefsFile)) {
+ // Make sure this is editable.
+ files = new ArraySet<>(files);
+ files.add(prefsFile);
+ allFiles.edit()
+ .putStringSet(ALL_FILES, files)
+ .commit();
+ }
+ mPinnedList = new CompatPinnedList(mContext, prefsFile);
+ mPermissionManager = permissionManager;
}
- @Override
- public boolean onCreate() {
- mPinnedList = new CompatPinnedList(getContext(),
- DATA_PREFIX + mSliceProvider.getClass().getName());
- return mSliceProvider.onCreateSliceProvider();
+ private Context getContext() {
+ return mContext;
}
- @Override
- public final int update(Uri uri, ContentValues values, String selection,
- String[] selectionArgs) {
- if (DEBUG) Log.d(TAG, "update " + uri);
- return 0;
+ public String getCallingPackage() {
+ return mProvider.getCallingPackage();
}
- @Override
- public final int delete(Uri uri, String selection, String[] selectionArgs) {
- if (DEBUG) Log.d(TAG, "delete " + uri);
- return 0;
- }
-
- @Override
- public final Cursor query(Uri uri, String[] projection, String selection,
- String[] selectionArgs, String sortOrder) {
- if (DEBUG) Log.d(TAG, "query " + uri);
- return null;
- }
-
- @Override
- public final Cursor query(Uri uri, String[] projection, String selection, String[]
- selectionArgs, String sortOrder, CancellationSignal cancellationSignal) {
- if (DEBUG) Log.d(TAG, "query " + uri);
- return null;
- }
-
- @Override
- public final Cursor query(Uri uri, String[] projection, Bundle queryArgs,
- CancellationSignal cancellationSignal) {
- if (DEBUG) Log.d(TAG, "query " + uri);
- return null;
- }
-
- @Override
- public final Uri insert(Uri uri, ContentValues values) {
- if (DEBUG) Log.d(TAG, "insert " + uri);
- return null;
- }
-
- @Override
- public final String getType(Uri uri) {
- if (DEBUG) Log.d(TAG, "getFormat " + uri);
- return SLICE_TYPE;
- }
-
- @Override
+ /**
+ * Called by SliceProvider when compat is needed.
+ */
public Bundle call(String method, String arg, Bundle extras) {
if (method.equals(METHOD_SLICE)) {
Uri uri = extras.getParcelable(EXTRA_BIND_URI);
@@ -163,23 +136,23 @@
Slice s = handleBindSlice(uri, specs, getCallingPackage());
Bundle b = new Bundle();
- b.putParcelable(EXTRA_SLICE, s.toBundle());
+ b.putParcelable(EXTRA_SLICE, s != null ? s.toBundle() : null);
return b;
} else if (method.equals(METHOD_MAP_INTENT)) {
Intent intent = extras.getParcelable(EXTRA_INTENT);
- Uri uri = mSliceProvider.onMapIntentToUri(intent);
+ Uri uri = mProvider.onMapIntentToUri(intent);
Bundle b = new Bundle();
if (uri != null) {
Set<SliceSpec> specs = getSpecs(extras);
Slice s = handleBindSlice(uri, specs, getCallingPackage());
- b.putParcelable(EXTRA_SLICE, s.toBundle());
+ b.putParcelable(EXTRA_SLICE, s != null ? s.toBundle() : null);
} else {
b.putParcelable(EXTRA_SLICE, null);
}
return b;
} else if (method.equals(METHOD_MAP_ONLY_INTENT)) {
Intent intent = extras.getParcelable(EXTRA_INTENT);
- Uri uri = mSliceProvider.onMapIntentToUri(intent);
+ Uri uri = mProvider.onMapIntentToUri(intent);
Bundle b = new Bundle();
b.putParcelable(EXTRA_SLICE, uri);
return b;
@@ -209,15 +182,37 @@
b.putParcelableArrayList(EXTRA_SLICE_DESCENDANTS,
new ArrayList<>(handleGetDescendants(uri)));
return b;
+ } else if (method.equals(METHOD_CHECK_PERMISSION)) {
+ Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+ String pkg = extras.getString(EXTRA_PKG);
+ int pid = extras.getInt(EXTRA_PID);
+ int uid = extras.getInt(EXTRA_UID);
+ Bundle b = new Bundle();
+ b.putInt(EXTRA_RESULT, mPermissionManager.checkSlicePermission(uri, pid, uid));
+ return b;
+ } else if (method.equals(METHOD_GRANT_PERMISSION)) {
+ Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+ String toPkg = extras.getString(EXTRA_PKG);
+ if (Binder.getCallingUid() != Process.myUid()) {
+ throw new SecurityException("Only the owning process can manage slice permissions");
+ }
+ mPermissionManager.grantSlicePermission(uri, toPkg);
+ } else if (method.equals(METHOD_REVOKE_PERMISSION)) {
+ Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+ String toPkg = extras.getString(EXTRA_PKG);
+ if (Binder.getCallingUid() != Process.myUid()) {
+ throw new SecurityException("Only the owning process can manage slice permissions");
+ }
+ mPermissionManager.revokeSlicePermission(uri, toPkg);
}
- return super.call(method, arg, extras);
+ return null;
}
private Collection<Uri> handleGetDescendants(Uri uri) {
mCallback = "onGetSliceDescendants";
mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
try {
- return mSliceProvider.onGetSliceDescendants(uri);
+ return mProvider.onGetSliceDescendants(uri);
} finally {
mHandler.removeCallbacks(mAnr);
}
@@ -227,7 +222,7 @@
mCallback = "onSlicePinned";
mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
try {
- mSliceProvider.onSlicePinned(sliceUri);
+ mProvider.onSlicePinned(sliceUri);
} finally {
mHandler.removeCallbacks(mAnr);
}
@@ -237,7 +232,7 @@
mCallback = "onSliceUnpinned";
mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
try {
- mSliceProvider.onSliceUnpinned(sliceUri);
+ mProvider.onSliceUnpinned(sliceUri);
} finally {
mHandler.removeCallbacks(mAnr);
}
@@ -249,74 +244,15 @@
// SliceManager#bindSlice.
String pkg = callingPkg != null ? callingPkg
: getContext().getPackageManager().getNameForUid(Binder.getCallingUid());
- if (Binder.getCallingUid() != Process.myUid()) {
- try {
- getContext().enforceUriPermission(sliceUri,
- Binder.getCallingPid(), Binder.getCallingUid(),
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
- "Slice binding requires write access to Uri");
- } catch (SecurityException e) {
- return createPermissionSlice(getContext(), sliceUri, pkg);
- }
+ if (mPermissionManager.checkSlicePermission(sliceUri, Binder.getCallingPid(),
+ Binder.getCallingUid()) != PERMISSION_GRANTED) {
+ return mProvider.createPermissionSlice(getContext(), sliceUri, pkg);
}
return onBindSliceStrict(sliceUri, specs);
}
- /**
- * Generate a slice that contains a permission request.
- */
- public static Slice createPermissionSlice(Context context, Uri sliceUri,
- String callingPackage) {
- Slice.Builder parent = new Slice.Builder(sliceUri);
-
- Slice.Builder action = new Slice.Builder(parent)
- .addHints(HINT_TITLE, HINT_SHORTCUT)
- .addAction(createPermissionIntent(context, sliceUri, callingPackage),
- new Slice.Builder(parent).build(), null);
-
- parent.addSubSlice(new Slice.Builder(sliceUri.buildUpon().appendPath("permission").build())
- .addText(getPermissionString(context, callingPackage), null)
- .addSubSlice(action.build())
- .build());
-
- return parent.addHints(HINT_PERMISSION_REQUEST).build();
- }
-
- /**
- * Create a PendingIntent pointing at the permission dialog.
- */
- public static PendingIntent createPermissionIntent(Context context, Uri sliceUri,
- String callingPackage) {
- Intent intent = new Intent();
- intent.setComponent(new ComponentName(context.getPackageName(),
- "androidx.slice.compat.SlicePermissionActivity"));
- intent.putExtra(EXTRA_BIND_URI, sliceUri);
- intent.putExtra(EXTRA_PKG, callingPackage);
- intent.putExtra(EXTRA_PROVIDER_PKG, context.getPackageName());
- // Unique pending intent.
- intent.setData(sliceUri.buildUpon().appendQueryParameter("package", callingPackage)
- .build());
-
- return PendingIntent.getActivity(context, 0, intent, 0);
- }
-
- /**
- * Get string describing permission request.
- */
- public static CharSequence getPermissionString(Context context, String callingPackage) {
- PackageManager pm = context.getPackageManager();
- try {
- return context.getString(R.string.abc_slices_permission_request,
- pm.getApplicationInfo(callingPackage, 0).loadLabel(pm),
- context.getApplicationInfo().loadLabel(pm));
- } catch (PackageManager.NameNotFoundException e) {
- // This shouldn't be possible since the caller is verified.
- throw new RuntimeException("Unknown calling app", e);
- }
- }
-
private Slice onBindSliceStrict(Uri sliceUri, Set<SliceSpec> specs) {
- ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
+ StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
mCallback = "onBindSlice";
mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
try {
@@ -326,7 +262,7 @@
.build());
SliceProvider.setSpecs(specs);
try {
- return mSliceProvider.onBindSlice(sliceUri);
+ return mProvider.onBindSlice(sliceUri);
} finally {
SliceProvider.setSpecs(null);
mHandler.removeCallbacks(mAnr);
@@ -336,21 +272,28 @@
}
}
+ private final Runnable mAnr = new Runnable() {
+ @Override
+ public void run() {
+ Process.sendSignal(Process.myPid(), Process.SIGNAL_QUIT);
+ Log.wtf(TAG, "Timed out while handling slice callback " + mCallback);
+ }
+ };
+
/**
* Compat version of {@link Slice#bindSlice}.
*/
public static Slice bindSlice(Context context, Uri uri,
Set<SliceSpec> supportedSpecs) {
- ContentProviderClient provider = context.getContentResolver()
- .acquireContentProviderClient(uri);
- if (provider == null) {
+ ProviderHolder holder = acquireClient(context.getContentResolver(), uri);
+ if (holder.mProvider == null) {
throw new IllegalArgumentException("Unknown URI " + uri);
}
try {
Bundle extras = new Bundle();
extras.putParcelable(EXTRA_BIND_URI, uri);
addSpecs(extras, supportedSpecs);
- final Bundle res = provider.call(METHOD_SLICE, null, extras);
+ final Bundle res = holder.mProvider.call(METHOD_SLICE, null, extras);
if (res == null) {
return null;
}
@@ -360,19 +303,16 @@
}
return new Slice((Bundle) bundle);
} catch (RemoteException e) {
- // Arbitrary and not worth documenting, as Activity
- // Manager will kill this process shortly anyway.
+ Log.e(TAG, "Unable to bind slice", e);
return null;
} finally {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- provider.close();
- } else {
- provider.release();
- }
}
}
- private static void addSpecs(Bundle extras, Set<SliceSpec> supportedSpecs) {
+ /**
+ * Compat way to push specs through the call.
+ */
+ public static void addSpecs(Bundle extras, Set<SliceSpec> supportedSpecs) {
ArrayList<String> types = new ArrayList<>();
ArrayList<Integer> revs = new ArrayList<>();
for (SliceSpec spec : supportedSpecs) {
@@ -383,7 +323,10 @@
extras.putIntegerArrayList(EXTRA_SUPPORTED_SPECS_REVS, revs);
}
- private static Set<SliceSpec> getSpecs(Bundle extras) {
+ /**
+ * Compat way to push specs through the call.
+ */
+ public static Set<SliceSpec> getSpecs(Bundle extras) {
ArraySet<SliceSpec> specs = new ArraySet<>();
ArrayList<String> types = extras.getStringArrayList(EXTRA_SUPPORTED_SPECS);
ArrayList<Integer> revs = extras.getIntegerArrayList(EXTRA_SUPPORTED_SPECS_REVS);
@@ -398,6 +341,10 @@
*/
public static Slice bindSlice(Context context, Intent intent,
Set<SliceSpec> supportedSpecs) {
+ Preconditions.checkNotNull(intent, "intent");
+ Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null
+ || intent.getData() != null,
+ String.format("Slice intent must be explicit %s", intent));
ContentResolver resolver = context.getContentResolver();
// Check if the intent has data for the slice uri on it and use that
@@ -406,23 +353,37 @@
return bindSlice(context, intentData, supportedSpecs);
}
// Otherwise ask the app
+ Intent queryIntent = new Intent(intent);
+ if (!queryIntent.hasCategory(CATEGORY_SLICE)) {
+ queryIntent.addCategory(CATEGORY_SLICE);
+ }
List<ResolveInfo> providers =
- context.getPackageManager().queryIntentContentProviders(intent, 0);
- if (providers == null) {
- throw new IllegalArgumentException("Unable to resolve intent " + intent);
+ context.getPackageManager().queryIntentContentProviders(queryIntent, 0);
+ if (providers == null || providers.isEmpty()) {
+ // There are no providers, see if this activity has a direct link.
+ ResolveInfo resolve = context.getPackageManager().resolveActivity(intent,
+ PackageManager.GET_META_DATA);
+ if (resolve != null && resolve.activityInfo != null
+ && resolve.activityInfo.metaData != null
+ && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) {
+ return bindSlice(context, Uri.parse(
+ resolve.activityInfo.metaData.getString(SLICE_METADATA_KEY)),
+ supportedSpecs);
+ }
+ return null;
}
String authority = providers.get(0).providerInfo.authority;
Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
.authority(authority).build();
- ContentProviderClient provider = resolver.acquireContentProviderClient(uri);
- if (provider == null) {
+ ProviderHolder holder = acquireClient(resolver, uri);
+ if (holder.mProvider == null) {
throw new IllegalArgumentException("Unknown URI " + uri);
}
try {
Bundle extras = new Bundle();
extras.putParcelable(EXTRA_INTENT, intent);
addSpecs(extras, supportedSpecs);
- final Bundle res = provider.call(METHOD_MAP_INTENT, null, extras);
+ final Bundle res = holder.mProvider.call(METHOD_MAP_INTENT, null, extras);
if (res == null) {
return null;
}
@@ -432,11 +393,8 @@
}
return new Slice((Bundle) bundle);
} catch (RemoteException e) {
- // Arbitrary and not worth documenting, as Activity
- // Manager will kill this process shortly anyway.
+ Log.e(TAG, "Unable to bind slice", e);
return null;
- } finally {
- provider.close();
}
}
@@ -445,9 +403,8 @@
*/
public static void pinSlice(Context context, Uri uri,
Set<SliceSpec> supportedSpecs) {
- ContentProviderClient provider = context.getContentResolver()
- .acquireContentProviderClient(uri);
- if (provider == null) {
+ ProviderHolder holder = acquireClient(context.getContentResolver(), uri);
+ if (holder.mProvider == null) {
throw new IllegalArgumentException("Unknown URI " + uri);
}
try {
@@ -455,12 +412,9 @@
extras.putParcelable(EXTRA_BIND_URI, uri);
extras.putString(EXTRA_PKG, context.getPackageName());
addSpecs(extras, supportedSpecs);
- provider.call(METHOD_PIN, null, extras);
+ holder.mProvider.call(METHOD_PIN, null, extras);
} catch (RemoteException e) {
- // Arbitrary and not worth documenting, as Activity
- // Manager will kill this process shortly anyway.
- } finally {
- provider.close();
+ Log.e(TAG, "Unable to pin slice", e);
}
}
@@ -469,9 +423,8 @@
*/
public static void unpinSlice(Context context, Uri uri,
Set<SliceSpec> supportedSpecs) {
- ContentProviderClient provider = context.getContentResolver()
- .acquireContentProviderClient(uri);
- if (provider == null) {
+ ProviderHolder holder = acquireClient(context.getContentResolver(), uri);
+ if (holder.mProvider == null) {
throw new IllegalArgumentException("Unknown URI " + uri);
}
try {
@@ -479,12 +432,9 @@
extras.putParcelable(EXTRA_BIND_URI, uri);
extras.putString(EXTRA_PKG, context.getPackageName());
addSpecs(extras, supportedSpecs);
- provider.call(METHOD_UNPIN, null, extras);
+ holder.mProvider.call(METHOD_UNPIN, null, extras);
} catch (RemoteException e) {
- // Arbitrary and not worth documenting, as Activity
- // Manager will kill this process shortly anyway.
- } finally {
- provider.close();
+ Log.e(TAG, "Unable to unpin slice", e);
}
}
@@ -492,26 +442,21 @@
* Compat version of {@link android.app.slice.SliceManager#getPinnedSpecs(Uri)}.
*/
public static Set<SliceSpec> getPinnedSpecs(Context context, Uri uri) {
- ContentProviderClient provider = context.getContentResolver()
- .acquireContentProviderClient(uri);
- if (provider == null) {
+ ProviderHolder holder = acquireClient(context.getContentResolver(), uri);
+ if (holder.mProvider == null) {
throw new IllegalArgumentException("Unknown URI " + uri);
}
try {
Bundle extras = new Bundle();
extras.putParcelable(EXTRA_BIND_URI, uri);
- final Bundle res = provider.call(METHOD_GET_PINNED_SPECS, null, extras);
- if (res == null) {
- return null;
+ final Bundle res = holder.mProvider.call(METHOD_GET_PINNED_SPECS, null, extras);
+ if (res != null) {
+ return getSpecs(res);
}
- return getSpecs(res);
} catch (RemoteException e) {
- // Arbitrary and not worth documenting, as Activity
- // Manager will kill this process shortly anyway.
- return null;
- } finally {
- provider.close();
+ Log.e(TAG, "Unable to get pinned specs", e);
}
+ return null;
}
/**
@@ -519,7 +464,8 @@
*/
public static Uri mapIntentToUri(Context context, Intent intent) {
Preconditions.checkNotNull(intent, "intent");
- Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null,
+ Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null
+ || intent.getData() != null,
String.format("Slice intent must be explicit %s", intent));
ContentResolver resolver = context.getContentResolver();
@@ -529,39 +475,41 @@
return intentData;
}
// Otherwise ask the app
+ Intent queryIntent = new Intent(intent);
+ if (!queryIntent.hasCategory(CATEGORY_SLICE)) {
+ queryIntent.addCategory(CATEGORY_SLICE);
+ }
List<ResolveInfo> providers =
- context.getPackageManager().queryIntentContentProviders(intent, 0);
+ context.getPackageManager().queryIntentContentProviders(queryIntent, 0);
if (providers == null || providers.isEmpty()) {
// There are no providers, see if this activity has a direct link.
ResolveInfo resolve = context.getPackageManager().resolveActivity(intent,
PackageManager.GET_META_DATA);
if (resolve != null && resolve.activityInfo != null
&& resolve.activityInfo.metaData != null
- && resolve.activityInfo.metaData.containsKey(SliceHints.SLICE_METADATA_KEY)) {
+ && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) {
return Uri.parse(
- resolve.activityInfo.metaData.getString(SliceHints.SLICE_METADATA_KEY));
+ resolve.activityInfo.metaData.getString(SLICE_METADATA_KEY));
}
return null;
}
String authority = providers.get(0).providerInfo.authority;
Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
.authority(authority).build();
- try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
- if (provider == null) {
+ try (ProviderHolder holder = acquireClient(resolver, uri)) {
+ if (holder.mProvider == null) {
throw new IllegalArgumentException("Unknown URI " + uri);
}
Bundle extras = new Bundle();
extras.putParcelable(EXTRA_INTENT, intent);
- final Bundle res = provider.call(METHOD_MAP_ONLY_INTENT, null, extras);
- if (res == null) {
- return null;
+ final Bundle res = holder.mProvider.call(METHOD_MAP_ONLY_INTENT, null, extras);
+ if (res != null) {
+ return res.getParcelable(EXTRA_SLICE);
}
- return res.getParcelable(EXTRA_SLICE);
} catch (RemoteException e) {
- // Arbitrary and not worth documenting, as Activity
- // Manager will kill this process shortly anyway.
- return null;
+ Log.e(TAG, "Unable to map slice", e);
}
+ return null;
}
/**
@@ -569,22 +517,114 @@
*/
public static @NonNull Collection<Uri> getSliceDescendants(Context context, @NonNull Uri uri) {
ContentResolver resolver = context.getContentResolver();
- try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
+ try (ProviderHolder holder = acquireClient(resolver, uri)) {
Bundle extras = new Bundle();
extras.putParcelable(EXTRA_BIND_URI, uri);
- final Bundle res = provider.call(METHOD_GET_DESCENDANTS, null, extras);
- return res.getParcelableArrayList(EXTRA_SLICE_DESCENDANTS);
+ final Bundle res = holder.mProvider.call(METHOD_GET_DESCENDANTS, null, extras);
+ if (res != null) {
+ return res.getParcelableArrayList(EXTRA_SLICE_DESCENDANTS);
+ }
} catch (RemoteException e) {
Log.e(TAG, "Unable to get slice descendants", e);
}
return Collections.emptyList();
}
- private final Runnable mAnr = new Runnable() {
- @Override
- public void run() {
- Process.sendSignal(Process.myPid(), Process.SIGNAL_QUIT);
- Log.wtf(TAG, "Timed out while handling slice callback " + mCallback);
+ /**
+ * Compat version of {@link android.app.slice.SliceManager#checkSlicePermission}.
+ */
+ public static int checkSlicePermission(Context context, String packageName, Uri uri, int pid,
+ int uid) {
+ ContentResolver resolver = context.getContentResolver();
+ try (ProviderHolder holder = acquireClient(resolver, uri)) {
+ Bundle extras = new Bundle();
+ extras.putParcelable(EXTRA_BIND_URI, uri);
+ extras.putString(EXTRA_PKG, packageName);
+ extras.putInt(EXTRA_PID, pid);
+ extras.putInt(EXTRA_UID, uid);
+
+ final Bundle res = holder.mProvider.call(METHOD_CHECK_PERMISSION, null, extras);
+ if (res != null) {
+ return res.getInt(EXTRA_RESULT);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to check slice permission", e);
}
- };
+ return PERMISSION_DENIED;
+ }
+
+ /**
+ * Compat version of {@link android.app.slice.SliceManager#grantSlicePermission}.
+ */
+ public static void grantSlicePermission(Context context, String packageName, String toPackage,
+ Uri uri) {
+ ContentResolver resolver = context.getContentResolver();
+ try (ProviderHolder holder = acquireClient(resolver, uri)) {
+ Bundle extras = new Bundle();
+ extras.putParcelable(EXTRA_BIND_URI, uri);
+ extras.putString(EXTRA_PROVIDER_PKG, packageName);
+ extras.putString(EXTRA_PKG, toPackage);
+
+ holder.mProvider.call(METHOD_GRANT_PERMISSION, null, extras);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to get slice descendants", e);
+ }
+ }
+
+ /**
+ * Compat version of {@link android.app.slice.SliceManager#revokeSlicePermission}.
+ */
+ public static void revokeSlicePermission(Context context, String packageName, String toPackage,
+ Uri uri) {
+ ContentResolver resolver = context.getContentResolver();
+ try (ProviderHolder holder = acquireClient(resolver, uri)) {
+ Bundle extras = new Bundle();
+ extras.putParcelable(EXTRA_BIND_URI, uri);
+ extras.putString(EXTRA_PROVIDER_PKG, packageName);
+ extras.putString(EXTRA_PKG, toPackage);
+
+ holder.mProvider.call(METHOD_REVOKE_PERMISSION, null, extras);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to get slice descendants", e);
+ }
+ }
+
+ /**
+ * Compat version of {@link android.app.slice.SliceManager#getPinnedSlices}.
+ */
+ public static List<Uri> getPinnedSlices(Context context) {
+ ArrayList<Uri> pinnedSlices = new ArrayList<>();
+ SharedPreferences prefs = context.getSharedPreferences(ALL_FILES, 0);
+ Set<String> prefSet = prefs.getStringSet(ALL_FILES, Collections.<String>emptySet());
+ for (String pref : prefSet) {
+ pinnedSlices.addAll(new CompatPinnedList(context, pref).getPinnedSlices());
+ }
+ return pinnedSlices;
+ }
+
+ private static ProviderHolder acquireClient(ContentResolver resolver, Uri uri) {
+ ContentProviderClient provider = resolver.acquireContentProviderClient(uri);
+ if (provider == null) {
+ throw new IllegalArgumentException("No provider found for " + uri);
+ }
+ return new ProviderHolder(provider);
+ }
+
+ private static class ProviderHolder implements AutoCloseable {
+ private final ContentProviderClient mProvider;
+
+ ProviderHolder(ContentProviderClient provider) {
+ this.mProvider = provider;
+ }
+
+ @Override
+ public void close() {
+ if (mProvider == null) return;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ mProvider.close();
+ } else {
+ mProvider.release();
+ }
+ }
+ }
}
diff --git a/slices/core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java b/slices/core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java
index 5118cc7..78eea93 100644
--- a/slices/core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java
+++ b/slices/core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java
@@ -22,7 +22,9 @@
import android.app.slice.Slice;
import android.app.slice.SliceProvider;
import android.app.slice.SliceSpec;
+import android.content.Context;
import android.content.Intent;
+import android.content.pm.ProviderInfo;
import android.net.Uri;
import androidx.annotation.NonNull;
@@ -46,13 +48,21 @@
private androidx.slice.SliceProvider mSliceProvider;
- public SliceProviderWrapper(androidx.slice.SliceProvider provider) {
+ public SliceProviderWrapper(androidx.slice.SliceProvider provider,
+ String[] autoGrantPermissions) {
+ super(autoGrantPermissions);
mSliceProvider = provider;
}
@Override
+ public void attachInfo(Context context, ProviderInfo info) {
+ mSliceProvider.attachInfo(context, info);
+ super.attachInfo(context, info);
+ }
+
+ @Override
public boolean onCreate() {
- return mSliceProvider.onCreateSliceProvider();
+ return true;
}
@Override
diff --git a/slices/core/src/main/java/androidx/slice/core/SliceActionImpl.java b/slices/core/src/main/java/androidx/slice/core/SliceActionImpl.java
index b5ca913..4fb81a6 100644
--- a/slices/core/src/main/java/androidx/slice/core/SliceActionImpl.java
+++ b/slices/core/src/main/java/androidx/slice/core/SliceActionImpl.java
@@ -27,7 +27,6 @@
import static android.app.slice.SliceItem.FORMAT_ACTION;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
import static android.app.slice.SliceItem.FORMAT_INT;
-import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TEXT;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
@@ -64,6 +63,7 @@
private boolean mIsChecked;
private int mPriority = -1;
private SliceItem mSliceItem;
+ private SliceItem mActionItem;
/**
* Construct a SliceAction representing a tappable icon.
@@ -147,40 +147,36 @@
@RestrictTo(LIBRARY)
public SliceActionImpl(SliceItem slice) {
mSliceItem = slice;
- if (slice.hasHint(HINT_SHORTCUT) && FORMAT_SLICE.equals(slice.getFormat())) {
- SliceItem actionItem = SliceQuery.find(slice, FORMAT_ACTION);
- if (actionItem == null) {
- // Can't have action slice without action
- return;
- }
- mAction = actionItem.getAction();
- SliceItem iconItem = SliceQuery.find(actionItem.getSlice(), FORMAT_IMAGE);
- if (iconItem != null) {
- mIcon = iconItem.getIcon();
- mImageMode = iconItem.hasHint(HINT_NO_TINT)
- ? iconItem.hasHint(HINT_LARGE) ? LARGE_IMAGE : SMALL_IMAGE
- : ICON_IMAGE;
- }
- SliceItem titleItem = SliceQuery.find(actionItem.getSlice(), FORMAT_TEXT, HINT_TITLE,
- null /* nonHints */);
- if (titleItem != null) {
- mTitle = titleItem.getText();
- }
- SliceItem cdItem = SliceQuery.findSubtype(actionItem.getSlice(), FORMAT_TEXT,
- SUBTYPE_CONTENT_DESCRIPTION);
- if (cdItem != null) {
- mContentDescription = cdItem.getText();
- }
- mIsToggle = SUBTYPE_TOGGLE.equals(actionItem.getSubType());
- if (mIsToggle) {
- mIsChecked = actionItem.hasHint(HINT_SELECTED);
- }
- SliceItem priority = SliceQuery.findSubtype(actionItem.getSlice(), FORMAT_INT,
- SUBTYPE_PRIORITY);
- mPriority = priority != null ? priority.getInt() : -1;
- } else if (FORMAT_ACTION.equals(slice.getFormat())) {
- mAction = slice.getAction();
+ SliceItem actionItem = SliceQuery.find(slice, FORMAT_ACTION);
+ if (actionItem == null) {
+ // Can't have action slice without action
+ return;
}
+ mActionItem = actionItem;
+ SliceItem iconItem = SliceQuery.find(actionItem.getSlice(), FORMAT_IMAGE);
+ if (iconItem != null) {
+ mIcon = iconItem.getIcon();
+ mImageMode = iconItem.hasHint(HINT_NO_TINT)
+ ? iconItem.hasHint(HINT_LARGE) ? LARGE_IMAGE : SMALL_IMAGE
+ : ICON_IMAGE;
+ }
+ SliceItem titleItem = SliceQuery.find(actionItem.getSlice(), FORMAT_TEXT, HINT_TITLE,
+ null /* nonHints */);
+ if (titleItem != null) {
+ mTitle = titleItem.getText();
+ }
+ SliceItem cdItem = SliceQuery.findSubtype(actionItem.getSlice(), FORMAT_TEXT,
+ SUBTYPE_CONTENT_DESCRIPTION);
+ if (cdItem != null) {
+ mContentDescription = cdItem.getText();
+ }
+ mIsToggle = SUBTYPE_TOGGLE.equals(actionItem.getSubType());
+ if (mIsToggle) {
+ mIsChecked = actionItem.hasHint(HINT_SELECTED);
+ }
+ SliceItem priority = SliceQuery.findSubtype(actionItem.getSlice(), FORMAT_INT,
+ SUBTYPE_PRIORITY);
+ mPriority = priority != null ? priority.getInt() : -1;
}
/**
@@ -218,7 +214,15 @@
@NonNull
@Override
public PendingIntent getAction() {
- return mAction;
+ return mAction != null ? mAction : mActionItem.getAction();
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public SliceItem getActionItem() {
+ return mActionItem;
}
/**
diff --git a/slices/core/src/main/java/androidx/slice/core/SliceHints.java b/slices/core/src/main/java/androidx/slice/core/SliceHints.java
index 588ff61..c29ad56 100644
--- a/slices/core/src/main/java/androidx/slice/core/SliceHints.java
+++ b/slices/core/src/main/java/androidx/slice/core/SliceHints.java
@@ -18,9 +18,13 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
import androidx.annotation.IntDef;
import androidx.annotation.RestrictTo;
+import java.lang.annotation.Retention;
+
/**
* Temporary class to contain hint constants for slices to be used.
* @hide
@@ -69,6 +73,7 @@
@IntDef({
LARGE_IMAGE, SMALL_IMAGE, ICON_IMAGE, UNKNOWN_IMAGE
})
+ @Retention(SOURCE)
public @interface ImageMode{}
/**
diff --git a/slices/core/src/main/res/values-as/strings.xml b/slices/core/src/main/res/values-as/strings.xml
new file mode 100644
index 0000000..95722b9
--- /dev/null
+++ b/slices/core/src/main/res/values-as/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+~ Copyright 2018 The Android Open Source Project
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_slices_permission_request" msgid="3604847235923472451">"<xliff:g id="APP_0">%1$s</xliff:g>এ <xliff:g id="APP_2">%2$s</xliff:g>ৰ অংশ দেখুওৱাব খুজিছে"</string>
+ <string name="abc_slice_permission_title" msgid="4175332421259324948">"<xliff:g id="APP_0">%1$s</xliff:g>ক <xliff:g id="APP_2">%2$s</xliff:g>ৰ অংশ দেখুওৱাবলৈ অনুমতি দিবনে?"</string>
+ <string name="abc_slice_permission_text_1" msgid="4525743640399572811">"- ই <xliff:g id="APP">%1$s</xliff:g>ৰ তথ্য পঢ়িব পাৰে"</string>
+ <string name="abc_slice_permission_text_2" msgid="7323565634860251794">"- ই <xliff:g id="APP">%1$s</xliff:g>ৰ ভিতৰত কাৰ্য কৰিব পাৰে"</string>
+ <string name="abc_slice_permission_checkbox" msgid="5696872682700058611">"<xliff:g id="APP">%1$s</xliff:g>ক যিকোনো এপৰ অংশ দেখুওৱাবলৈ অনুমতি দিয়ক"</string>
+ <string name="abc_slice_permission_allow" msgid="5024599872061409708">"অনুমতি দিয়ক"</string>
+ <string name="abc_slice_permission_deny" msgid="3819478292430407705">"অস্বীকাৰ কৰক"</string>
+</resources>
diff --git a/slices/core/src/main/res/values-be/strings.xml b/slices/core/src/main/res/values-be/strings.xml
index c9fc9a8..5fadc3f 100644
--- a/slices/core/src/main/res/values-be/strings.xml
+++ b/slices/core/src/main/res/values-be/strings.xml
@@ -17,11 +17,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="abc_slices_permission_request" msgid="3604847235923472451">"Праграма <xliff:g id="APP_0">%1$s</xliff:g> запытвае дазвол на паказ зрэзаў праграмы <xliff:g id="APP_2">%2$s</xliff:g>"</string>
- <string name="abc_slice_permission_title" msgid="4175332421259324948">"Дазволіць праграме <xliff:g id="APP_0">%1$s</xliff:g> паказваць зрэзы праграмы <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
+ <string name="abc_slices_permission_request" msgid="3604847235923472451">"Праграма <xliff:g id="APP_0">%1$s</xliff:g> запытвае дазвол на паказ фрагментаў праграмы <xliff:g id="APP_2">%2$s</xliff:g>"</string>
+ <string name="abc_slice_permission_title" msgid="4175332421259324948">"Дазволіць праграме <xliff:g id="APP_0">%1$s</xliff:g> паказваць фрагменты праграмы <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
<string name="abc_slice_permission_text_1" msgid="4525743640399572811">"- Можа счытваць інфармацыю з праграмы <xliff:g id="APP">%1$s</xliff:g>"</string>
<string name="abc_slice_permission_text_2" msgid="7323565634860251794">"- Можа выконваць дзеянні ў праграме <xliff:g id="APP">%1$s</xliff:g>"</string>
- <string name="abc_slice_permission_checkbox" msgid="5696872682700058611">"Дазволіць праграме <xliff:g id="APP">%1$s</xliff:g> паказваць зрэзы іншых праграм"</string>
+ <string name="abc_slice_permission_checkbox" msgid="5696872682700058611">"Дазволіць праграме <xliff:g id="APP">%1$s</xliff:g> паказваць фрагменты іншых праграм"</string>
<string name="abc_slice_permission_allow" msgid="5024599872061409708">"Дазволіць"</string>
<string name="abc_slice_permission_deny" msgid="3819478292430407705">"Адмовіць"</string>
</resources>
diff --git a/slices/core/src/main/res/values-ja/strings.xml b/slices/core/src/main/res/values-ja/strings.xml
index a04df0b..315326f 100644
--- a/slices/core/src/main/res/values-ja/strings.xml
+++ b/slices/core/src/main/res/values-ja/strings.xml
@@ -19,7 +19,7 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slices_permission_request" msgid="3604847235923472451">"<xliff:g id="APP_0">%1$s</xliff:g> が <xliff:g id="APP_2">%2$s</xliff:g> のスライスの表示をリクエストしています"</string>
<string name="abc_slice_permission_title" msgid="4175332421259324948">"<xliff:g id="APP_2">%2$s</xliff:g> のスライスの表示を <xliff:g id="APP_0">%1$s</xliff:g> に許可しますか?"</string>
- <string name="abc_slice_permission_text_1" msgid="4525743640399572811">"- <xliff:g id="APP">%1$s</xliff:g> からの情報を読み取ることがあります"</string>
+ <string name="abc_slice_permission_text_1" msgid="4525743640399572811">"- <xliff:g id="APP">%1$s</xliff:g> からの情報を読み取ることができます"</string>
<string name="abc_slice_permission_text_2" msgid="7323565634860251794">"- <xliff:g id="APP">%1$s</xliff:g> 内部で操作することがあります"</string>
<string name="abc_slice_permission_checkbox" msgid="5696872682700058611">"すべてのアプリのスライスを表示することを <xliff:g id="APP">%1$s</xliff:g> に許可する"</string>
<string name="abc_slice_permission_allow" msgid="5024599872061409708">"許可"</string>
diff --git a/slices/core/src/main/res/values-or/strings.xml b/slices/core/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000..f6b9443
--- /dev/null
+++ b/slices/core/src/main/res/values-or/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+~ Copyright 2018 The Android Open Source Project
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_slices_permission_request" msgid="3604847235923472451">"<xliff:g id="APP_0">%1$s</xliff:g>, <xliff:g id="APP_2">%2$s</xliff:g> ସ୍ଲାଇସ୍କୁ ଦେଖାଇବା ପାଇଁ ଚାହେଁ"</string>
+ <string name="abc_slice_permission_title" msgid="4175332421259324948">"<xliff:g id="APP_2">%2$s</xliff:g> ସ୍ଲାଇସ୍କୁ ଦେଖାଇବା ପାଇଁ <xliff:g id="APP_0">%1$s</xliff:g>କୁ ଅନୁମତି ଦେବେ?"</string>
+ <string name="abc_slice_permission_text_1" msgid="4525743640399572811">"- ଏହା <xliff:g id="APP">%1$s</xliff:g>ରୁ ସୂଚନାକୁ ପଢ଼ିପାରିବ"</string>
+ <string name="abc_slice_permission_text_2" msgid="7323565634860251794">"- ଏହା <xliff:g id="APP">%1$s</xliff:g> ଭିତରେ କାମ କରିପାରିବ"</string>
+ <string name="abc_slice_permission_checkbox" msgid="5696872682700058611">"ଯେକୌଣସି ଆପ୍ରେ ସ୍ଲାଇସ୍କୁ ଦେଖାଇବା ପାଇଁ <xliff:g id="APP">%1$s</xliff:g>କୁ ଅନୁମତି ଦିଅନ୍ତୁ"</string>
+ <string name="abc_slice_permission_allow" msgid="5024599872061409708">"ଅନୁମତି ଦିଅନ୍ତୁ"</string>
+ <string name="abc_slice_permission_deny" msgid="3819478292430407705">"ଅସ୍ଵୀକାର କରନ୍ତୁ"</string>
+</resources>
diff --git a/slices/view/api/current.txt b/slices/view/api/current.txt
index 04d4960..7be0368 100644
--- a/slices/view/api/current.txt
+++ b/slices/view/api/current.txt
@@ -5,6 +5,8 @@
method public abstract androidx.slice.Slice bindSlice(android.content.Intent);
method public abstract int checkSlicePermission(android.net.Uri, int, int);
method public static androidx.slice.SliceManager getInstance(android.content.Context);
+ method public abstract java.util.List<android.net.Uri> getPinnedSlices();
+ method public abstract java.util.Set<androidx.slice.SliceSpec> getPinnedSpecs(android.net.Uri);
method public abstract java.util.Collection<android.net.Uri> getSliceDescendants(android.net.Uri);
method public abstract void grantSlicePermission(java.lang.String, android.net.Uri);
method public abstract android.net.Uri mapIntentToUri(android.content.Intent);
@@ -24,6 +26,7 @@
method public static androidx.slice.SliceMetadata from(android.content.Context, androidx.slice.Slice);
method public long getExpiry();
method public int getHeaderType();
+ method public android.app.PendingIntent getInputRangeAction();
method public long getLastUpdatedTime();
method public int getLoadingState();
method public androidx.slice.core.SliceAction getPrimaryAction();
@@ -43,8 +46,8 @@
method public static deprecated int getLoadingState(androidx.slice.Slice);
method public static deprecated java.util.List<androidx.slice.SliceItem> getSliceActions(androidx.slice.Slice);
method public static deprecated java.util.List<java.lang.String> getSliceKeywords(androidx.slice.Slice);
- method public static androidx.slice.Slice parseSlice(java.io.InputStream, java.lang.String) throws java.io.IOException;
- method public static void serializeSlice(androidx.slice.Slice, android.content.Context, java.io.OutputStream, java.lang.String, androidx.slice.SliceUtils.SerializeOptions) throws java.io.IOException;
+ method public static androidx.slice.Slice parseSlice(android.content.Context, java.io.InputStream, java.lang.String, androidx.slice.SliceUtils.SliceActionListener) throws java.io.IOException, androidx.slice.SliceUtils.SliceParseException;
+ method public static void serializeSlice(androidx.slice.Slice, android.content.Context, java.io.OutputStream, java.lang.String, androidx.slice.SliceUtils.SerializeOptions) throws java.io.IOException, java.lang.IllegalArgumentException;
field public static final deprecated int LOADING_ALL = 0; // 0x0
field public static final deprecated int LOADING_COMPLETE = 2; // 0x2
field public static final deprecated int LOADING_PARTIAL = 1; // 0x1
@@ -54,11 +57,20 @@
ctor public SliceUtils.SerializeOptions();
method public androidx.slice.SliceUtils.SerializeOptions setActionMode(int);
method public androidx.slice.SliceUtils.SerializeOptions setImageMode(int);
- field public static final int MODE_DISABLE = 2; // 0x2
+ method public androidx.slice.SliceUtils.SerializeOptions setMaxImageHeight(int);
+ method public androidx.slice.SliceUtils.SerializeOptions setMaxImageWidth(int);
+ field public static final int MODE_CONVERT = 2; // 0x2
field public static final int MODE_REMOVE = 1; // 0x1
field public static final int MODE_THROW = 0; // 0x0
}
+ public static abstract interface SliceUtils.SliceActionListener {
+ method public abstract void onSliceAction(android.net.Uri);
+ }
+
+ public static class SliceUtils.SliceParseException extends java.lang.Exception {
+ }
+
}
package androidx.slice.widget {
@@ -108,12 +120,13 @@
method public java.util.List<androidx.slice.SliceItem> getSliceActions();
method public void onChanged(androidx.slice.Slice);
method public void onClick(android.view.View);
+ method public void setAccentColor(int);
method public void setMode(int);
method public void setOnSliceActionListener(androidx.slice.widget.SliceView.OnSliceActionListener);
method public void setScrollable(boolean);
method public void setSlice(androidx.slice.Slice);
method public void setSliceActions(java.util.List<androidx.slice.SliceItem>);
- method public void setTint(int);
+ method public deprecated void setTint(int);
field public static final int MODE_LARGE = 2; // 0x2
field public static final int MODE_SHORTCUT = 3; // 0x3
field public static final int MODE_SMALL = 1; // 0x1
diff --git a/slices/view/src/androidTest/AndroidManifest.xml b/slices/view/src/androidTest/AndroidManifest.xml
index 78f3ad8..7c90f90 100644
--- a/slices/view/src/androidTest/AndroidManifest.xml
+++ b/slices/view/src/androidTest/AndroidManifest.xml
@@ -27,6 +27,7 @@
android:exported="true">
<intent-filter>
<action android:name="androidx.slice.action.TEST" />
+ <category android:name="android.app.slice.category.SLICE" />
</intent-filter>
</provider>
diff --git a/slices/view/src/androidTest/java/androidx/slice/SliceManagerTest.java b/slices/view/src/androidTest/java/androidx/slice/SliceManagerTest.java
index 5564f72..e0e8f51 100644
--- a/slices/view/src/androidTest/java/androidx/slice/SliceManagerTest.java
+++ b/slices/view/src/androidTest/java/androidx/slice/SliceManagerTest.java
@@ -16,7 +16,10 @@
package androidx.slice;
+import static androidx.slice.compat.SliceProviderCompat.PERMS_PREFIX;
+
import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
@@ -36,8 +39,8 @@
import androidx.annotation.NonNull;
import androidx.core.os.BuildCompat;
+import androidx.slice.compat.CompatPermissionManager;
import androidx.slice.render.SliceRenderActivity;
-import androidx.slice.widget.SliceLiveData;
import org.junit.Before;
import org.junit.Test;
@@ -45,6 +48,7 @@
import java.util.Arrays;
import java.util.Collection;
+import java.util.List;
import java.util.concurrent.Executor;
@RunWith(AndroidJUnit4.class)
@@ -89,6 +93,28 @@
}
@Test
+ public void testPinList() {
+ Uri uri = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(mContext.getPackageName())
+ .build();
+ Uri longerUri = uri.buildUpon().appendPath("something").build();
+ try {
+ mManager.pinSlice(uri);
+ mManager.pinSlice(longerUri);
+ verify(mSliceProvider, timeout(2000)).onSlicePinned(eq(longerUri));
+
+ List<Uri> uris = mManager.getPinnedSlices();
+ assertEquals(2, uris.size());
+ assertTrue(uris.contains(uri));
+ assertTrue(uris.contains(longerUri));
+ } finally {
+ mManager.unpinSlice(uri);
+ mManager.unpinSlice(longerUri);
+ }
+ }
+
+ @Test
public void testCallback() {
if (BuildCompat.isAtLeastP()) {
return;
@@ -124,7 +150,8 @@
mManager.pinSlice(uri);
verify(mSliceProvider).onSlicePinned(eq(uri));
- assertEquals(SliceLiveData.SUPPORTED_SPECS, mManager.getPinnedSpecs(uri));
+ // Disabled while we update APIs.
+ //assertEquals(SliceLiveData.SUPPORTED_SPECS, mManager.getPinnedSpecs(uri));
}
@Test
@@ -145,8 +172,8 @@
when(mSliceProvider.onMapIntentToUri(eq(intent))).thenReturn(expected);
Uri uri = mManager.mapIntentToUri(intent);
- assertEquals(expected, uri);
verify(mSliceProvider).onMapIntentToUri(eq(intent));
+ assertEquals(expected, uri);
}
@Test
@@ -212,6 +239,12 @@
}
}
+ protected CompatPermissionManager onCreatePermissionManager(
+ String[] autoGrantPermissions) {
+ return new CompatPermissionManager(getContext(), PERMS_PREFIX + getClass().getName(),
+ -1 /* Different uid to run permissions */, autoGrantPermissions);
+ }
+
@Override
public Collection<Uri> onGetSliceDescendants(Uri uri) {
if (sSliceProviderReceiver != null) {
diff --git a/slices/view/src/androidTest/java/androidx/slice/SliceMetadataTest.java b/slices/view/src/androidTest/java/androidx/slice/SliceMetadataTest.java
index 2662368..78c8cf6 100644
--- a/slices/view/src/androidTest/java/androidx/slice/SliceMetadataTest.java
+++ b/slices/view/src/androidTest/java/androidx/slice/SliceMetadataTest.java
@@ -16,7 +16,6 @@
package androidx.slice;
-import static android.app.slice.Slice.HINT_PARTIAL;
import static android.app.slice.Slice.HINT_TITLE;
import static androidx.slice.SliceMetadata.LOADED_ALL;
@@ -24,6 +23,7 @@
import static androidx.slice.SliceMetadata.LOADED_PARTIAL;
import static androidx.slice.builders.ListBuilder.ICON_IMAGE;
import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
+import static androidx.slice.core.SliceHints.INFINITY;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;
@@ -36,7 +36,6 @@
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
-import android.graphics.drawable.Icon;
import android.net.Uri;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
@@ -47,7 +46,6 @@
import androidx.slice.builders.GridRowBuilder;
import androidx.slice.builders.ListBuilder;
import androidx.slice.builders.SliceAction;
-import androidx.slice.compat.SliceProviderCompat;
import androidx.slice.core.SliceActionImpl;
import androidx.slice.core.SliceHints;
import androidx.slice.render.SliceRenderActivity;
@@ -93,7 +91,7 @@
PendingIntent pi = getIntent("");
Bitmap b = Bitmap.createBitmap(50, 25, Bitmap.Config.ARGB_8888);
new Canvas(b).drawColor(0xffff0000);
- IconCompat icon = IconCompat.createFromIcon(Icon.createWithBitmap(b));
+ IconCompat icon = IconCompat.createWithBitmap(b);
SliceAction action1 = new SliceAction(pi, icon, "action1");
SliceAction action2 = new SliceAction(pi, icon, "action2");
@@ -124,7 +122,7 @@
PendingIntent pi = getIntent("");
Bitmap b = Bitmap.createBitmap(50, 25, Bitmap.Config.ARGB_8888);
new Canvas(b).drawColor(0xffff0000);
- IconCompat icon = IconCompat.createFromIcon(Icon.createWithBitmap(b));
+ IconCompat icon = IconCompat.createWithBitmap(b);
SliceAction primaryAction = new SliceAction(pi, icon, "action");
@@ -147,7 +145,7 @@
PendingIntent pi = getIntent("");
Bitmap b = Bitmap.createBitmap(50, 25, Bitmap.Config.ARGB_8888);
new Canvas(b).drawColor(0xffff0000);
- IconCompat icon = IconCompat.createFromIcon(Icon.createWithBitmap(b));
+ IconCompat icon = IconCompat.createWithBitmap(b);
SliceAction primaryAction = new SliceAction(pi, icon, "action");
SliceAction endAction = new SliceAction(pi, "toogle action", false);
@@ -170,7 +168,7 @@
PendingIntent pi = getIntent("");
Bitmap b = Bitmap.createBitmap(50, 25, Bitmap.Config.ARGB_8888);
new Canvas(b).drawColor(0xffff0000);
- IconCompat icon = IconCompat.createFromIcon(Icon.createWithBitmap(b));
+ IconCompat icon = IconCompat.createWithBitmap(b);
SliceAction primaryAction = new SliceAction(pi, icon, "action");
SliceAction sliceAction = new SliceAction(pi, "another action", true);
@@ -193,7 +191,7 @@
PendingIntent pi = getIntent("");
Bitmap b = Bitmap.createBitmap(50, 25, Bitmap.Config.ARGB_8888);
new Canvas(b).drawColor(0xffff0000);
- IconCompat icon = IconCompat.createFromIcon(Icon.createWithBitmap(b));
+ IconCompat icon = IconCompat.createWithBitmap(b);
SliceAction endAction1 = new SliceAction(pi, icon, "action");
SliceAction endAction2 = new SliceAction(pi, "toogle action", false);
@@ -264,7 +262,7 @@
.setTitle("another title")
.setValue(5)
.setMax(10)
- .setAction(pi));
+ .setInputAction(pi));
Slice sliderSlice = lb.build();
SliceMetadata sliderInfo = SliceMetadata.from(mContext, sliderSlice);
@@ -306,22 +304,22 @@
Uri uri = Uri.parse("content://pkg/slice");
Bitmap b = Bitmap.createBitmap(50, 25, Bitmap.Config.ARGB_8888);
new Canvas(b).drawColor(0xffff0000);
- Icon icon = Icon.createWithBitmap(b);
+ IconCompat icon = IconCompat.createWithBitmap(b);
ListBuilder lb = new ListBuilder(mContext, uri, ListBuilder.INFINITY);
GridRowBuilder grb = new GridRowBuilder(lb);
grb.addCell(new GridRowBuilder.CellBuilder(grb)
.addText("some text")
.addText("more text")
- .addImage(IconCompat.createFromIcon(icon), ICON_IMAGE));
+ .addImage(icon, ICON_IMAGE));
grb.addCell(new GridRowBuilder.CellBuilder(grb)
.addText("some text")
.addText("more text")
- .addImage(IconCompat.createFromIcon(icon), ICON_IMAGE));
+ .addImage(icon, ICON_IMAGE));
grb.addCell(new GridRowBuilder.CellBuilder(grb)
.addText("some text")
.addText("more text")
- .addImage(IconCompat.createFromIcon(icon), ICON_IMAGE));
+ .addImage(icon, ICON_IMAGE));
lb.addGridRow(grb);
Slice gridSlice = lb.build();
@@ -408,7 +406,7 @@
PendingIntent pi = getIntent("");
Bitmap b = Bitmap.createBitmap(50, 25, Bitmap.Config.ARGB_8888);
new Canvas(b).drawColor(0xffff0000);
- IconCompat icon = IconCompat.createFromIcon(Icon.createWithBitmap(b));
+ IconCompat icon = IconCompat.createWithBitmap(b);
SliceAction toggleAction = new SliceAction(pi, icon, "toggle", false /* isChecked */);
SliceAction toggleAction2 = new SliceAction(pi, icon, "toggle2", true /* isChecked */);
@@ -469,6 +467,31 @@
}
@Test
+ public void testGetInputRangeAction() {
+ Uri uri = Uri.parse("content://pkg/slice");
+ PendingIntent expectedIntent = getIntent("rangeintent");
+
+ Bitmap b = Bitmap.createBitmap(50, 25, Bitmap.Config.ARGB_8888);
+ new Canvas(b).drawColor(0xffff0000);
+ IconCompat icon = IconCompat.createWithBitmap(b);
+ SliceAction primaryAction = new SliceAction(getIntent(""), icon, "action");
+
+ ListBuilder lb = new ListBuilder(mContext, uri, ListBuilder.INFINITY);
+ lb.addInputRange(new ListBuilder.InputRangeBuilder(lb)
+ .setTitle("another title")
+ .setValue(7)
+ .setMin(5)
+ .setMax(10)
+ .setPrimaryAction(primaryAction)
+ .setInputAction(expectedIntent));
+ Slice sliderSlice = lb.build();
+
+ SliceMetadata sliderInfo = SliceMetadata.from(mContext, sliderSlice);
+ assertEquals(expectedIntent, sliderInfo.getInputRangeAction());
+ assertEquivalent(primaryAction, sliderInfo.getPrimaryAction());
+ }
+
+ @Test
public void testGetRangeForProgress() {
Uri uri = Uri.parse("content://pkg/slice");
@@ -528,17 +551,23 @@
@Test
public void testGetLoadingState() {
Uri uri = Uri.parse("content://pkg/slice");
- Slice s1 = new Slice.Builder(uri).build();
+ Slice s1 = new ListBuilder(mContext, uri, INFINITY).build();
SliceMetadata SliceMetadata1 = SliceMetadata.from(mContext, s1);
int actualState1 = SliceMetadata1.getLoadingState();
assertEquals(LOADED_NONE, actualState1);
- Slice s2 = new Slice.Builder(uri).addText(null, null, HINT_PARTIAL).build();
+ ListBuilder lb = new ListBuilder(mContext, uri, INFINITY);
+ Slice s2 = lb.addRow(new ListBuilder.RowBuilder(lb)
+ .setTitle(null, true /* isLoading */))
+ .build();
SliceMetadata SliceMetadata2 = SliceMetadata.from(mContext, s2);
int actualState2 = SliceMetadata2.getLoadingState();
assertEquals(LOADED_PARTIAL, actualState2);
- Slice s3 = new Slice.Builder(uri).addText("Text", null).build();
+ ListBuilder lb2 = new ListBuilder(mContext, uri, INFINITY);
+ Slice s3 = lb2.addRow(new ListBuilder.RowBuilder(lb2)
+ .setTitle("Title", false /* isLoading */))
+ .build();
SliceMetadata SliceMetadata3 = SliceMetadata.from(mContext, s3);
int actualState3 = SliceMetadata3.getLoadingState();
assertEquals(LOADED_ALL, actualState3);
@@ -551,9 +580,9 @@
long ttl = TimeUnit.DAYS.toMillis(1);
Slice ttlSlice = new Slice.Builder(uri)
.addText("Some text", null)
- .addTimestamp(timestamp, null)
- .addTimestamp(timestamp, null, SliceHints.HINT_LAST_UPDATED)
- .addTimestamp(ttl, null, SliceHints.HINT_TTL)
+ .addLong(timestamp, null)
+ .addLong(timestamp, null, SliceHints.HINT_LAST_UPDATED)
+ .addLong(ttl, null, SliceHints.HINT_TTL)
.build();
SliceMetadata si1 = SliceMetadata.from(mContext, ttlSlice);
@@ -562,7 +591,7 @@
Slice noTtlSlice = new Slice.Builder(uri)
.addText("Some text", null)
- .addTimestamp(timestamp, null).build();
+ .addLong(timestamp, null).build();
SliceMetadata si2 = SliceMetadata.from(mContext, noTtlSlice);
long retrievedTtl2 = si2.getExpiry();
assertEquals(0, retrievedTtl2);
@@ -575,9 +604,9 @@
long ttl = TimeUnit.DAYS.toMillis(1);
Slice ttlSlice = new Slice.Builder(uri)
.addText("Some text", null)
- .addTimestamp(timestamp - 20, null)
- .addTimestamp(timestamp, null, SliceHints.HINT_LAST_UPDATED)
- .addTimestamp(ttl, null, SliceHints.HINT_TTL)
+ .addLong(timestamp - 20, null)
+ .addLong(timestamp, null, SliceHints.HINT_LAST_UPDATED)
+ .addLong(ttl, null, SliceHints.HINT_TTL)
.build();
SliceMetadata si1 = SliceMetadata.from(mContext, ttlSlice);
@@ -586,7 +615,7 @@
Slice noTtlSlice = new Slice.Builder(uri)
.addText("Some text", null)
- .addTimestamp(timestamp, null).build();
+ .addLong(timestamp, null).build();
SliceMetadata si2 = SliceMetadata.from(mContext, noTtlSlice);
long retrievedLastUpdated2 = si2.getLastUpdatedTime();
@@ -597,7 +626,7 @@
public void testIsPermissionSlice() {
Uri uri = Uri.parse("content://pkg/slice");
Slice permissionSlice =
- SliceProviderCompat.createPermissionSlice(mContext, uri, mContext.getPackageName());
+ SliceProvider.createPermissionSlice(mContext, uri, mContext.getPackageName());
SliceMetadata metadata = SliceMetadata.from(mContext, permissionSlice);
assertEquals(true, metadata.isPermissionSlice());
diff --git a/slices/view/src/androidTest/java/androidx/slice/SlicePermissionTest.java b/slices/view/src/androidTest/java/androidx/slice/SlicePermissionTest.java
new file mode 100644
index 0000000..ffb65e0
--- /dev/null
+++ b/slices/view/src/androidTest/java/androidx/slice/SlicePermissionTest.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.slice;
+
+import static androidx.core.content.PermissionChecker.PERMISSION_DENIED;
+import static androidx.core.content.PermissionChecker.PERMISSION_GRANTED;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.Uri;
+import android.os.Process;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SlicePermissionTest {
+
+ private static final Uri BASE_URI = Uri.parse("content://androidx.slice.view.test/");
+ private final Context mContext = InstrumentationRegistry.getContext();
+ private String mTestPkg;
+ private int mTestUid;
+ private int mTestPid;
+ private SliceManager mSliceManager;
+
+ @Before
+ public void setup() throws NameNotFoundException {
+ mSliceManager = SliceManager.getInstance(mContext);
+ mTestPkg = mContext.getPackageName();
+ mTestUid = mContext.getPackageManager().getPackageUid(mTestPkg, 0);
+ mTestPid = Process.myPid();
+ }
+
+ @After
+ public void tearDown() {
+ mSliceManager.revokeSlicePermission(mTestPkg, BASE_URI);
+ }
+
+ @Test
+ public void testGrant() {
+ assertEquals(PERMISSION_DENIED,
+ mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+
+ mSliceManager.grantSlicePermission(mTestPkg, BASE_URI);
+
+ assertEquals(PERMISSION_GRANTED,
+ mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+ }
+
+ @Test
+ public void testGrantParent() {
+ Uri uri = BASE_URI.buildUpon()
+ .appendPath("something")
+ .build();
+
+ assertEquals(PERMISSION_DENIED,
+ mSliceManager.checkSlicePermission(uri, mTestPid, mTestUid));
+
+ mSliceManager.grantSlicePermission(mTestPkg, BASE_URI);
+
+ assertEquals(PERMISSION_GRANTED,
+ mSliceManager.checkSlicePermission(uri, mTestPid, mTestUid));
+ }
+
+ @Test
+ public void testGrantParentExpands() {
+ Uri uri = BASE_URI.buildUpon()
+ .appendPath("something")
+ .build();
+
+ assertEquals(PERMISSION_DENIED,
+ mSliceManager.checkSlicePermission(uri, mTestPid, mTestUid));
+
+ mSliceManager.grantSlicePermission(mTestPkg, uri);
+
+ // Only sub-path granted.
+ assertEquals(PERMISSION_GRANTED,
+ mSliceManager.checkSlicePermission(uri, mTestPid, mTestUid));
+ assertEquals(PERMISSION_DENIED,
+ mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+
+ mSliceManager.grantSlicePermission(mTestPkg, BASE_URI);
+
+ // Now all granted.
+ assertEquals(PERMISSION_GRANTED,
+ mSliceManager.checkSlicePermission(uri, mTestPid, mTestUid));
+ assertEquals(PERMISSION_GRANTED,
+ mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+ }
+
+ @Test
+ public void testGrantChild() {
+ Uri uri = BASE_URI.buildUpon()
+ .appendPath("something")
+ .build();
+
+ assertEquals(PERMISSION_DENIED,
+ mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+
+ mSliceManager.grantSlicePermission(mTestPkg, uri);
+
+ // Still no permission because only a child was granted
+ assertEquals(PERMISSION_DENIED,
+ mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+ }
+
+ @Test
+ public void testRevoke() {
+ assertEquals(PERMISSION_DENIED,
+ mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+
+ mSliceManager.grantSlicePermission(mTestPkg, BASE_URI);
+
+ assertEquals(PERMISSION_GRANTED,
+ mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+
+ mSliceManager.revokeSlicePermission(mTestPkg, BASE_URI);
+
+ assertEquals(PERMISSION_DENIED,
+ mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+ }
+
+ @Test
+ public void testRevokeParent() {
+ Uri uri = BASE_URI.buildUpon()
+ .appendPath("something")
+ .build();
+ assertEquals(PERMISSION_DENIED,
+ mSliceManager.checkSlicePermission(uri, mTestPid, mTestUid));
+
+ mSliceManager.grantSlicePermission(mTestPkg, uri);
+
+ assertEquals(PERMISSION_GRANTED,
+ mSliceManager.checkSlicePermission(uri, mTestPid, mTestUid));
+
+ mSliceManager.revokeSlicePermission(mTestPkg, BASE_URI);
+
+ // Revoked because parent was revoked
+ assertEquals(PERMISSION_DENIED,
+ mSliceManager.checkSlicePermission(uri, mTestPid, mTestUid));
+ }
+
+ @Test
+ public void testRevokeChild() {
+ Uri uri = BASE_URI.buildUpon()
+ .appendPath("something")
+ .build();
+ assertEquals(PERMISSION_DENIED,
+ mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+
+ mSliceManager.grantSlicePermission(mTestPkg, BASE_URI);
+
+ assertEquals(PERMISSION_GRANTED,
+ mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+
+ mSliceManager.revokeSlicePermission(mTestPkg, uri);
+
+ // Not revoked because child was revoked.
+ assertEquals(PERMISSION_GRANTED,
+ mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+ }
+
+}
diff --git a/slices/view/src/androidTest/java/androidx/slice/SliceXmlTest.java b/slices/view/src/androidTest/java/androidx/slice/SliceXmlTest.java
index 298d498..d1a647c 100644
--- a/slices/view/src/androidTest/java/androidx/slice/SliceXmlTest.java
+++ b/slices/view/src/androidTest/java/androidx/slice/SliceXmlTest.java
@@ -24,6 +24,11 @@
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;
+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.graphics.Bitmap;
import android.graphics.Canvas;
@@ -33,6 +38,8 @@
import android.support.test.runner.AndroidJUnit4;
import androidx.core.graphics.drawable.IconCompat;
+import androidx.slice.core.SliceQuery;
+import androidx.slice.view.R;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -51,7 +58,7 @@
public void testThrowForAction() throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Slice s = new Slice.Builder(Uri.parse("content://pkg/slice"))
- .addAction(null, null, null)
+ .addAction((PendingIntent) null, null, null)
.build();
SliceUtils.serializeSlice(s, mContext, outputStream, "UTF-8", new SliceUtils
.SerializeOptions());
@@ -81,7 +88,7 @@
public void testNoThrowForAction() throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Slice s = new Slice.Builder(Uri.parse("content://pkg/slice"))
- .addAction(null, null, null)
+ .addAction((PendingIntent) null, null, null)
.build();
SliceUtils.serializeSlice(s, mContext, outputStream, "UTF-8", new SliceUtils
.SerializeOptions().setActionMode(SliceUtils.SerializeOptions.MODE_REMOVE));
@@ -108,7 +115,7 @@
}
@Test
- public void testSerialization() throws IOException {
+ public void testSerialization() throws Exception {
Bitmap b = Bitmap.createBitmap(50, 25, Bitmap.Config.ARGB_8888);
new Canvas(b).drawColor(0xffff0000);
// Create a slice containing all the types in a hierarchy.
@@ -118,24 +125,32 @@
.build())
.addIcon(IconCompat.createWithBitmap(b), null)
.addText("Some text", null)
- .addAction(null, new Slice.Builder(Uri.parse("content://pkg/slice/sub"))
+ .addAction((PendingIntent) null,
+ new Slice.Builder(Uri.parse("content://pkg/slice/action"))
.addText("Action text", null)
.build(), null)
.addInt(0xff00ff00, "subtype")
+ .addIcon(IconCompat.createWithResource(mContext, R.drawable.abc_slice_see_more_bg),
+ null)
.addHints("Hint 1", "Hint 2")
.build();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
SliceUtils.serializeSlice(before, mContext, outputStream, "UTF-8",
new SliceUtils.SerializeOptions()
- .setImageMode(SliceUtils.SerializeOptions.MODE_DISABLE)
- .setActionMode(SliceUtils.SerializeOptions.MODE_DISABLE));
+ .setImageMode(SliceUtils.SerializeOptions.MODE_CONVERT)
+ .setActionMode(SliceUtils.SerializeOptions.MODE_CONVERT));
byte[] bytes = outputStream.toByteArray();
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
- Slice after = SliceUtils.parseSlice(inputStream, "UTF-8");
+ SliceUtils.SliceActionListener listener = mock(SliceUtils.SliceActionListener.class);
+ Slice after = SliceUtils.parseSlice(mContext, inputStream, "UTF-8", listener);
assertEquivalent(before, after);
+
+ SliceItem action = SliceQuery.find(after, FORMAT_ACTION);
+ action.fireAction(null, null);
+ verify(listener).onSliceAction(eq(Uri.parse("content://pkg/slice/action")));
}
private void assertEquivalent(Slice desired, Slice actual) {
diff --git a/slices/view/src/androidTest/java/androidx/slice/render/RenderTest.java b/slices/view/src/androidTest/java/androidx/slice/render/RenderTest.java
index deb1f90..4c3b6fc 100644
--- a/slices/view/src/androidTest/java/androidx/slice/render/RenderTest.java
+++ b/slices/view/src/androidTest/java/androidx/slice/render/RenderTest.java
@@ -16,12 +16,15 @@
package androidx.slice.render;
+import static android.os.Build.VERSION.SDK_INT;
+
import static androidx.slice.render.SliceRenderer.SCREENSHOT_DIR;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.os.Build;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
@@ -29,6 +32,11 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -39,7 +47,7 @@
private final Context mContext = InstrumentationRegistry.getContext();
@Test
- public void testRender() throws InterruptedException {
+ public void testRender() throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
@@ -53,8 +61,53 @@
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
latch.await(30000, TimeUnit.MILLISECONDS);
- String path = mContext.getDataDir().toString() + "/" + SCREENSHOT_DIR;
- InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
- "mv " + path + " " + "/sdcard/");
+ String path = new File(mContext.getFilesDir(), SCREENSHOT_DIR).getAbsolutePath();
+ if (SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+ "mv " + path + " " + "/sdcard/");
+ } else {
+ deleteDir(new File("/sdcard/" + SCREENSHOT_DIR));
+ copyDirectory(new File(path), new File("/sdcard/" + SCREENSHOT_DIR));
+ }
+ }
+
+
+ public static void copyDirectory(File sourceLocation, File targetLocation) throws Exception {
+ if (sourceLocation.isDirectory()) {
+ if (!targetLocation.exists()) {
+ targetLocation.mkdirs();
+ }
+
+ String[] children = sourceLocation.list();
+ for (int i = 0; i < children.length; i++) {
+ copyDirectory(new File(sourceLocation, children[i]), new File(
+ targetLocation, children[i]));
+ }
+ } else {
+ copyFile(sourceLocation, targetLocation);
+ }
+ }
+
+ public static void copyFile(File sourceLocation, File targetLocation) throws Exception {
+ InputStream in = new FileInputStream(sourceLocation);
+ OutputStream out = new FileOutputStream(targetLocation);
+
+ byte[] buf = new byte[1024];
+ int len;
+ while ((len = in.read(buf)) > 0) {
+ out.write(buf, 0, len);
+ }
+ in.close();
+ out.close();
+ }
+
+ static void deleteDir(File file) {
+ File[] contents = file.listFiles();
+ if (contents != null) {
+ for (File f : contents) {
+ deleteDir(f);
+ }
+ }
+ file.delete();
}
}
diff --git a/slices/view/src/androidTest/java/androidx/slice/render/SliceCreator.java b/slices/view/src/androidTest/java/androidx/slice/render/SliceCreator.java
index f181617..5f08f3f 100644
--- a/slices/view/src/androidTest/java/androidx/slice/render/SliceCreator.java
+++ b/slices/view/src/androidTest/java/androidx/slice/render/SliceCreator.java
@@ -36,11 +36,11 @@
import androidx.core.graphics.drawable.IconCompat;
import androidx.slice.Slice;
+import androidx.slice.SliceProvider;
import androidx.slice.builders.GridRowBuilder;
import androidx.slice.builders.ListBuilder;
import androidx.slice.builders.MessagingSliceBuilder;
import androidx.slice.builders.SliceAction;
-import androidx.slice.compat.SliceProviderCompat;
import androidx.slice.view.test.R;
import java.util.Arrays;
@@ -57,9 +57,27 @@
"com.example.androidx.slice.action.TOAST";
public static final String EXTRA_TOAST_MESSAGE = "com.example.androidx.extra.TOAST_MESSAGE";
- public static final String[] URI_PATHS = {"message", "wifi", "wifi2", "note", "ride",
- "ride-ttl", "toggle", "toggle2", "contact", "gallery", "subscription", "subscription2",
- "weather", "reservation", "inputrange", "range", "permission"};
+ public static final String[] URI_PATHS = {
+ "message",
+ "wifi",
+ "wifi2",
+ "note",
+ "ride",
+ "ride-ttl",
+ "toggle",
+ "toggle2",
+ "contact",
+ "gallery",
+ "subscription",
+ "subscription2",
+ "weather",
+ "reservation",
+ "inputrange",
+ "inputrange2",
+ "range",
+ "permission",
+ "empty",
+ };
private final Context mContext;
@@ -115,10 +133,14 @@
return createReservationSlice(sliceUri);
case "/inputrange":
return createStarRatingInputRange(sliceUri);
+ case "/inputrange2":
+ return createBasicInputRange(sliceUri);
case "/range":
return createDownloadProgressRange(sliceUri);
case "/permission":
return createPermissionSlice(sliceUri);
+ case "/empty":
+ return new ListBuilder(getContext(), sliceUri, INFINITY).build();
}
throw new IllegalArgumentException("Unknown uri " + sliceUri);
}
@@ -236,7 +258,7 @@
"See contact info"), IconCompat.createWithResource(getContext(),
R.drawable.mady), SMALL_IMAGE, "Mady");
GridRowBuilder gb = new GridRowBuilder(b);
- return b.setColor(0xff3949ab)
+ return b.setAccentColor(0xff3949ab)
.addRow(rb
.setTitle("Mady Pitza")
.setSubtitle("Frequently contacted contact")
@@ -292,7 +314,7 @@
private Slice createNoteSlice(Uri sliceUri) {
// TODO: Remote input.
ListBuilder lb = new ListBuilder(getContext(), sliceUri, INFINITY);
- return lb.setColor(0xfff4b400)
+ return lb.setAccentColor(0xfff4b400)
.addRow(new ListBuilder.RowBuilder(lb)
.setTitle("Create new note")
.setSubtitle("with this note taking app")
@@ -322,11 +344,11 @@
? -TimeUnit.MINUTES.toMillis(2) // negative for testing
: INFINITY;
ListBuilder lb = new ListBuilder(getContext(), sliceUri, ttl);
- return lb.setColor(0xff0F9D58)
+ return lb.setAccentColor(0xff0F9D58)
.setHeader(new ListBuilder.HeaderBuilder(lb)
.setTitle("Get ride")
.setSubtitle(headerSubtitle)
- .setSummarySubtitle("Ride to work in 12 min | Ride home in 1 hour 45 min")
+ .setSummary("Ride to work in 12 min | Ride home in 1 hour 45 min")
.setPrimaryAction(primaryAction))
.addRow(new ListBuilder.RowBuilder(lb)
.setTitle("Work")
@@ -345,7 +367,7 @@
private Slice createCustomToggleSlice(Uri sliceUri) {
ListBuilder b = new ListBuilder(getContext(), sliceUri, -TimeUnit.HOURS.toMillis(1));
- return b.setColor(0xffff4081)
+ return b.setAccentColor(0xffff4081)
.addRow(new ListBuilder.RowBuilder(b)
.setTitle("Custom toggle")
.addEndItem(
@@ -358,7 +380,7 @@
private Slice createTwoCustomToggleSlices(Uri sliceUri) {
ListBuilder lb = new ListBuilder(getContext(), sliceUri, INFINITY);
- return lb.setColor(0xffff4081)
+ return lb.setAccentColor(0xffff4081)
.addRow(new ListBuilder.RowBuilder(lb)
.setTitle("2 toggles")
.setSubtitle("each supports two states")
@@ -398,7 +420,7 @@
ListBuilder lb = new ListBuilder(getContext(), sliceUri, INFINITY);
SliceAction primaryAction = new SliceAction(getIntent(Settings.ACTION_WIFI_SETTINGS),
IconCompat.createWithResource(getContext(), R.drawable.ic_wifi), "Wi-fi Settings");
- lb.setColor(0xff4285f4);
+ lb.setAccentColor(0xff4285f4);
lb.addRow(new ListBuilder.RowBuilder(lb)
.setTitle("Wi-fi")
.setTitleItem(IconCompat.createWithResource(getContext(), R.drawable.ic_wifi),
@@ -459,7 +481,7 @@
.addCell(new GridRowBuilder.CellBuilder(gb2)
.addTitleText("Check Out")
.addText("11:00 AM, Feb 19"));
- return lb.setColor(0xffFF5252)
+ return lb.setAccentColor(0xffFF5252)
.setHeader(new ListBuilder.HeaderBuilder(lb)
.setTitle("Upcoming trip to Seattle")
.setSubtitle("Feb 1 - 19 | 2 guests"))
@@ -475,13 +497,30 @@
.build();
}
+ private Slice createBasicInputRange(Uri sliceUri) {
+ IconCompat icon = IconCompat.createWithResource(getContext(), R.drawable.ic_star_on);
+ SliceAction primaryAction =
+ new SliceAction(getBroadcastIntent(ACTION_TOAST, "open star rating"),
+ icon, "Rate");
+ ListBuilder lb = new ListBuilder(getContext(), sliceUri, INFINITY);
+ return lb.setAccentColor(0xff4285f4)
+ .addInputRange(new ListBuilder.InputRangeBuilder(lb)
+ .setTitle("Alarm volume")
+ .setSubtitle("Adjust your volume")
+ .setInputAction(getBroadcastIntent(ACTION_TOAST, "volume changed"))
+ .setValue(80)
+ .setPrimaryAction(primaryAction)
+ .setContentDescription("Slider for alarm volume"))
+ .build();
+ }
+
private Slice createStarRatingInputRange(Uri sliceUri) {
IconCompat icon = IconCompat.createWithResource(getContext(), R.drawable.ic_star_on);
SliceAction primaryAction =
new SliceAction(getBroadcastIntent(ACTION_TOAST, "open star rating"), icon,
"Rate");
ListBuilder lb = new ListBuilder(getContext(), sliceUri, INFINITY);
- return lb.setColor(0xffff4081)
+ return lb.setAccentColor(0xffff4081)
.addInputRange(new ListBuilder.InputRangeBuilder(lb)
.setTitle("Star rating")
.setSubtitle("Pick a rating from 0 to 5")
@@ -502,7 +541,7 @@
getBroadcastIntent(ACTION_TOAST, "open download"), icon,
"Download");
ListBuilder lb = new ListBuilder(getContext(), sliceUri, INFINITY);
- return lb.setColor(0xffff4081)
+ return lb.setAccentColor(0xffff4081)
.addRange(new ListBuilder.RangeBuilder(lb)
.setTitle("Download progress")
.setSubtitle("Download is happening")
@@ -513,7 +552,7 @@
}
private Slice createPermissionSlice(Uri uri) {
- return SliceProviderCompat.createPermissionSlice(getContext(), uri,
+ return SliceProvider.createPermissionSlice(getContext(), uri,
getContext().getPackageName());
}
diff --git a/slices/view/src/androidTest/java/androidx/slice/render/SliceRenderer.java b/slices/view/src/androidTest/java/androidx/slice/render/SliceRenderer.java
index 5625c34..ec5e55a 100644
--- a/slices/view/src/androidTest/java/androidx/slice/render/SliceRenderer.java
+++ b/slices/view/src/androidTest/java/androidx/slice/render/SliceRenderer.java
@@ -22,7 +22,7 @@
import android.app.ProgressDialog;
import android.graphics.Bitmap;
import android.graphics.Canvas;
-import android.os.AsyncTask;
+import android.net.Uri;
import android.os.Handler;
import android.util.Log;
import android.util.TypedValue;
@@ -30,24 +30,33 @@
import android.view.View;
import android.view.ViewGroup;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.util.concurrent.CountDownLatch;
-
import androidx.recyclerview.widget.RecyclerView;
import androidx.slice.Slice;
import androidx.slice.SliceProvider;
+import androidx.slice.SliceUtils;
import androidx.slice.view.test.R;
import androidx.slice.widget.SliceLiveData;
import androidx.slice.widget.SliceView;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
public class SliceRenderer {
private static final String TAG = "SliceRenderer";
public static final String SCREENSHOT_DIR = "slice-screenshots";
+
+ private static final int MAX_CONCURRENT = 5;
+
private static File sScreenshotDirectory;
+ private final Object mRenderLock = new Object();
+
private final Activity mContext;
private final View mLayout;
private final SliceView mSV1;
@@ -107,7 +116,7 @@
private File getScreenshotDirectory() {
if (sScreenshotDirectory == null) {
- File storage = mContext.getDataDir();
+ File storage = mContext.getFilesDir();
sScreenshotDirectory = new File(storage, SCREENSHOT_DIR);
if (!sScreenshotDirectory.exists()) {
if (!sScreenshotDirectory.mkdirs()) {
@@ -121,20 +130,46 @@
private void doRender() {
- File output = getScreenshotDirectory();
+ final File output = getScreenshotDirectory();
if (!output.exists()) {
output.mkdir();
}
- mDoneLatch = new CountDownLatch(SliceCreator.URI_PATHS.length);
- for (String slice : SliceCreator.URI_PATHS) {
- doRender(slice, new File(output, String.format("%s.png", slice)),
- true /* scrollable */);
+ mDoneLatch = new CountDownLatch(SliceCreator.URI_PATHS.length * 2 + 2);
+
+ ExecutorService executor = Executors.newFixedThreadPool(5);
+ for (final String slice : SliceCreator.URI_PATHS) {
+ final Slice s = mSliceCreator.onBindSlice(SliceCreator.getUri(slice, mContext));
+
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ doRender(slice, s, new File(output, String.format("%s.png", slice)),
+ true /* scrollable */);
+ }
+ });
+ final Slice serialized = serAndUnSer(s);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ doRender(slice + "-ser", serialized, new File(output, String.format(
+ "%s-serialized.png", slice)), true /* scrollable */);
+ }
+ });
if (slice.equals("wifi") || slice.equals("wifi2")) {
// Test scrolling
- doRender(slice, new File(output, String.format("%s-no-scroll.png", slice)),
- false /* scrollable */);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ doRender(slice + "-ns", s, new File(output, String.format(
+ "%s-no-scroll.png", slice)), false /* scrollable */);
+ }
+ });
}
}
+ try {
+ mDoneLatch.await();
+ } catch (InterruptedException e) {
+ }
Log.d(TAG, "Wrote render to " + output.getAbsolutePath());
mContext.runOnUiThread(new Runnable() {
@Override
@@ -142,71 +177,83 @@
((ViewGroup) mParent.getParent()).removeView(mParent);
}
});
+ }
+
+ private Slice serAndUnSer(Slice s) {
try {
- mDoneLatch.await();
- } catch (InterruptedException e) {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ SliceUtils.serializeSlice(s, mContext, outputStream, "UTF-8",
+ new SliceUtils.SerializeOptions()
+ .setImageMode(SliceUtils.SerializeOptions.MODE_CONVERT)
+ .setActionMode(SliceUtils.SerializeOptions.MODE_CONVERT));
+
+ byte[] bytes = outputStream.toByteArray();
+ Log.d(TAG, "Serialized: " + new String(bytes));
+ ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
+ return SliceUtils.parseSlice(mContext, inputStream, "UTF-8",
+ new SliceUtils.SliceActionListener() {
+ @Override
+ public void onSliceAction(Uri actionUri) { }
+ });
+ } catch (Exception e) {
+ throw new RuntimeException(e);
}
}
- private void doRender(final String slice, final File file, final boolean scrollable) {
+ private void doRender(final String slice, final Slice s, final File file,
+ final boolean scrollable) {
Log.d(TAG, "Rendering " + slice + " to " + file.getAbsolutePath());
- final Slice s = mSliceCreator.onBindSlice(SliceCreator.getUri(slice, mContext));
-
- final CountDownLatch l = new CountDownLatch(1);
- mContext.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mSV1.setSlice(s);
- mSV2.setSlice(s);
- mSV3.setSlice(s);
- mSV3.setScrollable(scrollable);
- mSV1.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
+ try {
+ final CountDownLatch l = new CountDownLatch(1);
+ final Bitmap[] b = new Bitmap[1];
+ synchronized (mRenderLock) {
+ mContext.runOnUiThread(new Runnable() {
@Override
- public void onLayoutChange(View v, int left, int top, int right, int bottom,
- int oldLeft, int oldTop, int oldRight, int oldBottom) {
- mSV1.removeOnLayoutChangeListener(this);
- mSV1.postDelayed(new Runnable() {
+ public void run() {
+ mSV1.setSlice(s);
+ mSV2.setSlice(s);
+ mSV3.setSlice(s);
+ mSV3.setScrollable(scrollable);
+ mSV1.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@Override
- public void run() {
- Log.d(TAG, "Drawing " + slice);
- Bitmap b = Bitmap.createBitmap(mLayout.getMeasuredWidth(),
- mLayout.getMeasuredHeight(),
- Bitmap.Config.ARGB_8888);
+ public void onLayoutChange(View v, int left, int top, int right,
+ int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ mSV1.removeOnLayoutChangeListener(this);
+ mSV1.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ Log.d(TAG, "Drawing " + slice);
+ b[0] = Bitmap.createBitmap(mLayout.getMeasuredWidth(),
+ mLayout.getMeasuredHeight(),
+ Bitmap.Config.ARGB_8888);
- mLayout.draw(new Canvas(b));
- try {
- doCompress(slice, b, new FileOutputStream(file));
- } catch (FileNotFoundException e) {
- throw new RuntimeException(e);
- }
- l.countDown();
+ mLayout.draw(new Canvas(b[0]));
+ l.countDown();
+ }
+ }, 10);
}
- }, 10);
+ });
}
});
+ l.await();
}
- });
- try {
- l.await();
- } catch (InterruptedException e) {
+ doCompress(slice, b[0], new FileOutputStream(file));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
}
}
private void doCompress(final String slice, final Bitmap b, final FileOutputStream s) {
- AsyncTask.execute(new Runnable() {
- @Override
- public void run() {
- Log.d(TAG, "Compressing " + slice);
- if (!b.compress(Bitmap.CompressFormat.PNG, 100, s)) {
- throw new RuntimeException("Unable to compress");
- }
+ Log.d(TAG, "Compressing " + slice);
+ if (!b.compress(Bitmap.CompressFormat.PNG, 100, s)) {
+ throw new RuntimeException("Unable to compress");
+ }
- b.recycle();
- Log.d(TAG, "Done " + slice);
- mDoneLatch.countDown();
- }
- });
+ b.recycle();
+ mDoneLatch.countDown();
+ Log.d(TAG, "Done " + slice);
}
public void renderAll(final Runnable runnable) {
diff --git a/slices/view/src/main/java/androidx/slice/SliceManager.java b/slices/view/src/main/java/androidx/slice/SliceManager.java
index 63c56e8..83febcc 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManager.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManager.java
@@ -28,6 +28,7 @@
import androidx.core.os.BuildCompat;
import java.util.Collection;
+import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
@@ -125,10 +126,8 @@
* <p>
* This is the set of specs supported for a specific pinned slice. It will take
* into account all clients and returns only specs supported by all.
- * @hide
* @see SliceSpec
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public abstract @NonNull Set<SliceSpec> getPinnedSpecs(@NonNull Uri uri);
/**
@@ -141,9 +140,8 @@
public abstract @Nullable Slice bindSlice(@NonNull Uri uri);
/**
- * Turns a slice intent into slice content. Expects an explicit intent. If there is no
- * {@link android.content.ContentProvider} associated with the given intent this will throw
- * {@link IllegalArgumentException}.
+ * Turns a slice intent into slice content. Is a shortcut to perform the action
+ * of both {@link #mapIntentToUri(Intent)} and {@link #bindSlice(Uri)} at once.
*
* @param intent The intent associated with a slice.
* @return The Slice provided by the app or null if none is given.
@@ -154,12 +152,23 @@
public abstract @Nullable Slice bindSlice(@NonNull Intent intent);
/**
- * Turns a slice intent into a slice uri. Expects an explicit intent. If there is no
- * {@link android.content.ContentProvider} associated with the given intent this will throw
- * {@link IllegalArgumentException}.
- *
+ * Turns a slice intent into a slice uri. Expects an explicit intent.
+ * <p>
+ * This goes through a several stage resolution process to determine if any slice
+ * can represent this intent.
+ * <ol>
+ * <li> If the intent contains data that {@link android.content.ContentResolver#getType} is
+ * {@link android.app.slice.SliceProvider#SLICE_TYPE} then the data will be returned.</li>
+ * <li>If the intent explicitly points at an activity, and that activity has
+ * meta-data for key {@link android.app.slice.SliceManager#SLICE_METADATA_KEY},
+ * then the Uri specified there will be returned.</li>
+ * <li>Lastly, if the intent with {@link android.app.slice.SliceManager#CATEGORY_SLICE} added
+ * resolves to a provider, then the provider will be asked to
+ * {@link SliceProvider#onMapIntentToUri} and that result will be returned.</li>
+ * <li>If no slice is found, then {@code null} is returned.</li>
+ * </ol>
* @param intent The intent associated with a slice.
- * @return The Slice Uri provided by the app or null if none is given.
+ * @return The Slice Uri provided by the app or null if none exists.
* @see Slice
* @see SliceProvider#onMapIntentToUri(Intent)
* @see Intent
@@ -223,6 +232,12 @@
public abstract @NonNull Collection<Uri> getSliceDescendants(@NonNull Uri uri);
/**
+ * Get the list of currently pinned slices for this app.
+ * @see SliceProvider#onSlicePinned
+ */
+ public abstract @NonNull List<Uri> getPinnedSlices();
+
+ /**
* Class that listens to changes in {@link Slice}s.
*/
public interface SliceCallback {
diff --git a/slices/view/src/main/java/androidx/slice/SliceManagerBase.java b/slices/view/src/main/java/androidx/slice/SliceManagerBase.java
index fb9fccd..5192dad 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManagerBase.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManagerBase.java
@@ -19,11 +19,9 @@
import static androidx.slice.widget.SliceLiveData.SUPPORTED_SPECS;
import android.content.Context;
-import android.content.Intent;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.AsyncTask;
-import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.ArrayMap;
@@ -31,7 +29,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
-import androidx.core.content.PermissionChecker;
import java.util.concurrent.Executor;
@@ -71,31 +68,6 @@
if (impl != null) impl.stopListening();
}
- @Override
- @PermissionChecker.PermissionResult
- public int checkSlicePermission(@NonNull Uri uri, int pid, int uid) {
- // TODO: Switch off Uri permissions.
- return mContext.checkUriPermission(uri, pid, uid,
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
- }
-
- @Override
- public void grantSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
- // TODO: Switch off Uri permissions.
- mContext.grantUriPermission(toPackage, uri,
- Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
- | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
- | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
- }
-
- @Override
- public void revokeSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
- // TODO: Switch off Uri permissions.
- if (Build.VERSION.SDK_INT >= 26) {
- mContext.revokeUriPermission(toPackage, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
- }
- }
-
private SliceListenerImpl getListener(Uri uri, SliceCallback callback,
SliceListenerImpl listener) {
diff --git a/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java b/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java
index 1badbb4..0018c13 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java
@@ -29,6 +29,7 @@
import androidx.slice.widget.SliceLiveData;
import java.util.Collection;
+import java.util.List;
import java.util.Set;
@@ -76,7 +77,30 @@
}
@Override
+ public int checkSlicePermission(Uri uri, int pid, int uid) {
+ return SliceProviderCompat.checkSlicePermission(mContext, mContext.getPackageName(), uri,
+ pid, uid);
+ }
+
+ @Override
+ public void grantSlicePermission(String toPackage, Uri uri) {
+ SliceProviderCompat.grantSlicePermission(mContext, mContext.getPackageName(), toPackage,
+ uri);
+ }
+
+ @Override
+ public void revokeSlicePermission(String toPackage, Uri uri) {
+ SliceProviderCompat.revokeSlicePermission(mContext, mContext.getPackageName(), toPackage,
+ uri);
+ }
+
+ @Override
public Collection<Uri> getSliceDescendants(Uri uri) {
return SliceProviderCompat.getSliceDescendants(mContext, uri);
}
+
+ @Override
+ public List<Uri> getPinnedSlices() {
+ return SliceProviderCompat.getPinnedSlices(mContext);
+ }
}
diff --git a/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java b/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java
index ac38e36..2dc7281 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java
@@ -19,7 +19,6 @@
import static androidx.slice.SliceConvert.unwrap;
import static androidx.slice.widget.SliceLiveData.SUPPORTED_SPECS;
-import android.app.slice.SliceManager;
import android.app.slice.SliceSpec;
import android.content.Context;
import android.content.Intent;
@@ -29,9 +28,11 @@
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import androidx.core.content.PermissionChecker;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import java.util.Set;
@@ -44,12 +45,6 @@
private final android.app.slice.SliceManager mManager;
private final List<SliceSpec> mSpecs;
- private final SliceManager.SliceCallback mCallback = new SliceManager.SliceCallback() {
- @Override
- public void onSliceUpdated(@NonNull android.app.slice.Slice s) {
-
- }
- };
SliceManagerWrapper(Context context) {
this(context, context.getSystemService(android.app.slice.SliceManager.class));
@@ -63,31 +58,31 @@
@Override
public void pinSlice(@NonNull Uri uri) {
- mManager.registerSliceCallback(uri, mSpecs, mCallback);
+ mManager.pinSlice(uri, mSpecs);
}
@Override
public void unpinSlice(@NonNull Uri uri) {
- mManager.unregisterSliceCallback(uri, mCallback);
+ mManager.unpinSlice(uri);
}
@Override
public @NonNull Set<androidx.slice.SliceSpec> getPinnedSpecs(@NonNull Uri uri) {
- return SliceConvert.wrap(mManager.getPinnedSpecs(uri));
+ // Disabled while we update APIs.
+ //return SliceConvert.wrap(mManager.getPinnedSpecs(uri));
+ return Collections.EMPTY_SET;
}
@Nullable
@Override
public androidx.slice.Slice bindSlice(@NonNull Uri uri) {
- return SliceConvert.wrap(android.app.slice.Slice.bindSlice(
- mContext.getContentResolver(), uri, mSpecs));
+ return SliceConvert.wrap(mManager.bindSlice(uri, mSpecs));
}
@Nullable
@Override
public androidx.slice.Slice bindSlice(@NonNull Intent intent) {
- return SliceConvert.wrap(android.app.slice.Slice.bindSlice(
- mContext, intent, mSpecs));
+ return SliceConvert.wrap(mManager.bindSlice(intent, mSpecs));
}
@Override
@@ -95,11 +90,30 @@
return mManager.getSliceDescendants(uri);
}
+ @Override
+ @PermissionChecker.PermissionResult
+ public int checkSlicePermission(@NonNull Uri uri, int pid, int uid) {
+ return mManager.checkSlicePermission(uri, pid, uid);
+ }
+
+ @Override
+ public void grantSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
+ mManager.grantSlicePermission(toPackage, uri);
+ }
+
+ @Override
+ public void revokeSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
+ mManager.revokeSlicePermission(toPackage, uri);
+ }
+
@Nullable
@Override
public Uri mapIntentToUri(@NonNull Intent intent) {
- // TODO: Switch over to mapIntentToUri once it lands in prebuilt.
- Slice slice = bindSlice(intent);
- return slice != null ? slice.getUri() : null;
+ return mManager.mapIntentToUri(intent);
+ }
+
+ @Override
+ public List<Uri> getPinnedSlices() {
+ return mManager.getPinnedSlices();
}
}
diff --git a/slices/view/src/main/java/androidx/slice/SliceMetadata.java b/slices/view/src/main/java/androidx/slice/SliceMetadata.java
index 8a4ee03..a2c996c 100644
--- a/slices/view/src/main/java/androidx/slice/SliceMetadata.java
+++ b/slices/view/src/main/java/androidx/slice/SliceMetadata.java
@@ -35,6 +35,7 @@
import static androidx.slice.widget.EventInfo.ROW_TYPE_PROGRESS;
import static androidx.slice.widget.EventInfo.ROW_TYPE_SLIDER;
+import android.app.PendingIntent;
import android.content.Context;
import android.text.TextUtils;
@@ -128,7 +129,7 @@
}
mSliceActions = getSliceActions(mSlice);
- mListContent = new ListContent(context, slice);
+ mListContent = new ListContent(context, slice, null, 0, 0);
mHeaderItem = mListContent.getHeaderItem();
mTemplateType = mListContent.getHeaderTemplateType();
@@ -197,6 +198,23 @@
}
/**
+ * Gets the input range action associated for this slice, if it exists.
+ *
+ * @return the {@link android.app.PendingIntent} for the input range.
+ */
+ @Nullable
+ public PendingIntent getInputRangeAction() {
+ if (mTemplateType == ROW_TYPE_SLIDER) {
+ RowContent rc = new RowContent(mContext, mHeaderItem, true /* isHeader */);
+ SliceItem range = rc.getRange();
+ if (range != null) {
+ return range.getAction();
+ }
+ }
+ return null;
+ }
+
+ /**
* Gets the range information associated with a progress bar or input range associated with this
* slice, if it exists.
*
@@ -270,7 +288,7 @@
public int getLoadingState() {
// Check loading state
boolean hasHintPartial = SliceQuery.find(mSlice, null, HINT_PARTIAL, null) != null;
- if (mSlice.getItems().size() == 0) {
+ if (!mListContent.isValid()) {
// Empty slice
return LOADED_NONE;
} else if (hasHintPartial) {
diff --git a/slices/view/src/main/java/androidx/slice/SliceUtils.java b/slices/view/src/main/java/androidx/slice/SliceUtils.java
index 93b149c..442b324 100644
--- a/slices/view/src/main/java/androidx/slice/SliceUtils.java
+++ b/slices/view/src/main/java/androidx/slice/SliceUtils.java
@@ -30,7 +30,10 @@
import static androidx.slice.SliceMetadata.LOADED_PARTIAL;
import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
import android.content.Context;
+import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.IntDef;
@@ -42,6 +45,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.lang.annotation.Retention;
import java.util.ArrayList;
import java.util.List;
@@ -65,10 +69,11 @@
* @param output The output of the serialization.
* @param encoding The encoding to use for serialization.
* @param options Options defining how to handle non-serializable items.
+ * @throws IllegalArgumentException if the slice cannot be serialized using the given options.
*/
public static void serializeSlice(@NonNull Slice s, @NonNull Context context,
@NonNull OutputStream output, @NonNull String encoding,
- @NonNull SerializeOptions options) throws IOException {
+ @NonNull SerializeOptions options) throws IOException, IllegalArgumentException {
SliceXml.serializeSlice(s, context, output, encoding, options);
}
@@ -76,13 +81,18 @@
* Parse a slice that has been previously serialized.
* <p>
* Parses a slice that was serialized with {@link #serializeSlice}.
+ * <p>
+ * Note: Slices returned by this cannot be passed to {@link SliceConvert#unwrap(Slice)}.
*
* @param input The input stream to read from.
* @param encoding The encoding to read as.
+ * @param listener Listener used to handle actions when reconstructing the slice.
+ * @throws SliceParseException if the InputStream cannot be parsed.
*/
- public static @NonNull Slice parseSlice(@NonNull InputStream input, @NonNull String encoding)
- throws IOException {
- return SliceXml.parseSlice(input, encoding);
+ public static @NonNull Slice parseSlice(@NonNull Context context, @NonNull InputStream input,
+ @NonNull String encoding, @NonNull SliceActionListener listener)
+ throws IOException, SliceParseException {
+ return SliceXml.parseSlice(context, input, encoding, listener);
}
/**
@@ -101,17 +111,22 @@
/**
* Constant indicating that the SliceItem should be serialized as much as possible.
* <p>
- * For images this means it will be replaced with an empty image. For actions, the
- * action will be removed but the content of the action will be serialized.
+ * For images this means they will be attempted to be serialized. For actions, the
+ * action will be removed but the content of the action will be serialized. The action
+ * may be triggered later on a de-serialized slice by binding the slice again and activating
+ * a pending-intent at the same location as the serialized action.
*/
- public static final int MODE_DISABLE = 2;
+ public static final int MODE_CONVERT = 2;
- @IntDef({MODE_THROW, MODE_REMOVE, MODE_DISABLE})
+ @IntDef({MODE_THROW, MODE_REMOVE, MODE_CONVERT})
+ @Retention(SOURCE)
@interface FormatMode {
}
private int mActionMode = MODE_THROW;
private int mImageMode = MODE_THROW;
+ private int mMaxWidth = 1000;
+ private int mMaxHeight = 1000;
/**
* @hide
@@ -149,6 +164,22 @@
}
/**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public int getMaxWidth() {
+ return mMaxWidth;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public int getMaxHeight() {
+ return mMaxHeight;
+ }
+
+ /**
* Sets how {@link android.app.slice.SliceItem#FORMAT_ACTION} items should be handled.
*
* The default mode is {@link #MODE_THROW}.
@@ -169,6 +200,30 @@
mImageMode = mode;
return this;
}
+
+ /**
+ * Set the maximum width of an image to use when serializing.
+ * <p>
+ * Will only be used if the {@link #setImageMode(int)} is set to {@link #MODE_CONVERT}.
+ * Any images larger than the maximum size will be scaled down to fit within that size.
+ * The default value is 1000.
+ */
+ public SerializeOptions setMaxImageWidth(int width) {
+ mMaxWidth = width;
+ return this;
+ }
+
+ /**
+ * Set the maximum height of an image to use when serializing.
+ * <p>
+ * Will only be used if the {@link #setImageMode(int)} is set to {@link #MODE_CONVERT}.
+ * Any images larger than the maximum size will be scaled down to fit within that size.
+ * The default value is 1000.
+ */
+ public SerializeOptions setMaxImageHeight(int height) {
+ mMaxHeight = height;
+ return this;
+ }
}
/**
@@ -255,4 +310,39 @@
}
return null;
}
+
+ /**
+ * A listener used to receive events on slices parsed with
+ * {@link #parseSlice(Context, InputStream, String, SliceActionListener)}.
+ */
+ public interface SliceActionListener {
+ /**
+ * Called when an action is triggered on a slice parsed with
+ * {@link #parseSlice(Context, InputStream, String, SliceActionListener)}.
+ * @param actionUri The uri of the action selected.
+ */
+ void onSliceAction(Uri actionUri);
+ }
+
+ /**
+ * Exception thrown during
+ * {@link #parseSlice(Context, InputStream, String, SliceActionListener)}.
+ */
+ public static class SliceParseException extends Exception {
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public SliceParseException(String s, Throwable e) {
+ super(s, e);
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public SliceParseException(String s) {
+ super(s);
+ }
+ }
}
diff --git a/slices/view/src/main/java/androidx/slice/SliceXml.java b/slices/view/src/main/java/androidx/slice/SliceXml.java
index 65f1e7d..50d1e17 100644
--- a/slices/view/src/main/java/androidx/slice/SliceXml.java
+++ b/slices/view/src/main/java/androidx/slice/SliceXml.java
@@ -20,22 +20,32 @@
import static org.xmlpull.v1.XmlPullParser.TEXT;
import android.annotation.SuppressLint;
+import android.content.ContentResolver;
import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
import android.net.Uri;
+import android.os.Build;
import android.text.Html;
import android.text.Spanned;
import android.text.TextUtils;
+import android.util.Base64;
import androidx.annotation.RestrictTo;
import androidx.core.graphics.drawable.IconCompat;
+import androidx.core.util.Consumer;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmlpull.v1.XmlSerializer;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -50,14 +60,24 @@
private static final String NAMESPACE = null;
private static final String TAG_SLICE = "slice";
+ private static final String TAG_ACTION = "action";
private static final String TAG_ITEM = "item";
private static final String ATTR_URI = "uri";
private static final String ATTR_FORMAT = "format";
private static final String ATTR_SUBTYPE = "subtype";
private static final String ATTR_HINTS = "hints";
+ private static final String ATTR_ICON_TYPE = "iconType";
+ private static final String ATTR_ICON_PACKAGE = "pkg";
+ private static final String ATTR_ICON_RES_TYPE = "resType";
- public static Slice parseSlice(InputStream input, String encoding) throws IOException {
+ private static final String ICON_TYPE_RES = "res";
+ private static final String ICON_TYPE_URI = "uri";
+ private static final String ICON_TYPE_DEFAULT = "def";
+
+ public static Slice parseSlice(Context context, InputStream input,
+ String encoding, SliceUtils.SliceActionListener listener)
+ throws IOException, SliceUtils.SliceParseException {
try {
XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
parser.setInput(input, encoding);
@@ -70,7 +90,7 @@
if (type != START_TAG) {
continue;
}
- s = parseSlice(parser);
+ s = parseSlice(context, parser, listener);
}
return s;
} catch (XmlPullParserException e) {
@@ -79,9 +99,10 @@
}
@SuppressLint("WrongConstant")
- private static Slice parseSlice(XmlPullParser parser)
- throws IOException, XmlPullParserException {
- if (!TAG_SLICE.equals(parser.getName())) {
+ private static Slice parseSlice(Context context, XmlPullParser parser,
+ SliceUtils.SliceActionListener listener)
+ throws IOException, XmlPullParserException, SliceUtils.SliceParseException {
+ if (!TAG_SLICE.equals(parser.getName()) && !TAG_ACTION.equals(parser.getName())) {
throw new IOException("Unexpected tag " + parser.getName());
}
int outerDepth = parser.getDepth();
@@ -94,20 +115,24 @@
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type == START_TAG && TAG_ITEM.equals(parser.getName())) {
- parseItem(b, parser);
+ parseItem(context, b, parser, listener);
}
}
return b.build();
}
@SuppressLint("WrongConstant")
- private static void parseItem(Slice.Builder b, XmlPullParser parser)
- throws IOException, XmlPullParserException {
+ private static void parseItem(Context context, Slice.Builder b,
+ XmlPullParser parser, final SliceUtils.SliceActionListener listener)
+ throws IOException, XmlPullParserException, SliceUtils.SliceParseException {
int type;
int outerDepth = parser.getDepth();
String format = parser.getAttributeValue(NAMESPACE, ATTR_FORMAT);
String subtype = parser.getAttributeValue(NAMESPACE, ATTR_SUBTYPE);
String hintStr = parser.getAttributeValue(NAMESPACE, ATTR_HINTS);
+ String iconType = parser.getAttributeValue(NAMESPACE, ATTR_ICON_TYPE);
+ String pkg = parser.getAttributeValue(NAMESPACE, ATTR_ICON_PACKAGE);
+ String resType = parser.getAttributeValue(NAMESPACE, ATTR_ICON_RES_TYPE);
String[] hints = hints(hintStr);
String v;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
@@ -118,16 +143,37 @@
// Nothing for now.
break;
case android.app.slice.SliceItem.FORMAT_IMAGE:
- v = parser.getText();
- if (!TextUtils.isEmpty(v)) {
- if (android.os.Build.VERSION.SDK_INT
- >= android.os.Build.VERSION_CODES.M) {
- String[] split = v.split(",");
- int w = Integer.parseInt(split[0]);
- int h = Integer.parseInt(split[1]);
- Bitmap image = Bitmap.createBitmap(w, h, Bitmap.Config.ALPHA_8);
+ switch (iconType) {
+ case ICON_TYPE_RES:
+ String resName = parser.getText();
+ try {
+ Resources r = context.getPackageManager()
+ .getResourcesForApplication(pkg);
+ int id = r.getIdentifier(resName, resType, pkg);
+ if (id != 0) {
+ b.addIcon(IconCompat.createWithResource(
+ context.createPackageContext(pkg, 0), id), subtype,
+ hints);
+ } else {
+ throw new SliceUtils.SliceParseException(
+ "Cannot find resource " + pkg + ":" + resType
+ + "/" + resName);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new SliceUtils.SliceParseException(
+ "Invalid icon package " + pkg, e);
+ }
+ break;
+ case ICON_TYPE_URI:
+ v = parser.getText();
+ b.addIcon(IconCompat.createWithContentUri(v), subtype, hints);
+ break;
+ default:
+ v = parser.getText();
+ byte[] data = Base64.decode(v, Base64.NO_WRAP);
+ Bitmap image = BitmapFactory.decodeByteArray(data, 0, data.length);
b.addIcon(IconCompat.createWithBitmap(image), subtype, hints);
- }
+ break;
}
break;
case android.app.slice.SliceItem.FORMAT_INT:
@@ -136,17 +182,28 @@
break;
case android.app.slice.SliceItem.FORMAT_TEXT:
v = parser.getText();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
+ // 19-21 don't allow special characters in XML, so we base64 encode it.
+ v = new String(Base64.decode(v, Base64.NO_WRAP));
+ }
b.addText(Html.fromHtml(v), subtype, hints);
break;
- case android.app.slice.SliceItem.FORMAT_TIMESTAMP:
+ case android.app.slice.SliceItem.FORMAT_LONG:
v = parser.getText();
- b.addTimestamp(Long.parseLong(v), subtype, hints);
+ b.addLong(Long.parseLong(v), subtype, hints);
break;
default:
throw new IllegalArgumentException("Unrecognized format " + format);
}
} else if (type == START_TAG && TAG_SLICE.equals(parser.getName())) {
- b.addSubSlice(parseSlice(parser), subtype);
+ b.addSubSlice(parseSlice(context, parser, listener), subtype);
+ } else if (type == START_TAG && TAG_ACTION.equals(parser.getName())) {
+ b.addAction(new Consumer<Uri>() {
+ @Override
+ public void accept(Uri uri) {
+ listener.onSliceAction(uri);
+ }
+ }, parseSlice(context, parser, listener), subtype);
}
}
}
@@ -162,7 +219,7 @@
serializer.setOutput(output, encoding);
serializer.startDocument(encoding, null);
- serialize(s, context, options, serializer);
+ serialize(s, context, options, serializer, false, null);
serializer.endDocument();
serializer.flush();
@@ -172,9 +229,12 @@
}
private static void serialize(Slice s, Context context, SliceUtils.SerializeOptions options,
- XmlSerializer serializer) throws IOException {
- serializer.startTag(NAMESPACE, TAG_SLICE);
+ XmlSerializer serializer, boolean isAction, String subType) throws IOException {
+ serializer.startTag(NAMESPACE, isAction ? TAG_ACTION : TAG_SLICE);
serializer.attribute(NAMESPACE, ATTR_URI, s.getUri().toString());
+ if (subType != null) {
+ serializer.attribute(NAMESPACE, ATTR_SUBTYPE, subType);
+ }
if (!s.getHints().isEmpty()) {
serializer.attribute(NAMESPACE, ATTR_HINTS, hintStr(s.getHints()));
}
@@ -182,7 +242,7 @@
serialize(item, context, options, serializer);
}
- serializer.endTag(NAMESPACE, TAG_SLICE);
+ serializer.endTag(NAMESPACE, isAction ? TAG_ACTION : TAG_SLICE);
}
private static void serialize(SliceItem item, Context context,
@@ -201,37 +261,65 @@
switch (format) {
case android.app.slice.SliceItem.FORMAT_ACTION:
- if (options.getActionMode() == SliceUtils.SerializeOptions.MODE_DISABLE) {
- serialize(item.getSlice(), context, options, serializer);
+ if (options.getActionMode() == SliceUtils.SerializeOptions.MODE_CONVERT) {
+ serialize(item.getSlice(), context, options, serializer, true,
+ item.getSubType());
+ } else if (options.getActionMode() == SliceUtils.SerializeOptions.MODE_THROW) {
+ throw new IllegalArgumentException("Slice contains an action " + item);
}
break;
case android.app.slice.SliceItem.FORMAT_REMOTE_INPUT:
// Nothing for now.
break;
case android.app.slice.SliceItem.FORMAT_IMAGE:
- if (options.getImageMode() == SliceUtils.SerializeOptions.MODE_DISABLE) {
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
- Drawable d = item.getIcon().loadDrawable(context);
- serializer.text(String.format("%d,%d",
- d.getIntrinsicWidth(), d.getIntrinsicHeight()));
+ if (options.getImageMode() == SliceUtils.SerializeOptions.MODE_CONVERT) {
+ IconCompat icon = item.getIcon();
+
+ switch (icon.getType()) {
+ case Icon.TYPE_RESOURCE:
+ serializeResIcon(serializer, icon, context);
+ break;
+ case Icon.TYPE_URI:
+ Uri uri = icon.getUri();
+ if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
+ serializeFileIcon(serializer, icon, context);
+ } else {
+ serializeIcon(serializer, icon, context, options);
+ }
+ break;
+ default:
+ serializeIcon(serializer, icon, context, options);
+ break;
}
+ } else if (options.getImageMode() == SliceUtils.SerializeOptions.MODE_THROW) {
+ throw new IllegalArgumentException("Slice contains an image " + item);
}
break;
case android.app.slice.SliceItem.FORMAT_INT:
serializer.text(String.valueOf(item.getInt()));
break;
case android.app.slice.SliceItem.FORMAT_SLICE:
- serialize(item.getSlice(), context, options, serializer);
+ serialize(item.getSlice(), context, options, serializer, false, item.getSubType());
break;
case android.app.slice.SliceItem.FORMAT_TEXT:
if (item.getText() instanceof Spanned) {
- serializer.text(Html.toHtml((Spanned) item.getText()));
+ String text = Html.toHtml((Spanned) item.getText());
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
+ // 19-21 don't allow special characters in XML, so we base64 encode it.
+ text = Base64.encodeToString(text.getBytes(), Base64.NO_WRAP);
+ }
+ serializer.text(text);
} else {
- serializer.text(String.valueOf(item.getText()));
+ String text = String.valueOf(item.getText());
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
+ // 19-21 don't allow special characters in XML, so we base64 encode it.
+ text = Base64.encodeToString(text.getBytes(), Base64.NO_WRAP);
+ }
+ serializer.text(text);
}
break;
- case android.app.slice.SliceItem.FORMAT_TIMESTAMP:
- serializer.text(String.valueOf(item.getTimestamp()));
+ case android.app.slice.SliceItem.FORMAT_LONG:
+ serializer.text(String.valueOf(item.getLong()));
break;
default:
throw new IllegalArgumentException("Unrecognized format " + format);
@@ -239,6 +327,53 @@
serializer.endTag(NAMESPACE, TAG_ITEM);
}
+ private static void serializeResIcon(XmlSerializer serializer, IconCompat icon, Context context)
+ throws IOException {
+ try {
+ Resources res = context.getPackageManager().getResourcesForApplication(
+ icon.getResPackage());
+ int id = icon.getResId();
+ serializer.attribute(NAMESPACE, ATTR_ICON_TYPE, ICON_TYPE_RES);
+ serializer.attribute(NAMESPACE, ATTR_ICON_PACKAGE, res.getResourcePackageName(id));
+ serializer.attribute(NAMESPACE, ATTR_ICON_RES_TYPE, res.getResourceTypeName(id));
+ serializer.text(res.getResourceEntryName(id));
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new IllegalArgumentException("Slice contains invalid icon", e);
+ }
+ }
+
+ private static void serializeFileIcon(XmlSerializer serializer, IconCompat icon,
+ Context context) throws IOException {
+ serializer.attribute(NAMESPACE, ATTR_ICON_TYPE, ICON_TYPE_URI);
+ serializer.text(icon.getUri().toString());
+ }
+
+ private static void serializeIcon(XmlSerializer serializer, IconCompat icon,
+ Context context, SliceUtils.SerializeOptions options) throws IOException {
+ Drawable d = icon.loadDrawable(context);
+ int width = d.getIntrinsicWidth();
+ int height = d.getIntrinsicHeight();
+ if (width > options.getMaxWidth()) {
+ height = (int) (options.getMaxWidth() * height / (double) width);
+ width = options.getMaxWidth();
+ }
+ if (height > options.getMaxHeight()) {
+ width = (int) (options.getMaxHeight() * width / (double) height);
+ height = options.getMaxHeight();
+ }
+ Bitmap b = Bitmap.createBitmap(width, height,
+ Bitmap.Config.ARGB_8888);
+ Canvas c = new Canvas(b);
+ d.setBounds(0, 0, c.getWidth(), c.getHeight());
+ d.draw(c);
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ b.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
+ b.recycle();
+
+ serializer.attribute(NAMESPACE, ATTR_ICON_TYPE, ICON_TYPE_DEFAULT);
+ serializer.text(new String(Base64.encode(outputStream.toByteArray(), Base64.NO_WRAP)));
+ }
+
private static String hintStr(List<String> hints) {
return TextUtils.join(",", hints);
}
diff --git a/slices/view/src/main/java/androidx/slice/widget/ActionRow.java b/slices/view/src/main/java/androidx/slice/widget/ActionRow.java
index bd6ad13..55bbca1 100644
--- a/slices/view/src/main/java/androidx/slice/widget/ActionRow.java
+++ b/slices/view/src/main/java/androidx/slice/widget/ActionRow.java
@@ -22,7 +22,6 @@
import static androidx.slice.core.SliceHints.ICON_IMAGE;
-import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.app.RemoteInput;
import android.app.slice.Slice;
@@ -132,7 +131,7 @@
} else if (action.hasHint(Slice.HINT_SHORTCUT)) {
final SliceActionImpl ac = new SliceActionImpl(action);
IconCompat iconItem = ac.getIcon();
- if (iconItem != null && ac.getAction() != null) {
+ if (iconItem != null && ac.getActionItem() != null) {
boolean tint = ac.getImageMode() == ICON_IMAGE;
addAction(iconItem, tint).setOnClickListener(
new OnClickListener() {
@@ -140,7 +139,7 @@
public void onClick(View v) {
try {
// TODO - should log events here
- ac.getAction().send();
+ ac.getActionItem().fireAction(null, null);
} catch (CanceledException e) {
e.printStackTrace();
}
@@ -165,7 +164,7 @@
new OnClickListener() {
@Override
public void onClick(View v) {
- handleRemoteInputClick(v, action.getAction(),
+ handleRemoteInputClick(v, action,
input.getRemoteInput());
}
});
@@ -182,7 +181,7 @@
}
@RequiresApi(21)
- private boolean handleRemoteInputClick(View view, PendingIntent pendingIntent,
+ private boolean handleRemoteInputClick(View view, SliceItem action,
RemoteInput input) {
if (input == null) {
return false;
@@ -223,7 +222,7 @@
Math.max((w - cx) + cy, (w - cx) + (h - cy)));
riv.setRevealParameters(cx, cy, r);
- riv.setPendingIntent(pendingIntent);
+ riv.setAction(action);
riv.setRemoteInput(new RemoteInput[] {
input
}, input);
diff --git a/slices/view/src/main/java/androidx/slice/widget/GridContent.java b/slices/view/src/main/java/androidx/slice/widget/GridContent.java
index 69d5c44..5ca395c 100644
--- a/slices/view/src/main/java/androidx/slice/widget/GridContent.java
+++ b/slices/view/src/main/java/androidx/slice/widget/GridContent.java
@@ -17,7 +17,6 @@
package androidx.slice.widget;
import static android.app.slice.Slice.HINT_ACTIONS;
-import static android.app.slice.Slice.HINT_LIST_ITEM;
import static android.app.slice.Slice.HINT_SEE_MORE;
import static android.app.slice.Slice.HINT_SHORTCUT;
import static android.app.slice.Slice.HINT_TITLE;
@@ -102,7 +101,11 @@
new String[] {HINT_ACTIONS} /* nonHints */);
mAllImages = true;
if (FORMAT_SLICE.equals(gridItem.getFormat())) {
- List<SliceItem> items = gridItem.getSlice().getItems().get(0).getSlice().getItems();
+ List<SliceItem> items = gridItem.getSlice().getItems();
+ if (items.size() == 1 && FORMAT_SLICE.equals(items.get(0).getFormat())) {
+ // TODO: this can be removed at release
+ items = items.get(0).getSlice().getItems();
+ }
items = filterAndProcessItems(items);
// Check if it it's only one item that is a slice
if (items.size() == 1 && items.get(0).getFormat().equals(FORMAT_SLICE)) {
@@ -197,11 +200,14 @@
List<SliceItem> filteredItems = new ArrayList<>();
for (int i = 0; i < items.size(); i++) {
SliceItem item = items.get(i);
- boolean isNonCellContent = item.hasAnyHints(HINT_SHORTCUT, HINT_SEE_MORE,
- HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED);
+ // TODO: This see more can be removed at release
+ boolean containsSeeMore = SliceQuery.find(item, null, HINT_SEE_MORE, null) != null;
+ boolean isNonCellContent = containsSeeMore
+ || item.hasAnyHints(HINT_SHORTCUT, HINT_SEE_MORE, HINT_KEYWORDS, HINT_TTL,
+ HINT_LAST_UPDATED);
if (SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType())) {
mContentDescr = item;
- } else if (item.hasHint(HINT_LIST_ITEM) && !isNonCellContent) {
+ } else if (!isNonCellContent) {
filteredItems.add(item);
}
}
@@ -224,6 +230,8 @@
/**
* @return the height to display a grid row at when it is used as a small template.
+ * Does not include padding that might be added by slice view attributes,
+ * see {@link ListContent#getListHeight(Context, List)}.
*/
public int getSmallHeight() {
return getHeight(true /* isSmall */);
@@ -231,6 +239,8 @@
/**
* @return the height the content in this template requires to be displayed.
+ * Does not include padding that might be added by slice view attributes,
+ * see {@link ListContent#getListHeight(Context, List)}.
*/
public int getActualHeight() {
return getHeight(false /* isSmall */);
diff --git a/slices/view/src/main/java/androidx/slice/widget/GridRowView.java b/slices/view/src/main/java/androidx/slice/widget/GridRowView.java
index 1bb90e7..9d06d85 100644
--- a/slices/view/src/main/java/androidx/slice/widget/GridRowView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/GridRowView.java
@@ -48,7 +48,6 @@
import androidx.annotation.ColorInt;
import androidx.annotation.RestrictTo;
-import androidx.slice.Slice;
import androidx.slice.SliceItem;
import androidx.slice.core.SliceQuery;
import androidx.slice.view.R;
@@ -79,9 +78,12 @@
private static final int MAX_CELL_IMAGES = 1;
private int mRowIndex;
+ private int mRowCount;
+
private int mSmallImageSize;
private int mIconSize;
private int mGutter;
+ private int mTextPadding;
private GridContent mGridContent;
private LinearLayout mViewContainer;
@@ -100,17 +102,43 @@
mIconSize = res.getDimensionPixelSize(R.dimen.abc_slice_icon_size);
mSmallImageSize = res.getDimensionPixelSize(R.dimen.abc_slice_small_image_size);
mGutter = res.getDimensionPixelSize(R.dimen.abc_slice_grid_gutter);
+ mTextPadding = res.getDimensionPixelSize(R.dimen.abc_slice_grid_text_padding);
}
@Override
public int getSmallHeight() {
// GridRow is small if its the first element in a list without a header presented in small
- return mGridContent != null ? mGridContent.getSmallHeight() : 0;
+ if (mGridContent == null) {
+ return 0;
+ }
+ return mGridContent.getSmallHeight() + getExtraTopPadding() + getExtraBottomPadding();
}
@Override
public int getActualHeight() {
- return mGridContent != null ? mGridContent.getActualHeight() : 0;
+ if (mGridContent == null) {
+ return 0;
+ }
+ return mGridContent.getActualHeight() + getExtraTopPadding() + getExtraBottomPadding();
+ }
+
+ private int getExtraTopPadding() {
+ if (mGridContent != null && mGridContent.isAllImages()) {
+ // Might need to add padding if in first or last position
+ if (mRowIndex == 0) {
+ return mGridTopPadding;
+ }
+ }
+ return 0;
+ }
+
+ private int getExtraBottomPadding() {
+ if (mGridContent != null && mGridContent.isAllImages()) {
+ if (mRowIndex == mRowCount - 1 || getMode() == MODE_SMALL) {
+ return mGridBottomPadding;
+ }
+ }
+ return 0;
}
@Override
@@ -132,22 +160,19 @@
}
}
- @Override
- public void setSlice(Slice slice) {
- // Nothing to do
- }
-
/**
* This is called when GridView is being used as a component in a larger template.
*/
@Override
- public void setSliceItem(SliceItem slice, boolean isHeader, int index,
- SliceView.OnSliceActionListener observer) {
+ public void setSliceItem(SliceItem slice, boolean isHeader, int rowIndex,
+ int rowCount, SliceView.OnSliceActionListener observer) {
resetView();
setSliceActionListener(observer);
- mRowIndex = index;
+ mRowIndex = rowIndex;
+ mRowCount = rowCount;
mGridContent = new GridContent(getContext(), slice);
populateViews(mGridContent);
+ mViewContainer.setPadding(0, getExtraTopPadding(), 0, getExtraBottomPadding());
}
private void populateViews(GridContent gc) {
@@ -204,6 +229,11 @@
seeMoreView = (LinearLayout) inflater.inflate(
R.layout.abc_slice_grid_see_more, mViewContainer, false);
extraText = seeMoreView.findViewById(R.id.text_see_more_count);
+
+ // Update text appearance
+ TextView moreText = seeMoreView.findViewById(R.id.text_see_more);
+ moreText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mGridTitleSize);
+ moreText.setTextColor(mTitleColor);
}
mViewContainer.addView(seeMoreView, new LinearLayout.LayoutParams(0, MATCH_PARENT, 1));
extraText.setText(getResources().getString(R.string.abc_slice_more_content, numExtra));
@@ -249,25 +279,29 @@
Iterator<SliceItem> iterator = textItems.iterator();
while (textItems.size() > 1) {
SliceItem item = iterator.next();
- if (!item.hasHint(HINT_TITLE)) {
+ if (!item.hasAnyHints(HINT_TITLE, HINT_LARGE)) {
iterator.remove();
}
}
}
+ SliceItem prevItem = null;
for (int i = 0; i < cellItems.size(); i++) {
SliceItem item = cellItems.get(i);
final String itemFormat = item.getFormat();
+ int padding = determinePadding(prevItem);
if (textCount < maxCellText && (FORMAT_TEXT.equals(itemFormat)
|| FORMAT_TIMESTAMP.equals(itemFormat))) {
if (textItems != null && !textItems.contains(item)) {
continue;
}
- if (addItem(item, mTintColor, cellContainer, singleItem)) {
+ if (addItem(item, mTintColor, cellContainer, padding)) {
+ prevItem = item;
textCount++;
added = true;
}
} else if (imageCount < MAX_CELL_IMAGES && FORMAT_IMAGE.equals(item.getFormat())) {
- if (addItem(item, mTintColor, cellContainer, singleItem)) {
+ if (addItem(item, mTintColor, cellContainer, 0)) {
+ prevItem = item;
imageCount++;
added = true;
}
@@ -300,22 +334,28 @@
/**
* Adds simple items to a container. Simple items include icons, text, and timestamps.
+ *
+ * @param item item to add to the container.
+ * @param container the container to add to.
+ * @param padding the padding to apply to the item.
+ *
* @return Whether an item was added.
*/
- private boolean addItem(SliceItem item, int color, ViewGroup container, boolean singleItem) {
+ private boolean addItem(SliceItem item, int color, ViewGroup container, int padding) {
final String format = item.getFormat();
View addedView = null;
if (FORMAT_TEXT.equals(format) || FORMAT_TIMESTAMP.equals(format)) {
boolean title = SliceQuery.hasAnyHints(item, HINT_LARGE, HINT_TITLE);
TextView tv = (TextView) LayoutInflater.from(getContext()).inflate(title
? TITLE_TEXT_LAYOUT : TEXT_LAYOUT, null);
- tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, title ? mTitleSize : mSubtitleSize);
+ tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, title ? mGridTitleSize : mGridSubtitleSize);
tv.setTextColor(title ? mTitleColor : mSubtitleColor);
CharSequence text = FORMAT_TIMESTAMP.equals(format)
? SliceViewUtil.getRelativeTimeString(item.getTimestamp())
: item.getText();
tv.setText(text);
container.addView(tv);
+ tv.setPadding(0, padding, 0, 0);
addedView = tv;
} else if (FORMAT_IMAGE.equals(format)) {
ImageView iv = new ImageView(getContext());
@@ -339,6 +379,19 @@
return addedView != null;
}
+ private int determinePadding(SliceItem prevItem) {
+ if (prevItem == null) {
+ // No need for top padding
+ return 0;
+ } else if (FORMAT_IMAGE.equals(prevItem.getFormat())) {
+ return mTextPadding;
+ } else if (FORMAT_TEXT.equals(prevItem.getFormat())
+ || FORMAT_LONG.equals(prevItem.getFormat())) {
+ return mVerticalGridTextPadding;
+ }
+ return 0;
+ }
+
private void makeClickable(View layout, boolean isClickable) {
layout.setOnClickListener(isClickable ? this : null);
layout.setBackground(isClickable
@@ -354,7 +407,7 @@
final EventInfo info = tagItem.second;
if (actionItem != null && FORMAT_ACTION.equals(actionItem.getFormat())) {
try {
- actionItem.getAction().send();
+ actionItem.fireAction(null, null);
if (mObserver != null) {
mObserver.onSliceAction(info, actionItem);
}
diff --git a/slices/view/src/main/java/androidx/slice/widget/LargeSliceAdapter.java b/slices/view/src/main/java/androidx/slice/widget/LargeSliceAdapter.java
index 9270353c..81059d4 100644
--- a/slices/view/src/main/java/androidx/slice/widget/LargeSliceAdapter.java
+++ b/slices/view/src/main/java/androidx/slice/widget/LargeSliceAdapter.java
@@ -17,6 +17,7 @@
package androidx.slice.widget;
import static android.app.slice.Slice.HINT_HORIZONTAL;
+import static android.app.slice.Slice.HINT_SUMMARY;
import static android.app.slice.Slice.SUBTYPE_MESSAGE;
import static android.app.slice.Slice.SUBTYPE_SOURCE;
import static android.app.slice.SliceItem.FORMAT_INT;
@@ -103,14 +104,14 @@
/**
* Set the {@link SliceItem}'s to be displayed in the adapter and the accent color.
*/
- public void setSliceItems(List<SliceItem> slices, int color) {
+ public void setSliceItems(List<SliceItem> slices, int color, int mode) {
if (slices == null) {
mSlices.clear();
} else {
mIdGen.resetUsage();
mSlices = new ArrayList<>(slices.size());
for (SliceItem s : slices) {
- mSlices.add(new SliceWrapper(s, mIdGen));
+ mSlices.add(new SliceWrapper(s, mIdGen, mode));
}
}
mColor = color;
@@ -199,10 +200,10 @@
private final int mType;
private final long mId;
- public SliceWrapper(SliceItem item, IdGenerator idGen) {
+ public SliceWrapper(SliceItem item, IdGenerator idGen, int mode) {
mItem = item;
mType = getFormat(item);
- mId = idGen.getId(item);
+ mId = idGen.getId(item, mode);
}
public static int getFormat(SliceItem item) {
@@ -250,12 +251,12 @@
mSliceChildView.setMode(mode);
mSliceChildView.setTint(mColor);
mSliceChildView.setStyle(mAttrs, mDefStyleAttr, mDefStyleRes);
- mSliceChildView.setSliceItem(item, isHeader, position, mSliceObserver);
- if (isHeader && mSliceChildView instanceof RowView) {
+ mSliceChildView.setSliceItem(item, isHeader, position, getItemCount(), mSliceObserver);
+ mSliceChildView.setSliceActions(isHeader ? mSliceActions : null);
+ mSliceChildView.setLastUpdated(isHeader ? mLastUpdated : -1);
+ mSliceChildView.setShowLastUpdated(isHeader && mShowLastUpdated);
+ if (mSliceChildView instanceof RowView) {
((RowView) mSliceChildView).setSingleItem(getItemCount() == 1);
- mSliceChildView.setSliceActions(mSliceActions);
- mSliceChildView.setLastUpdated(mLastUpdated);
- mSliceChildView.setShowLastUpdated(mShowLastUpdated);
}
int[] info = new int[2];
info[0] = ListContent.getRowType(mContext, item, isHeader, mSliceActions);
@@ -285,8 +286,12 @@
private final ArrayMap<String, Long> mCurrentIds = new ArrayMap<>();
private final ArrayMap<String, Integer> mUsedIds = new ArrayMap<>();
- public long getId(SliceItem item) {
+ public long getId(SliceItem item, int mode) {
String str = genString(item);
+ SliceItem summary = SliceQuery.find(item, null, HINT_SUMMARY, null);
+ if (summary != null) {
+ str += mode; // mode matters
+ }
if (!mCurrentIds.containsKey(str)) {
mCurrentIds.put(str, mNextLong++);
}
diff --git a/slices/view/src/main/java/androidx/slice/widget/LargeTemplateView.java b/slices/view/src/main/java/androidx/slice/widget/LargeTemplateView.java
index 6632097..0cba347 100644
--- a/slices/view/src/main/java/androidx/slice/widget/LargeTemplateView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/LargeTemplateView.java
@@ -16,7 +16,7 @@
package androidx.slice.widget;
-import static android.app.slice.Slice.HINT_HORIZONTAL;
+import static androidx.slice.widget.SliceView.MODE_SMALL;
import android.content.Context;
import android.os.Build;
@@ -28,7 +28,6 @@
import androidx.annotation.RestrictTo;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
-import androidx.slice.Slice;
import androidx.slice.SliceItem;
import java.util.ArrayList;
@@ -45,7 +44,6 @@
private final View mForeground;
private final LargeSliceAdapter mAdapter;
private final RecyclerView mRecyclerView;
- private Slice mSlice;
private boolean mIsScrollable;
private ListContent mListContent;
private List<SliceItem> mDisplayedItems = new ArrayList<>();
@@ -131,13 +129,8 @@
return 0;
}
SliceItem headerItem = mListContent.getHeaderItem();
- if (headerItem.hasHint(HINT_HORIZONTAL)) {
- GridContent gc = new GridContent(getContext(), headerItem);
- return gc.getSmallHeight();
- } else {
- RowContent rc = new RowContent(getContext(), headerItem, mListContent.hasHeader());
- return rc.getSmallHeight();
- }
+ return mListContent.getHeight(getContext(), headerItem, true /* isHeader */,
+ 0 /* rowIndex */, 1 /* rowCount */, MODE_SMALL);
}
@Override
@@ -160,8 +153,8 @@
}
@Override
- public void setSlice(Slice slice) {
- mSlice = slice;
+ public void setSliceContent(ListContent sliceContent) {
+ mListContent = sliceContent;
populate();
}
@@ -184,11 +177,10 @@
}
private void populate() {
- if (mSlice == null) {
+ if (mListContent == null) {
resetView();
return;
}
- mListContent = new ListContent(getContext(), mSlice);
updateDisplayedItems(getMeasuredHeight());
}
@@ -216,21 +208,21 @@
} else {
mDisplayedItems = mListContent.getRowItems();
}
- mDisplayedItemsHeight = ListContent.getListHeight(getContext(), mDisplayedItems);
- if (getMode() == SliceView.MODE_LARGE) {
- mAdapter.setSliceItems(mDisplayedItems, mTintColor);
- } else if (getMode() == SliceView.MODE_SMALL) {
+ mDisplayedItemsHeight = mListContent.getListHeight(getContext(), mDisplayedItems);
+ int mode = getMode();
+ if (mode == SliceView.MODE_LARGE) {
+ mAdapter.setSliceItems(mDisplayedItems, mTintColor, mode);
+ } else if (mode == MODE_SMALL) {
mAdapter.setSliceItems(
- Collections.singletonList(mDisplayedItems.get(0)), mTintColor);
+ Collections.singletonList(mDisplayedItems.get(0)), mTintColor, mode);
}
}
@Override
public void resetView() {
- mSlice = null;
mDisplayedItemsHeight = 0;
mDisplayedItems.clear();
- mAdapter.setSliceItems(null, -1);
+ mAdapter.setSliceItems(null, -1, getMode());
mListContent = null;
}
}
diff --git a/slices/view/src/main/java/androidx/slice/widget/ListContent.java b/slices/view/src/main/java/androidx/slice/widget/ListContent.java
index 1dca6cb..8f3cdf2 100644
--- a/slices/view/src/main/java/androidx/slice/widget/ListContent.java
+++ b/slices/view/src/main/java/androidx/slice/widget/ListContent.java
@@ -30,8 +30,12 @@
import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
import static androidx.slice.core.SliceHints.HINT_LAST_UPDATED;
import static androidx.slice.core.SliceHints.HINT_TTL;
+import static androidx.slice.widget.SliceView.MODE_LARGE;
+import static androidx.slice.widget.SliceView.MODE_SMALL;
import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -42,6 +46,7 @@
import androidx.slice.core.SliceAction;
import androidx.slice.core.SliceActionImpl;
import androidx.slice.core.SliceQuery;
+import androidx.slice.view.R;
import java.util.ArrayList;
import java.util.List;
@@ -53,6 +58,7 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class ListContent {
+ private Slice mSlice;
private SliceItem mHeaderItem;
private SliceItem mColorItem;
private SliceItem mSeeMoreItem;
@@ -60,8 +66,54 @@
private List<SliceItem> mSliceActions;
private Context mContext;
- public ListContent(Context context, Slice slice) {
+ private int mHeaderTitleSize;
+ private int mHeaderSubtitleSize;
+ private int mVerticalHeaderTextPadding;
+ private int mTitleSize;
+ private int mSubtitleSize;
+ private int mVerticalTextPadding;
+ private int mGridTitleSize;
+ private int mGridSubtitleSize;
+ private int mVerticalGridTextPadding;
+ private int mGridTopPadding;
+ private int mGridBottomPadding;
+
+ public ListContent(Context context, Slice slice, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ mSlice = slice;
mContext = context;
+
+ // TODO: duplicated code from SliceChildView; could do something better
+ // Some of this information will impact the size calculations for slice content.
+ TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SliceView,
+ defStyleAttr, defStyleRes);
+ try {
+ mHeaderTitleSize = (int) a.getDimension(
+ R.styleable.SliceView_headerTitleSize, 0);
+ mHeaderSubtitleSize = (int) a.getDimension(
+ R.styleable.SliceView_headerSubtitleSize, 0);
+ mVerticalHeaderTextPadding = (int) a.getDimension(
+ R.styleable.SliceView_headerTextVerticalPadding, 0);
+
+ mTitleSize = (int) a.getDimension(R.styleable.SliceView_titleSize, 0);
+ mSubtitleSize = (int) a.getDimension(
+ R.styleable.SliceView_subtitleSize, 0);
+ mVerticalTextPadding = (int) a.getDimension(
+ R.styleable.SliceView_textVerticalPadding, 0);
+
+ mGridTitleSize = (int) a.getDimension(R.styleable.SliceView_gridTitleSize, 0);
+ mGridSubtitleSize = (int) a.getDimension(
+ R.styleable.SliceView_gridSubtitleSize, 0);
+ int defaultVerticalGridPadding = context.getResources().getDimensionPixelSize(
+ R.dimen.abc_slice_grid_text_inner_padding);
+ mVerticalGridTextPadding = (int) a.getDimension(
+ R.styleable.SliceView_gridTextVerticalPadding, defaultVerticalGridPadding);
+ mGridTopPadding = (int) a.getDimension(R.styleable.SliceView_gridTopPadding, 0);
+ mGridBottomPadding = (int) a.getDimension(R.styleable.SliceView_gridTopPadding, 0);
+ } finally {
+ a.recycle();
+ }
+
populate(slice);
}
@@ -107,7 +159,7 @@
*
* @return the total height of all the rows contained in the provided list.
*/
- public static int getListHeight(Context context, List<SliceItem> listItems) {
+ public int getListHeight(Context context, List<SliceItem> listItems) {
if (listItems == null) {
return 0;
}
@@ -119,10 +171,12 @@
hasRealHeader = !maybeHeader.hasAnyHints(HINT_LIST_ITEM, HINT_HORIZONTAL);
}
if (listItems.size() == 1 && !maybeHeader.hasHint(HINT_HORIZONTAL)) {
- return getHeight(context, maybeHeader, true);
+ return getHeight(context, maybeHeader, true /* isHeader */, 0, 1, MODE_LARGE);
}
+ int rowCount = listItems.size();
for (int i = 0; i < listItems.size(); i++) {
- height += getHeight(context, listItems.get(i), i == 0 && hasRealHeader /* isHeader */);
+ height += getHeight(context, listItems.get(i), i == 0 && hasRealHeader /* isHeader */,
+ i, rowCount, MODE_LARGE);
}
return height;
}
@@ -149,8 +203,10 @@
RowContent rc = new RowContent(mContext, mSeeMoreItem, false /* isHeader */);
visibleHeight += rc.getActualHeight();
}
- for (int i = 0; i < mRowItems.size(); i++) {
- int itemHeight = getHeight(mContext, mRowItems.get(i), i == 0 /* isHeader */);
+ int rowCount = mRowItems.size();
+ for (int i = 0; i < rowCount; i++) {
+ int itemHeight = getHeight(mContext, mRowItems.get(i), i == 0 /* isHeader */,
+ i, rowCount, MODE_LARGE);
if ((height == -1 && i > idealItemCount)
|| (height > 0 && visibleHeight + itemHeight > height)) {
break;
@@ -170,13 +226,20 @@
return visibleItems;
}
- private static int getHeight(Context context, SliceItem item, boolean isHeader) {
+ /**
+ * Determines the height of the provided {@link SliceItem}.
+ */
+ public int getHeight(Context context, SliceItem item, boolean isHeader, int index,
+ int count, int mode) {
if (item.hasHint(HINT_HORIZONTAL)) {
GridContent gc = new GridContent(context, item);
- return gc.getActualHeight();
+ int topPadding = gc.isAllImages() && index == 0 ? mGridTopPadding : 0;
+ int bottomPadding = gc.isAllImages() && index == count - 1 ? mGridBottomPadding : 0;
+ int height = mode == MODE_SMALL ? gc.getSmallHeight() : gc.getActualHeight();
+ return height + topPadding + bottomPadding;
} else {
RowContent rc = new RowContent(context, item, isHeader);
- return rc.getActualHeight();
+ return mode == MODE_SMALL ? rc.getSmallHeight() : rc.getActualHeight();
}
}
@@ -188,6 +251,11 @@
}
@Nullable
+ public Slice getSlice() {
+ return mSlice;
+ }
+
+ @Nullable
public SliceItem getColorItem() {
return mColorItem;
}
@@ -291,7 +359,7 @@
private static SliceItem findHeaderItem(@NonNull Slice slice) {
// See if header is specified
String[] nonHints = new String[] {HINT_LIST_ITEM, HINT_SHORTCUT, HINT_ACTIONS,
- HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED};
+ HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED, HINT_HORIZONTAL};
SliceItem header = SliceQuery.find(slice, FORMAT_SLICE, null, nonHints);
if (header != null && isValidHeader(header)) {
return header;
@@ -302,7 +370,7 @@
@Nullable
private static SliceItem getSeeMoreItem(@NonNull Slice slice) {
SliceItem item = SliceQuery.find(slice, null, HINT_SEE_MORE, null);
- if (item != null && item.hasHint(HINT_SEE_MORE)) {
+ if (item != null) {
if (FORMAT_SLICE.equals(item.getFormat())) {
List<SliceItem> items = item.getSlice().getItems();
if (items.size() == 1 && FORMAT_ACTION.equals(items.get(0).getFormat())) {
diff --git a/slices/view/src/main/java/androidx/slice/widget/MessageView.java b/slices/view/src/main/java/androidx/slice/widget/MessageView.java
index 6035d0a..c597ed4 100644
--- a/slices/view/src/main/java/androidx/slice/widget/MessageView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/MessageView.java
@@ -30,7 +30,6 @@
import android.widget.TextView;
import androidx.annotation.RestrictTo;
-import androidx.slice.Slice;
import androidx.slice.SliceItem;
import androidx.slice.core.SliceQuery;
@@ -57,11 +56,6 @@
}
@Override
- public void setSlice(Slice slice) {
- // Do nothing it's always a list item
- }
-
- @Override
public void resetView() {
// TODO
}
@@ -75,7 +69,7 @@
@Override
public void setSliceItem(SliceItem slice, boolean isHeader, int index,
- SliceView.OnSliceActionListener observer) {
+ int rowCount, SliceView.OnSliceActionListener observer) {
setSliceActionListener(observer);
mRowIndex = index;
SliceItem source = SliceQuery.findSubtype(slice, FORMAT_IMAGE, SUBTYPE_SOURCE);
diff --git a/slices/view/src/main/java/androidx/slice/widget/RemoteInputView.java b/slices/view/src/main/java/androidx/slice/widget/RemoteInputView.java
index 1792f29..d4ff299 100644
--- a/slices/view/src/main/java/androidx/slice/widget/RemoteInputView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/RemoteInputView.java
@@ -50,6 +50,7 @@
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.content.ContextCompat;
+import androidx.slice.SliceItem;
import androidx.slice.view.R;
/**
@@ -72,7 +73,7 @@
private RemoteEditText mEditText;
private ImageButton mSendButton;
private ProgressBar mProgressBar;
- private PendingIntent mPendingIntent;
+ private SliceItem mAction;
private RemoteInput[] mRemoteInputs;
private RemoteInput mRemoteInput;
@@ -139,11 +140,11 @@
// but that's an edge case, and also because we can't always know which package will receive
// an intent, so we just reset for the creator.
//getContext().getSystemService(ShortcutManager.class).onApplicationActive(
- // mPendingIntent.getCreatorPackage(),
+ // mAction.getCreatorPackage(),
// getContext().getUserId());
try {
- mPendingIntent.send(getContext(), 0, fillInIntent);
+ mAction.fireAction(getContext(), fillInIntent);
reset();
} catch (PendingIntent.CanceledException e) {
Log.i(TAG, "Unable to send remote input result", e);
@@ -185,8 +186,8 @@
/**
* Set the pending intent for remote input.
*/
- public void setPendingIntent(PendingIntent pendingIntent) {
- mPendingIntent = pendingIntent;
+ public void setAction(SliceItem action) {
+ mAction = action;
}
/**
diff --git a/slices/view/src/main/java/androidx/slice/widget/RowContent.java b/slices/view/src/main/java/androidx/slice/widget/RowContent.java
index c600af5..c865b02 100644
--- a/slices/view/src/main/java/androidx/slice/widget/RowContent.java
+++ b/slices/view/src/main/java/androidx/slice/widget/RowContent.java
@@ -16,7 +16,7 @@
package androidx.slice.widget;
-import static android.app.slice.Slice.HINT_ACTIONS;
+import static android.app.slice.Slice.HINT_HORIZONTAL;
import static android.app.slice.Slice.HINT_PARTIAL;
import static android.app.slice.Slice.HINT_SEE_MORE;
import static android.app.slice.Slice.HINT_SHORTCUT;
@@ -95,15 +95,7 @@
Log.w(TAG, "Provided SliceItem is invalid for RowContent");
return false;
}
- // Find primary action first (otherwise filtered out of valid row items)
- String[] hints = new String[] {HINT_SHORTCUT, HINT_TITLE};
- mPrimaryAction = SliceQuery.find(rowSlice, FORMAT_SLICE, hints,
- new String[] { HINT_ACTIONS, HINT_KEYWORDS} /* nonHints */);
-
- if (mPrimaryAction == null && FORMAT_ACTION.equals(rowSlice.getFormat())
- && rowSlice.getSlice().getItems().size() == 1) {
- mPrimaryAction = rowSlice;
- }
+ determineStartAndPrimaryAction(rowSlice);
mContentDescr = SliceQuery.findSubtype(rowSlice, FORMAT_TEXT, SUBTYPE_CONTENT_DESCRIPTION);
@@ -112,7 +104,7 @@
// If we've only got one item that's a slice / action use those items instead
if (rowItems.size() == 1 && (FORMAT_ACTION.equals(rowItems.get(0).getFormat())
|| FORMAT_SLICE.equals(rowItems.get(0).getFormat()))
- && !rowItems.get(0).hasHint(HINT_SHORTCUT)) {
+ && !rowItems.get(0).hasAnyHints(HINT_SHORTCUT, HINT_TITLE)) {
if (isValidRow(rowItems.get(0))) {
rowSlice = rowItems.get(0);
rowItems = filterInvalidItems(rowSlice);
@@ -122,14 +114,12 @@
mRange = rowSlice;
}
if (rowItems.size() > 0) {
- // Start item
- SliceItem firstItem = rowItems.get(0);
- if (FORMAT_SLICE.equals(firstItem.getFormat())) {
- SliceItem unwrappedItem = firstItem.getSlice().getItems().get(0);
- if (isStartType(unwrappedItem)) {
- mStartItem = unwrappedItem;
- rowItems.remove(0);
- }
+ // Remove the things we already know about
+ if (mStartItem != null) {
+ rowItems.remove(mStartItem);
+ }
+ if (mPrimaryAction != null) {
+ rowItems.remove(mPrimaryAction);
}
// Text + end items
@@ -155,23 +145,18 @@
if (hasText(mSubtitleItem)) {
mLineCount++;
}
- // Special rules for end items: only one timestamp, can't be mixture of icons / actions
+ // Special rules for end items: only one timestamp
boolean hasTimestamp = mStartItem != null
&& FORMAT_TIMESTAMP.equals(mStartItem.getFormat());
- String desiredFormat = null;
for (int i = 0; i < endItems.size(); i++) {
final SliceItem item = endItems.get(i);
- boolean isAction = FORMAT_SLICE.equals(item.getFormat())
- && item.hasHint(HINT_SHORTCUT);
+ boolean isAction = SliceQuery.find(item, FORMAT_ACTION) != null;
if (FORMAT_TIMESTAMP.equals(item.getFormat())) {
if (!hasTimestamp) {
hasTimestamp = true;
mEndItems.add(item);
}
- } else if (desiredFormat == null) {
- desiredFormat = item.getFormat();
- processContent(item, isAction);
- } else if (desiredFormat.equals(item.getFormat())) {
+ } else {
processContent(item, isAction);
}
}
@@ -191,6 +176,37 @@
}
/**
+ * Sets the {@link #getPrimaryAction()} and {@link #getStartItem()} for this row.
+ */
+ private void determineStartAndPrimaryAction(@NonNull SliceItem rowSlice) {
+ List<SliceItem> possibleStartItems = SliceQuery.findAll(rowSlice, null, HINT_TITLE, null);
+ if (possibleStartItems.size() > 0) {
+ // The start item will be at position 0 if it exists
+ String format = possibleStartItems.get(0).getFormat();
+ if ((FORMAT_ACTION.equals(format)
+ && SliceQuery.find(possibleStartItems.get(0), FORMAT_IMAGE) != null)
+ || FORMAT_SLICE.equals(format)
+ || FORMAT_LONG.equals(format)
+ || FORMAT_IMAGE.equals(format)) {
+ mStartItem = possibleStartItems.get(0);
+ }
+ }
+
+ String[] hints = new String[] {HINT_SHORTCUT, HINT_TITLE};
+ List<SliceItem> possiblePrimaries = SliceQuery.findAll(rowSlice, FORMAT_SLICE, hints, null);
+ if (possiblePrimaries.isEmpty() && FORMAT_ACTION.equals(rowSlice.getFormat())
+ && rowSlice.getSlice().getItems().size() == 1) {
+ mPrimaryAction = rowSlice;
+ } else if (mStartItem != null && possiblePrimaries.size() > 1
+ && possiblePrimaries.get(0) == mStartItem) {
+ // Next item is the primary action
+ mPrimaryAction = possiblePrimaries.get(1);
+ } else if (possiblePrimaries.size() > 0) {
+ mPrimaryAction = possiblePrimaries.get(0);
+ }
+ }
+
+ /**
* @return the {@link SliceItem} used to populate this row.
*/
@NonNull
@@ -207,6 +223,22 @@
}
/**
+ * @return the {@link SliceItem} for the icon to use for the input range thumb drawable.
+ */
+ @Nullable
+ public SliceItem getInputRangeThumb() {
+ if (mRange != null) {
+ List<SliceItem> items = mRange.getSlice().getItems();
+ for (int i = 0; i < items.size(); i++) {
+ if (FORMAT_IMAGE.equals(items.get(i).getFormat())) {
+ return items.get(i);
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
* @return the {@link SliceItem} used for the main intent for this row; can be null.
*/
@Nullable
@@ -326,6 +358,7 @@
*/
public boolean isValid() {
return mStartItem != null
+ || mPrimaryAction != null
|| mTitleItem != null
|| mSubtitleItem != null
|| mEndItems.size() > 0
@@ -343,17 +376,17 @@
// Must be slice or action
if (FORMAT_SLICE.equals(rowSlice.getFormat())
|| FORMAT_ACTION.equals(rowSlice.getFormat())) {
- // Must have at least one legitimate child
List<SliceItem> rowItems = rowSlice.getSlice().getItems();
+ // Special case: default see more just has an action but no other items
+ if (rowSlice.hasHint(HINT_SEE_MORE) && rowItems.isEmpty()) {
+ return true;
+ }
+ // Must have at least one legitimate child
for (int i = 0; i < rowItems.size(); i++) {
if (isValidRowContent(rowSlice, rowItems.get(i))) {
return true;
}
}
- // Special case: default see more just has an action but no other items
- if (rowSlice.hasHint(HINT_SEE_MORE) && rowItems.isEmpty()) {
- return true;
- }
}
return false;
}
@@ -373,27 +406,20 @@
}
/**
- * @return whether this item is valid content to display in a row.
+ * @return whether this item is valid content to visibly appear in a row.
*/
private static boolean isValidRowContent(SliceItem slice, SliceItem item) {
- if (item.hasAnyHints(HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED)) {
+ if (item.hasAnyHints(HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED, HINT_HORIZONTAL)
+ || SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType())) {
return false;
}
- if (FORMAT_SLICE.equals(item.getFormat()) && !item.hasHint(HINT_SHORTCUT)) {
- // Unpack contents of slice
- item = item.getSlice().getItems().get(0);
- }
final String itemFormat = item.getFormat();
- return (FORMAT_TEXT.equals(itemFormat)
- && !SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType()))
- || FORMAT_IMAGE.equals(itemFormat)
+ return FORMAT_IMAGE.equals(itemFormat)
+ || FORMAT_TEXT.equals(itemFormat)
|| FORMAT_TIMESTAMP.equals(itemFormat)
- || FORMAT_REMOTE_INPUT.equals(itemFormat)
- || (FORMAT_SLICE.equals(itemFormat) && item.hasHint(HINT_TITLE)
- && !item.hasHint(HINT_SHORTCUT))
- || (FORMAT_SLICE.equals(itemFormat) && item.hasHint(HINT_SHORTCUT)
- && !item.hasHint(HINT_TITLE))
|| FORMAT_ACTION.equals(itemFormat)
+ || FORMAT_REMOTE_INPUT.equals(itemFormat)
+ || FORMAT_SLICE.equals(itemFormat)
|| (FORMAT_INT.equals(itemFormat) && SUBTYPE_RANGE.equals(slice.getSubType()));
}
diff --git a/slices/view/src/main/java/androidx/slice/widget/RowView.java b/slices/view/src/main/java/androidx/slice/widget/RowView.java
index 4f25fed..341f21c 100644
--- a/slices/view/src/main/java/androidx/slice/widget/RowView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/RowView.java
@@ -21,7 +21,6 @@
import static android.app.slice.Slice.HINT_PARTIAL;
import static android.app.slice.Slice.HINT_SHORTCUT;
import static android.app.slice.Slice.SUBTYPE_MAX;
-import static android.app.slice.Slice.SUBTYPE_TOGGLE;
import static android.app.slice.Slice.SUBTYPE_VALUE;
import static android.app.slice.SliceItem.FORMAT_ACTION;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
@@ -38,7 +37,6 @@
import static androidx.slice.widget.EventInfo.ROW_TYPE_TOGGLE;
import static androidx.slice.widget.SliceView.MODE_SMALL;
-import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.content.Context;
import android.content.Intent;
@@ -47,6 +45,7 @@
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.StyleSpan;
+import android.util.ArrayMap;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
@@ -63,13 +62,11 @@
import androidx.annotation.RestrictTo;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.graphics.drawable.IconCompat;
-import androidx.slice.Slice;
import androidx.slice.SliceItem;
import androidx.slice.core.SliceActionImpl;
import androidx.slice.core.SliceQuery;
import androidx.slice.view.R;
-import java.util.ArrayList;
import java.util.List;
/**
@@ -93,7 +90,7 @@
private TextView mSecondaryText;
private TextView mLastUpdatedText;
private View mDivider;
- private ArrayList<SliceActionView> mToggles = new ArrayList<>();
+ private ArrayMap<SliceActionImpl, SliceActionView> mToggles = new ArrayMap<>();
private LinearLayout mEndContainer;
private ProgressBar mRangeBar;
private View mSeeMoreView;
@@ -218,17 +215,12 @@
}
}
- @Override
- public void setSlice(Slice slice) {
- // Nothing to do
- }
-
/**
* This is called when RowView is being used as a component in a large template.
*/
@Override
public void setSliceItem(SliceItem slice, boolean isHeader, int index,
- SliceView.OnSliceActionListener observer) {
+ int rowCount, SliceView.OnSliceActionListener observer) {
setSliceActionListener(observer);
mRowIndex = index;
mIsHeader = ListContent.isValidHeader(slice);
@@ -247,9 +239,9 @@
if (contentDescr != null) {
mContent.setContentDescription(contentDescr);
}
- boolean showStart = false;
final SliceItem startItem = mRowContent.getStartItem();
- if (startItem != null) {
+ boolean showStart = startItem != null && mRowIndex > 0;
+ if (showStart) {
showStart = addItem(startItem, mTintColor, true /* isStart */);
}
mStartContainer.setVisibility(showStart ? View.VISIBLE : View.GONE);
@@ -270,11 +262,12 @@
addSubtitle(subtitleItem);
SliceItem primaryAction = mRowContent.getPrimaryAction();
- if (primaryAction != null) {
+ if (primaryAction != null && primaryAction != startItem) {
mRowAction = new SliceActionImpl(primaryAction);
if (mRowAction.isToggle()) {
// If primary action is a toggle, add it and we're done
addAction(mRowAction, mTintColor, mEndContainer, false /* isStart */);
+ // TODO: if start item is tappable, touch feedback should exclude it
setViewClickable(mRootView, true);
return;
}
@@ -291,42 +284,21 @@
// If we're here we can can show end items; check for top level actions first
List<SliceItem> endItems = mRowContent.getEndItems();
- String desiredFormat = FORMAT_ACTION;
if (mHeaderActions != null && mHeaderActions.size() > 0) {
// Use these if we have them instead
endItems = mHeaderActions;
- } else if (!endItems.isEmpty()) {
- // Prefer to show actions as end items if possible; fall back to the first format type.
- SliceItem firstEndItem = endItems.get(0);
- desiredFormat = mRowContent.endItemsContainAction()
- ? FORMAT_ACTION : firstEndItem.getSlice().getItems().get(0).getFormat();
}
- boolean hasRowAction = mRowAction != null;
- if (endItems.isEmpty()) {
- if (hasRowAction) setViewClickable(mRootView, true);
- return;
- }
-
// If we're here we might be able to show end items
int itemCount = 0;
boolean firstItemIsADefaultToggle = false;
+ boolean hasEndItemAction = false;
for (int i = 0; i < endItems.size(); i++) {
final SliceItem endItem = endItems.get(i);
- final String endFormat = endItem.hasHint(HINT_SHORTCUT)
- ? FORMAT_ACTION
- : endItem.getSlice().getItems().get(0).getFormat();
- // Only show one type of format at the end of the slice, use whatever is first
- if (itemCount < MAX_END_ITEMS
- && (desiredFormat.equals(endFormat)
- || FORMAT_TIMESTAMP.equals(endFormat))) {
-
- final EventInfo info = new EventInfo(getMode(),
- EventInfo.ACTION_TYPE_BUTTON,
- EventInfo.ROW_TYPE_LIST, mRowIndex);
- info.setPosition(EventInfo.POSITION_END, i,
- Math.min(endItems.size(), MAX_END_ITEMS));
-
+ if (itemCount < MAX_END_ITEMS) {
if (addItem(endItem, mTintColor, false /* isStart */)) {
+ if (SliceQuery.find(endItem, FORMAT_ACTION) != null) {
+ hasEndItemAction = true;
+ }
itemCount++;
if (itemCount == 1) {
firstItemIsADefaultToggle = !mToggles.isEmpty()
@@ -336,21 +308,22 @@
}
}
- boolean hasEndItemAction = FORMAT_ACTION.contentEquals(desiredFormat);
// If there is a row action and the first end item is a default toggle, show the divider.
- mDivider.setVisibility(hasRowAction && firstItemIsADefaultToggle
+ mDivider.setVisibility(mRowAction != null && firstItemIsADefaultToggle
? View.VISIBLE : View.GONE);
- if (hasRowAction) {
- if (itemCount > 0 && hasEndItemAction) {
- setViewClickable(mContent, true);
+ boolean hasStartAction = startItem != null
+ && SliceQuery.find(startItem, FORMAT_ACTION) != null;
+
+ if (mRowAction != null) {
+ // If there are outside actions make only the content bit clickable
+ // TODO: if start item is an image touch feedback should include it
+ setViewClickable((hasEndItemAction || hasStartAction) ? mContent : mRootView, true);
+ } else if (hasEndItemAction != hasStartAction && (itemCount == 1 || hasStartAction)) {
+ // Single action; make whole row clickable for it
+ if (!mToggles.isEmpty()) {
+ mRowAction = mToggles.keySet().iterator().next();
} else {
- setViewClickable(mRootView, true);
- }
- } else if (mRowContent.endItemsContainAction() && itemCount == 1) {
- // If the only end item is an action, make the whole row clickable.
- SliceItem unwrappedActionItem = endItems.get(0).getSlice().getItems().get(0);
- if (!SUBTYPE_TOGGLE.equals(unwrappedActionItem.getSubType())) {
- mRowAction = new SliceActionImpl(endItems.get(0));
+ mRowAction = new SliceActionImpl(hasEndItemAction ? endItems.get(0) : startItem);
}
setViewClickable(mRootView, true);
}
@@ -358,7 +331,7 @@
private void addSubtitle(final SliceItem subtitleItem) {
CharSequence subtitleTimeString = null;
- if (mShowLastUpdated) {
+ if (mShowLastUpdated && mLastUpdated != -1) {
subtitleTimeString = getResources().getString(R.string.abc_slice_updated,
SliceViewUtil.getRelativeTimeString(mLastUpdated));
}
@@ -371,6 +344,8 @@
? mHeaderSubtitleSize
: mSubtitleSize);
mSecondaryText.setTextColor(mSubtitleColor);
+ int verticalPadding = mIsHeader ? mVerticalHeaderTextPadding : mVerticalTextPadding;
+ mSecondaryText.setPadding(0, verticalPadding, 0, 0);
}
if (subtitleTimeString != null) {
if (!TextUtils.isEmpty(subtitle)) {
@@ -415,7 +390,7 @@
addView(progressBar);
mRangeBar = progressBar;
if (isSeekBar) {
- SliceItem thumb = SliceQuery.find(range, FORMAT_IMAGE);
+ SliceItem thumb = mRowContent.getInputRangeThumb();
SeekBar seekBar = (SeekBar) mRangeBar;
if (thumb != null) {
seekBar.setThumb(thumb.getIcon().loadDrawable(getContext()));
@@ -431,10 +406,9 @@
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
progress += finalMinValue;
try {
- PendingIntent pi = range.getAction();
- Intent i = new Intent().putExtra(EXTRA_RANGE_VALUE, progress);
// TODO: sending this PendingIntent should be rate limited.
- pi.send(getContext(), 0, i, null, null);
+ range.fireAction(getContext(),
+ new Intent().putExtra(EXTRA_RANGE_VALUE, progress));
} catch (CanceledException e) { }
}
@@ -463,9 +437,8 @@
info.setPosition(EventInfo.POSITION_START, 0, 1);
}
sav.setAction(actionContent, info, mObserver, color);
-
if (isToggle) {
- mToggles.add(sav);
+ mToggles.put(actionContent, sav);
}
}
@@ -478,7 +451,8 @@
int imageMode = 0;
SliceItem timeStamp = null;
ViewGroup container = isStart ? mStartContainer : mEndContainer;
- if (FORMAT_SLICE.equals(sliceItem.getFormat())) {
+ if (FORMAT_SLICE.equals(sliceItem.getFormat())
+ || FORMAT_ACTION.equals(sliceItem.getFormat())) {
if (sliceItem.hasHint(HINT_SHORTCUT)) {
addAction(new SliceActionImpl(sliceItem), color, container, isStart);
return true;
@@ -532,7 +506,7 @@
EventInfo.ROW_TYPE_LIST, mRowIndex);
mObserver.onSliceAction(info, mRowContent.getSlice());
}
- mRowContent.getSlice().getAction().send();
+ mRowContent.getSlice().fireAction(null, null);
} catch (CanceledException e) {
Log.e(TAG, "PendingIntent for slice cannot be sent", e);
}
@@ -547,21 +521,25 @@
@Override
public void onClick(View view) {
- if (mRowAction != null && mRowAction.getAction() != null && !mRowAction.isToggle()) {
- // Check for a row action
- try {
- mRowAction.getAction().send();
- if (mObserver != null) {
- EventInfo info = new EventInfo(getMode(), EventInfo.ACTION_TYPE_CONTENT,
- EventInfo.ROW_TYPE_LIST, mRowIndex);
- mObserver.onSliceAction(info, mRowAction.getSliceItem());
+ if (mRowAction != null && mRowAction.getActionItem() != null) {
+ // Check if it's a row click for a toggle, in this case need to update the UI
+ if (mRowAction.isToggle() && !(view instanceof SliceActionView)) {
+ SliceActionView sav = mToggles.get(mRowAction);
+ if (sav != null) {
+ sav.toggle();
}
- } catch (CanceledException e) {
- Log.e(TAG, "PendingIntent for slice cannot be sent", e);
+ } else {
+ try {
+ mRowAction.getActionItem().fireAction(null, null);
+ if (mObserver != null) {
+ EventInfo info = new EventInfo(getMode(), EventInfo.ACTION_TYPE_CONTENT,
+ EventInfo.ROW_TYPE_LIST, mRowIndex);
+ mObserver.onSliceAction(info, mRowAction.getSliceItem());
+ }
+ } catch (CanceledException e) {
+ Log.e(TAG, "PendingIntent for slice cannot be sent", e);
+ }
}
- } else if (mToggles.size() == 1) {
- // If there is only one toggle and no row action, just toggle it.
- mToggles.get(0).toggle();
}
}
@@ -575,16 +553,18 @@
@Override
public void resetView() {
- mRootView.setVisibility(View.VISIBLE);
+ mRootView.setVisibility(VISIBLE);
setViewClickable(mRootView, false);
setViewClickable(mContent, false);
mStartContainer.removeAllViews();
mEndContainer.removeAllViews();
mPrimaryText.setText(null);
mSecondaryText.setText(null);
+ mLastUpdatedText.setText(null);
+ mLastUpdatedText.setVisibility(GONE);
mToggles.clear();
mRowAction = null;
- mDivider.setVisibility(View.GONE);
+ mDivider.setVisibility(GONE);
if (mRangeBar != null) {
removeView(mRangeBar);
}
diff --git a/slices/view/src/main/java/androidx/slice/widget/ShortcutView.java b/slices/view/src/main/java/androidx/slice/widget/ShortcutView.java
index 6f02437..2f97c64 100644
--- a/slices/view/src/main/java/androidx/slice/widget/ShortcutView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/ShortcutView.java
@@ -26,7 +26,6 @@
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TEXT;
-import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.content.Context;
@@ -43,6 +42,7 @@
import android.widget.ImageView;
import androidx.annotation.RestrictTo;
+import androidx.core.graphics.drawable.DrawableCompat;
import androidx.slice.Slice;
import androidx.slice.SliceItem;
import androidx.slice.core.SliceQuery;
@@ -56,7 +56,7 @@
private static final String TAG = "ShortcutView";
- private Slice mSlice;
+ private ListContent mListContent;
private Uri mUri;
private SliceItem mActionItem;
private SliceItem mLabel;
@@ -72,21 +72,23 @@
mLargeIconSize = res.getDimensionPixelSize(R.dimen.abc_slice_shortcut_size);
}
- @SuppressLint("NewApi") // mIcon can only be non-null on API 23+
@Override
- public void setSlice(Slice slice) {
+ public void setSliceContent(ListContent sliceContent) {
resetView();
- mSlice = slice;
- determineShortcutItems(getContext(), slice);
- SliceItem colorItem = SliceQuery.findSubtype(slice, FORMAT_INT, SUBTYPE_COLOR);
+ mListContent = sliceContent;
+ if (mListContent == null) {
+ return;
+ }
+ determineShortcutItems(getContext());
+ SliceItem colorItem = mListContent.getColorItem();
if (colorItem == null) {
- colorItem = SliceQuery.findSubtype(slice, FORMAT_INT, SUBTYPE_COLOR);
+ colorItem = SliceQuery.findSubtype(sliceContent.getSlice(), FORMAT_INT, SUBTYPE_COLOR);
}
final int color = colorItem != null
? colorItem.getInt()
: SliceViewUtil.getColorAccent(getContext());
- ShapeDrawable circle = new ShapeDrawable(new OvalShape());
- circle.setTint(color);
+ Drawable circle = DrawableCompat.wrap(new ShapeDrawable(new OvalShape()));
+ DrawableCompat.setTint(circle, color);
ImageView iv = new ImageView(getContext());
if (mIcon != null && !mIcon.hasHint(HINT_NO_TINT)) {
// Only set the background if we're tintable
@@ -98,7 +100,7 @@
final int iconSize = isImage ? mLargeIconSize : mSmallIconSize;
SliceViewUtil.createCircledIcon(getContext(), iconSize, mIcon.getIcon(),
isImage, this /* parent */);
- mUri = slice.getUri();
+ mUri = sliceContent.getSlice().getUri();
setClickable(true);
} else {
setClickable(false);
@@ -112,10 +114,13 @@
@Override
public boolean performClick() {
+ if (mListContent == null) {
+ return false;
+ }
if (!callOnClick()) {
try {
if (mActionItem != null) {
- mActionItem.getAction().send();
+ mActionItem.fireAction(null, null);
} else {
Intent intent = new Intent(Intent.ACTION_VIEW).setData(mUri);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -127,8 +132,8 @@
EventInfo.ROW_TYPE_SHORTCUT, 0 /* rowIndex */);
SliceItem interactedItem = mActionItem != null
? mActionItem
- : new SliceItem(mSlice, FORMAT_SLICE, null /* subtype */,
- mSlice.getHints());
+ : new SliceItem(mListContent.getSlice(), FORMAT_SLICE,
+ null /* subtype */, mListContent.getSlice().getHints());
mObserver.onSliceAction(ei, interactedItem);
}
} catch (CanceledException e) {
@@ -141,9 +146,12 @@
/**
* Looks at the slice and determines which items are best to use to compose the shortcut.
*/
- private void determineShortcutItems(Context context, Slice slice) {
- ListContent lc = new ListContent(context, slice);
- SliceItem primaryAction = lc.getPrimaryAction();
+ private void determineShortcutItems(Context context) {
+ if (mListContent == null) {
+ return;
+ }
+ SliceItem primaryAction = mListContent.getPrimaryAction();
+ Slice slice = mListContent.getSlice();
if (primaryAction != null) {
// Preferred case: slice has a primary action
@@ -204,7 +212,7 @@
@Override
public void resetView() {
- mSlice = null;
+ mListContent = null;
mUri = null;
mActionItem = null;
mLabel = null;
diff --git a/slices/view/src/main/java/androidx/slice/widget/SliceActionView.java b/slices/view/src/main/java/androidx/slice/widget/SliceActionView.java
index 54da214..ffe4587 100644
--- a/slices/view/src/main/java/androidx/slice/widget/SliceActionView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/SliceActionView.java
@@ -168,14 +168,14 @@
// Update the intent extra state
boolean isChecked = ((Checkable) mActionView).isChecked();
Intent i = new Intent().putExtra(EXTRA_TOGGLE_STATE, isChecked);
- pi.send(getContext(), 0, i, null, null);
+ mSliceAction.getActionItem().fireAction(getContext(), i);
// Update event info state
if (mEventInfo != null) {
mEventInfo.state = isChecked ? EventInfo.STATE_ON : EventInfo.STATE_OFF;
}
} else {
- pi.send();
+ mSliceAction.getActionItem().fireAction(null, null);
}
if (mObserver != null && mEventInfo != null) {
mObserver.onSliceAction(mEventInfo, mSliceAction.getSliceItem());
diff --git a/slices/view/src/main/java/androidx/slice/widget/SliceChildView.java b/slices/view/src/main/java/androidx/slice/widget/SliceChildView.java
index 2400e73..fcd71bb 100644
--- a/slices/view/src/main/java/androidx/slice/widget/SliceChildView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/SliceChildView.java
@@ -19,13 +19,11 @@
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
-import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
-import androidx.slice.Slice;
import androidx.slice.SliceItem;
import androidx.slice.view.R;
@@ -45,10 +43,15 @@
protected int mSubtitleColor;
protected int mHeaderTitleSize;
protected int mHeaderSubtitleSize;
+ protected int mVerticalHeaderTextPadding;
protected int mTitleSize;
protected int mSubtitleSize;
+ protected int mVerticalTextPadding;
protected int mGridTitleSize;
protected int mGridSubtitleSize;
+ protected int mVerticalGridTextPadding;
+ protected int mGridTopPadding;
+ protected int mGridBottomPadding;
protected boolean mShowLastUpdated;
protected long mLastUpdated = -1;
@@ -60,6 +63,46 @@
this(context);
}
+ /**
+ * Called when the view should be reset.
+ */
+ public abstract void resetView();
+
+ /**
+ * Sets the content to display in this slice.
+ */
+ public void setSliceContent(ListContent content) {
+ // Do nothing
+ }
+
+ /**
+ * Called when the slice being displayed in this view is an element of a larger list.
+ */
+ public void setSliceItem(SliceItem slice, boolean isHeader, int rowIndex,
+ int rowCount, SliceView.OnSliceActionListener observer) {
+ // Do nothing
+ }
+
+ /**
+ * Sets the slice actions for this view.
+ */
+ public void setSliceActions(List<SliceItem> actions) {
+ // Do nothing
+ }
+
+ /**
+ * @return the height of the view when displayed in {@link SliceView#MODE_SMALL}.
+ */
+ public int getSmallHeight() {
+ return 0;
+ }
+
+ /**
+ * @return the height of the view when displayed in {@link SliceView#MODE_LARGE}.
+ */
+ public int getActualHeight() {
+ return 0;
+ }
/**
* Set the mode of the slice being presented.
@@ -77,37 +120,6 @@
}
/**
- * @return the height of this view when displayed in {@link SliceView#MODE_SMALL}.
- */
- public int getSmallHeight() {
- return 0;
- }
-
- /**
- * @return the height of this view if it displayed all of its contents.
- */
- public int getActualHeight() {
- return 0;
- }
-
- /**
- * @param slice the slice to show in this view.
- */
- public abstract void setSlice(Slice slice);
-
- /**
- * Called when the view should be reset.
- */
- public abstract void resetView();
-
- /**
- * @return the view.
- */
- public View getView() {
- return this;
- }
-
- /**
* Sets a custom color to use for tinting elements like icons for this view.
*/
public void setTint(@ColorInt int tintColor) {
@@ -146,33 +158,31 @@
mTintColor = themeColor != -1 ? themeColor : mTintColor;
mTitleColor = a.getColor(R.styleable.SliceView_titleColor, 0);
mSubtitleColor = a.getColor(R.styleable.SliceView_subtitleColor, 0);
+
mHeaderTitleSize = (int) a.getDimension(
R.styleable.SliceView_headerTitleSize, 0);
mHeaderSubtitleSize = (int) a.getDimension(
R.styleable.SliceView_headerSubtitleSize, 0);
+ mVerticalHeaderTextPadding = (int) a.getDimension(
+ R.styleable.SliceView_headerTextVerticalPadding, 0);
+
mTitleSize = (int) a.getDimension(R.styleable.SliceView_titleSize, 0);
mSubtitleSize = (int) a.getDimension(
R.styleable.SliceView_subtitleSize, 0);
+ mVerticalTextPadding = (int) a.getDimension(
+ R.styleable.SliceView_textVerticalPadding, 0);
+
mGridTitleSize = (int) a.getDimension(R.styleable.SliceView_gridTitleSize, 0);
mGridSubtitleSize = (int) a.getDimension(
R.styleable.SliceView_gridSubtitleSize, 0);
+ int defaultVerticalGridPadding = getContext().getResources().getDimensionPixelSize(
+ R.dimen.abc_slice_grid_text_inner_padding);
+ mVerticalGridTextPadding = (int) a.getDimension(
+ R.styleable.SliceView_gridTextVerticalPadding, defaultVerticalGridPadding);
+ mGridTopPadding = (int) a.getDimension(R.styleable.SliceView_gridTopPadding, 0);
+ mGridBottomPadding = (int) a.getDimension(R.styleable.SliceView_gridTopPadding, 0);
} finally {
a.recycle();
}
}
-
- /**
- * Called when the slice being displayed in this view is an element of a larger list.
- */
- public void setSliceItem(SliceItem slice, boolean isHeader, int rowIndex,
- SliceView.OnSliceActionListener observer) {
- // Do nothing
- }
-
- /**
- * Sets the slice actions for this view.
- */
- public void setSliceActions(List<SliceItem> actions) {
- // Do nothing
- }
}
diff --git a/slices/view/src/main/java/androidx/slice/widget/SliceLiveData.java b/slices/view/src/main/java/androidx/slice/widget/SliceLiveData.java
index 2ffc02c..8639893 100644
--- a/slices/view/src/main/java/androidx/slice/widget/SliceLiveData.java
+++ b/slices/view/src/main/java/androidx/slice/widget/SliceLiveData.java
@@ -63,8 +63,7 @@
/**
* Produces an {@link LiveData} that tracks a Slice for a given Uri. To use
- * this method your app must have the permission to the slice Uri or hold
- * {@link android.Manifest.permission#BIND_SLICE}).
+ * this method your app must have the permission to the slice Uri.
*/
public static LiveData<Slice> fromUri(Context context, Uri uri) {
return new SliceLiveDataImpl(context.getApplicationContext(), uri);
@@ -72,8 +71,7 @@
/**
* Produces an {@link LiveData} that tracks a Slice for a given Intent. To use
- * this method your app must have the permission to the slice Uri or hold
- * {@link android.Manifest.permission#BIND_SLICE}).
+ * this method your app must have the permission to the slice Uri.
*/
public static LiveData<Slice> fromIntent(@NonNull Context context, @NonNull Intent intent) {
return new SliceLiveDataImpl(context.getApplicationContext(), intent);
diff --git a/slices/view/src/main/java/androidx/slice/widget/SliceView.java b/slices/view/src/main/java/androidx/slice/widget/SliceView.java
index 855bded..7ba9eeb 100644
--- a/slices/view/src/main/java/androidx/slice/widget/SliceView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/SliceView.java
@@ -32,6 +32,7 @@
import android.view.ViewConfiguration;
import android.view.ViewGroup;
+import androidx.annotation.ColorInt;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -312,7 +313,7 @@
private int getHeightForMode() {
int mode = getMode();
if (mode == MODE_SHORTCUT) {
- return mShortcutSize;
+ return mListContent != null && mListContent.isValid() ? mShortcutSize : 0;
}
return mode == MODE_LARGE
? mCurrentView.getActualHeight()
@@ -375,7 +376,7 @@
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
- View v = mCurrentView.getView();
+ View v = mCurrentView;
final int left = getPaddingLeft();
final int top = getPaddingTop();
v.layout(left, top, left + v.getMeasuredWidth(), top + v.getMeasuredHeight());
@@ -484,15 +485,23 @@
}
/**
+ * @deprecated TO BE REMOVED; use {@link #setAccentColor(int)} instead.
+ */
+ @Deprecated
+ public void setTint(int tintColor) {
+ setAccentColor(tintColor);
+ }
+
+ /**
* Contents of a slice such as icons, text, and controls (e.g. toggle) can be tinted. Normally
* a color for tinting will be provided by the slice. Using this method will override
- * this color information and instead tint elements with the provided color.
+ * the slice-provided color information and instead tint elements with the color set here.
*
- * @param tintColor the color to use for tinting contents of this view.
+ * @param accentColor the color to use for tinting contents of this view.
*/
- public void setTint(int tintColor) {
- mThemeTintColor = tintColor;
- mCurrentView.setTint(tintColor);
+ public void setAccentColor(@ColorInt int accentColor) {
+ mThemeTintColor = accentColor;
+ mCurrentView.setTint(accentColor);
}
/**
@@ -540,11 +549,14 @@
private void reinflate() {
if (mCurrentSlice == null) {
mCurrentView.resetView();
+ updateActions();
return;
}
- mListContent = new ListContent(getContext(), mCurrentSlice);
+ mListContent = new ListContent(getContext(), mCurrentSlice, mAttrs, mDefStyleAttr,
+ mDefStyleRes);
if (!mListContent.isValid()) {
mCurrentView.resetView();
+ updateActions();
return;
}
@@ -579,7 +591,7 @@
mCurrentView.setShowLastUpdated(mShowLastUpdated && expired);
// Set the slice
- mCurrentView.setSlice(mCurrentSlice);
+ mCurrentView.setSliceContent(mListContent);
updateActions();
}
diff --git a/slices/view/src/main/res-public/values/public_attrs.xml b/slices/view/src/main/res-public/values/public_attrs.xml
index ad909ea..a535a6d 100644
--- a/slices/view/src/main/res-public/values/public_attrs.xml
+++ b/slices/view/src/main/res-public/values/public_attrs.xml
@@ -22,9 +22,14 @@
<public type="attr" name="tintColor" />
<public type="attr" name="headerTitleSize" />
<public type="attr" name="headerSubtitleSize" />
+ <public type="attr" name="headerTextVerticalPadding" />
<public type="attr" name="titleSize" />
<public type="attr" name="subtitleSize" />
+ <public type="attr" name="textVerticalPadding" />
<public type="attr" name="gridTitleSize" />
<public type="attr" name="gridSubtitleSize" />
+ <public type="attr" name="gridTextVerticalPadding" />
+ <public type="attr" name="gridTopPadding" />
+ <public type="attr" name="gridBottomPadding" />
<public type="attr" name="sliceViewStyle" />
-</resources>
\ No newline at end of file
+</resources>
diff --git a/slices/view/src/main/res/layout/abc_slice_grid_see_more.xml b/slices/view/src/main/res/layout/abc_slice_grid_see_more.xml
index 17c1c0f..0a2e746 100644
--- a/slices/view/src/main/res/layout/abc_slice_grid_see_more.xml
+++ b/slices/view/src/main/res/layout/abc_slice_grid_see_more.xml
@@ -33,5 +33,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
+ android:paddingTop="@dimen/abc_slice_grid_text_padding"
android:text="@string/abc_slice_more"/>
</LinearLayout>
\ No newline at end of file
diff --git a/slices/view/src/main/res/layout/abc_slice_secondary_text.xml b/slices/view/src/main/res/layout/abc_slice_secondary_text.xml
index 0870465..c6ff594 100644
--- a/slices/view/src/main/res/layout/abc_slice_secondary_text.xml
+++ b/slices/view/src/main/res/layout/abc_slice_secondary_text.xml
@@ -15,14 +15,11 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<!-- LinearLayout -->
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
- android:textAppearance="?android:attr/textAppearanceSmall"
- android:textColor="?android:attr/textColorSecondary"
android:gravity="center"
android:layout_height="wrap_content"
- android:padding="4dp"
android:layout_width="match_parent"
android:maxLines="1"
+ android:singleLine="true"
android:ellipsize="end"/>
diff --git a/slices/view/src/main/res/layout/abc_slice_title.xml b/slices/view/src/main/res/layout/abc_slice_title.xml
index 70a1400..eac5e8c 100644
--- a/slices/view/src/main/res/layout/abc_slice_title.xml
+++ b/slices/view/src/main/res/layout/abc_slice_title.xml
@@ -18,11 +18,9 @@
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:textColor="?android:attr/textColorPrimary"
android:gravity="center"
android:layout_height="wrap_content"
- android:padding="4dp"
android:layout_width="match_parent"
android:maxLines="1"
+ android:singleLine="true"
android:ellipsize="end"/>
diff --git a/slices/view/src/main/res/values-af/strings.xml b/slices/view/src/main/res/values-af/strings.xml
index ea5ab25..cfe8138 100644
--- a/slices/view/src/main/res/values-af/strings.xml
+++ b/slices/view/src/main/res/values-af/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Meer"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Wys meer"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Opgedateer om <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> min. gelede</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> min. gelede</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> jaar gelede</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> jaar gelede</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> dae gelede</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> dag gelede</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Kon nie koppel nie"</string>
</resources>
diff --git a/slices/view/src/main/res/values-am/strings.xml b/slices/view/src/main/res/values-am/strings.xml
index ea5ab25..da542e1 100644
--- a/slices/view/src/main/res/values-am/strings.xml
+++ b/slices/view/src/main/res/values-am/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"ተጨማሪ"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"ተጨማሪ አሳይ"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"የተዘመነው <xliff:g id="TIME">%1$s</xliff:g> ላይ"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> ደቂቃ በፊት</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> ደቂቃ በፊት</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> ዓመት በፊት</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> ዓመት በፊት</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> ቀናት በፊት</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> ቀናት በፊት</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"መገናኘት አልተቻለም"</string>
</resources>
diff --git a/slices/view/src/main/res/values-ar/strings.xml b/slices/view/src/main/res/values-ar/strings.xml
index 21aa8a4..1e9ca94 100644
--- a/slices/view/src/main/res/values-ar/strings.xml
+++ b/slices/view/src/main/res/values-ar/strings.xml
@@ -18,4 +18,32 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"بالإضافة إلى <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"المزيد"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"عرض المزيد"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"وقت التحديث الأخير: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="zero">قبل <xliff:g id="ID_2">%d</xliff:g> دقيقة</item>
+ <item quantity="two">قبل دقيقتين (<xliff:g id="ID_2">%d</xliff:g>)</item>
+ <item quantity="few">قبل <xliff:g id="ID_2">%d</xliff:g> دقائق</item>
+ <item quantity="many">قبل <xliff:g id="ID_2">%d</xliff:g> دقيقة</item>
+ <item quantity="other">قبل <xliff:g id="ID_2">%d</xliff:g> دقيقة</item>
+ <item quantity="one">قبل <xliff:g id="ID_1">%d</xliff:g> دقيقة</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="zero">قبل <xliff:g id="ID_2">%d</xliff:g> سنة</item>
+ <item quantity="two">قبل سنتين (<xliff:g id="ID_2">%d</xliff:g>)</item>
+ <item quantity="few">قبل <xliff:g id="ID_2">%d</xliff:g> سنوات</item>
+ <item quantity="many">قبل <xliff:g id="ID_2">%d</xliff:g> سنة</item>
+ <item quantity="other">قبل <xliff:g id="ID_2">%d</xliff:g> سنة</item>
+ <item quantity="one">قبل <xliff:g id="ID_1">%d</xliff:g> سنة</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="zero">قبل <xliff:g id="ID_2">%d</xliff:g> يوم</item>
+ <item quantity="two">قبل يومين (<xliff:g id="ID_2">%d</xliff:g>)</item>
+ <item quantity="few">قبل <xliff:g id="ID_2">%d</xliff:g> أيام</item>
+ <item quantity="many">قبل <xliff:g id="ID_2">%d</xliff:g> يومًا</item>
+ <item quantity="other">قبل <xliff:g id="ID_2">%d</xliff:g> يوم</item>
+ <item quantity="one">قبل <xliff:g id="ID_1">%d</xliff:g> يوم</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"تعذّر الاتصال."</string>
</resources>
diff --git a/slices/view/src/main/res/values-as/strings.xml b/slices/view/src/main/res/values-as/strings.xml
new file mode 100644
index 0000000..77f9af6
--- /dev/null
+++ b/slices/view/src/main/res/values-as/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"অধিক"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"অধিক দেখুৱাওক"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> আপডেট কৰা হৈছিল"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"সংযোগ কৰিব পৰা নগ\'ল"</string>
+</resources>
diff --git a/slices/view/src/main/res/values-az/strings.xml b/slices/view/src/main/res/values-az/strings.xml
index ea5ab25..837f08c 100644
--- a/slices/view/src/main/res/values-az/strings.xml
+++ b/slices/view/src/main/res/values-az/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Digər"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Digərinə baxın"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> tarixində yenilənib"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> dəq əvvəl</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> dəq əvvəl</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> il əvvəl</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> il əvvəl</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> gün əvvəl</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> gün əvvəl</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Qoşulmaq mümkün olmadı"</string>
</resources>
diff --git a/slices/view/src/main/res/values-b+sr+Latn/strings.xml b/slices/view/src/main/res/values-b+sr+Latn/strings.xml
index ffd9b9b..8deb17c 100644
--- a/slices/view/src/main/res/values-b+sr+Latn/strings.xml
+++ b/slices/view/src/main/res/values-b+sr+Latn/strings.xml
@@ -18,4 +18,23 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"i još <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Još"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Prikaži više"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Ažurirano <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="one">pre <xliff:g id="ID_2">%d</xliff:g> min</item>
+ <item quantity="few">pre <xliff:g id="ID_2">%d</xliff:g> min</item>
+ <item quantity="other">pre <xliff:g id="ID_2">%d</xliff:g> min</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="one">pre <xliff:g id="ID_2">%d</xliff:g> god</item>
+ <item quantity="few">pre <xliff:g id="ID_2">%d</xliff:g> god</item>
+ <item quantity="other">pre <xliff:g id="ID_2">%d</xliff:g> god</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="one">pre <xliff:g id="ID_2">%d</xliff:g> dan</item>
+ <item quantity="few">pre <xliff:g id="ID_2">%d</xliff:g> dana</item>
+ <item quantity="other">pre <xliff:g id="ID_2">%d</xliff:g> dana</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Povezivanje nije uspelo"</string>
</resources>
diff --git a/slices/view/src/main/res/values-be/strings.xml b/slices/view/src/main/res/values-be/strings.xml
index df8e965..f1ae045 100644
--- a/slices/view/src/main/res/values-be/strings.xml
+++ b/slices/view/src/main/res/values-be/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"Яшчэ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Яшчэ"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Яшчэ"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Абноўлена <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"Не атрымалася падключыцца"</string>
</resources>
diff --git a/slices/view/src/main/res/values-bg/strings.xml b/slices/view/src/main/res/values-bg/strings.xml
index ea5ab25..b019a46 100644
--- a/slices/view/src/main/res/values-bg/strings.xml
+++ b/slices/view/src/main/res/values-bg/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Още"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Показване на още"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Актуализирано <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="other">Преди <xliff:g id="ID_2">%d</xliff:g> мин</item>
+ <item quantity="one">Преди <xliff:g id="ID_1">%d</xliff:g> мин</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="other">Преди <xliff:g id="ID_2">%d</xliff:g> год</item>
+ <item quantity="one">Преди <xliff:g id="ID_1">%d</xliff:g> год</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="other">Преди <xliff:g id="ID_2">%d</xliff:g> дни</item>
+ <item quantity="one">Преди <xliff:g id="ID_1">%d</xliff:g> ден</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Не можа да се установи връзка"</string>
</resources>
diff --git a/slices/view/src/main/res/values-bn/strings.xml b/slices/view/src/main/res/values-bn/strings.xml
index 8737d8c..3b8acdf 100644
--- a/slices/view/src/main/res/values-bn/strings.xml
+++ b/slices/view/src/main/res/values-bn/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>টি"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"আরও"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"আরও দেখুন"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> আপডেট করা হয়েছে"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"কানেক্ট করা যায়নি"</string>
</resources>
diff --git a/slices/view/src/main/res/values-bs/strings.xml b/slices/view/src/main/res/values-bs/strings.xml
index ea5ab25..cb84c18 100644
--- a/slices/view/src/main/res/values-bs/strings.xml
+++ b/slices/view/src/main/res/values-bs/strings.xml
@@ -18,4 +18,23 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Više"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Prikaži više"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Ažurirano <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="one">Prije <xliff:g id="ID_2">%d</xliff:g> min.</item>
+ <item quantity="few">Prije <xliff:g id="ID_2">%d</xliff:g> min.</item>
+ <item quantity="other">Prije <xliff:g id="ID_2">%d</xliff:g> min.</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="one">Prije <xliff:g id="ID_2">%d</xliff:g> god.</item>
+ <item quantity="few">Prije <xliff:g id="ID_2">%d</xliff:g> god.</item>
+ <item quantity="other">Prije <xliff:g id="ID_2">%d</xliff:g> god.</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="one">Prije <xliff:g id="ID_2">%d</xliff:g> dan</item>
+ <item quantity="few">Prije <xliff:g id="ID_2">%d</xliff:g> dana</item>
+ <item quantity="other">Prije <xliff:g id="ID_2">%d</xliff:g> dana</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Povezivanje nije uspjelo"</string>
</resources>
diff --git a/slices/view/src/main/res/values-ca/strings.xml b/slices/view/src/main/res/values-ca/strings.xml
index 8ae6f78..52e1988 100644
--- a/slices/view/src/main/res/values-ca/strings.xml
+++ b/slices/view/src/main/res/values-ca/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"<xliff:g id="NUMBER">%1$d</xliff:g> més"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Més"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Mostra\'n més"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"S\'ha actualitzat <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"No s\'ha pogut connectar"</string>
</resources>
diff --git a/slices/view/src/main/res/values-cs/strings.xml b/slices/view/src/main/res/values-cs/strings.xml
index 1d39c0f..c4c1fce 100644
--- a/slices/view/src/main/res/values-cs/strings.xml
+++ b/slices/view/src/main/res/values-cs/strings.xml
@@ -18,4 +18,26 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"a ještě <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Více"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Zobrazit více"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Aktualizováno <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="few">před <xliff:g id="ID_2">%d</xliff:g> min</item>
+ <item quantity="many">před <xliff:g id="ID_2">%d</xliff:g> min</item>
+ <item quantity="other">před <xliff:g id="ID_2">%d</xliff:g> min</item>
+ <item quantity="one">před <xliff:g id="ID_1">%d</xliff:g> min</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="few">před <xliff:g id="ID_2">%d</xliff:g> lety</item>
+ <item quantity="many">před <xliff:g id="ID_2">%d</xliff:g> roku</item>
+ <item quantity="other">před <xliff:g id="ID_2">%d</xliff:g> lety</item>
+ <item quantity="one">před <xliff:g id="ID_1">%d</xliff:g> rokem</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="few">před <xliff:g id="ID_2">%d</xliff:g> dny</item>
+ <item quantity="many">před <xliff:g id="ID_2">%d</xliff:g> dne</item>
+ <item quantity="other">před <xliff:g id="ID_2">%d</xliff:g> dny</item>
+ <item quantity="one">před <xliff:g id="ID_1">%d</xliff:g> dnem</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Nelze se připojit"</string>
</resources>
diff --git a/slices/view/src/main/res/values-da/strings.xml b/slices/view/src/main/res/values-da/strings.xml
index 40de41d..33d9c9d 100644
--- a/slices/view/src/main/res/values-da/strings.xml
+++ b/slices/view/src/main/res/values-da/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"<xliff:g id="NUMBER">%1$d</xliff:g> mere"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Mere"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Se mere"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Opdateret <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"Der kunne ikke oprettes forbindelse"</string>
</resources>
diff --git a/slices/view/src/main/res/values-de/strings.xml b/slices/view/src/main/res/values-de/strings.xml
index 9d9bede..96da7eb 100644
--- a/slices/view/src/main/res/values-de/strings.xml
+++ b/slices/view/src/main/res/values-de/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Mehr"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Mehr anzeigen"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Aktualisiert: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"Verbindung nicht möglich"</string>
</resources>
diff --git a/slices/view/src/main/res/values-el/strings.xml b/slices/view/src/main/res/values-el/strings.xml
index ea5ab25..31092af 100644
--- a/slices/view/src/main/res/values-el/strings.xml
+++ b/slices/view/src/main/res/values-el/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Περισσότ."</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Εμφάνιση περισσότερων"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Ενημερώθηκε <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> λεπ. πριν</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> λεπ. πριν</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> χρ. πριν</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> χρ. πριν</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> ημ. πριν</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> ημ. πριν</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Αδυναμία σύνδεσης"</string>
</resources>
diff --git a/slices/view/src/main/res/values-en-rAU/strings.xml b/slices/view/src/main/res/values-en-rAU/strings.xml
index ea5ab25..8fa5b45 100644
--- a/slices/view/src/main/res/values-en-rAU/strings.xml
+++ b/slices/view/src/main/res/values-en-rAU/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"More"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Show more"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Updated <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> min ago</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> min ago</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> yr ago</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> yr ago</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> days ago</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> day ago</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Couldn\'t connect"</string>
</resources>
diff --git a/slices/view/src/main/res/values-en-rCA/strings.xml b/slices/view/src/main/res/values-en-rCA/strings.xml
index ea5ab25..8fa5b45 100644
--- a/slices/view/src/main/res/values-en-rCA/strings.xml
+++ b/slices/view/src/main/res/values-en-rCA/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"More"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Show more"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Updated <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> min ago</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> min ago</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> yr ago</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> yr ago</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> days ago</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> day ago</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Couldn\'t connect"</string>
</resources>
diff --git a/slices/view/src/main/res/values-en-rGB/strings.xml b/slices/view/src/main/res/values-en-rGB/strings.xml
index ea5ab25..8fa5b45 100644
--- a/slices/view/src/main/res/values-en-rGB/strings.xml
+++ b/slices/view/src/main/res/values-en-rGB/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"More"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Show more"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Updated <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> min ago</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> min ago</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> yr ago</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> yr ago</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> days ago</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> day ago</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Couldn\'t connect"</string>
</resources>
diff --git a/slices/view/src/main/res/values-en-rIN/strings.xml b/slices/view/src/main/res/values-en-rIN/strings.xml
index ea5ab25..8fa5b45 100644
--- a/slices/view/src/main/res/values-en-rIN/strings.xml
+++ b/slices/view/src/main/res/values-en-rIN/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"More"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Show more"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Updated <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> min ago</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> min ago</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> yr ago</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> yr ago</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> days ago</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> day ago</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Couldn\'t connect"</string>
</resources>
diff --git a/slices/view/src/main/res/values-en-rXC/strings.xml b/slices/view/src/main/res/values-en-rXC/strings.xml
index b793412..b24c1e6 100644
--- a/slices/view/src/main/res/values-en-rXC/strings.xml
+++ b/slices/view/src/main/res/values-en-rXC/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"More"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Show more"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Updated <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> min ago</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> min ago</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> yr ago</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> yr ago</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> days ago</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> day ago</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Couldn\'t connect"</string>
</resources>
diff --git a/slices/view/src/main/res/values-es-rUS/strings.xml b/slices/view/src/main/res/values-es-rUS/strings.xml
index a445505..124c40c 100644
--- a/slices/view/src/main/res/values-es-rUS/strings.xml
+++ b/slices/view/src/main/res/values-es-rUS/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"<xliff:g id="NUMBER">%1$d</xliff:g> más"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Más"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Mostrar más"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Última actualización: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"No se pudo establecer conexión"</string>
</resources>
diff --git a/slices/view/src/main/res/values-es/strings.xml b/slices/view/src/main/res/values-es/strings.xml
index dcdd33e..57685ba 100644
--- a/slices/view/src/main/res/values-es/strings.xml
+++ b/slices/view/src/main/res/values-es/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"<xliff:g id="NUMBER">%1$d</xliff:g> más"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Más"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Ver más"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Última actualización: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"No se ha podido establecer la conexión"</string>
</resources>
diff --git a/slices/view/src/main/res/values-et/strings.xml b/slices/view/src/main/res/values-et/strings.xml
index a1675ff..4bd8a92 100644
--- a/slices/view/src/main/res/values-et/strings.xml
+++ b/slices/view/src/main/res/values-et/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"ja veel <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Rohkem"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Kuva rohkem"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Värskendatud kell <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"Ühendamine ebaõnnestus"</string>
</resources>
diff --git a/slices/view/src/main/res/values-eu/strings.xml b/slices/view/src/main/res/values-eu/strings.xml
index 1c79d4a..e689ada 100644
--- a/slices/view/src/main/res/values-eu/strings.xml
+++ b/slices/view/src/main/res/values-eu/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"Beste <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Gehiago"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Erakutsi gehiago"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Azken eguneratzea: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"Ezin izan da konektatu"</string>
</resources>
diff --git a/slices/view/src/main/res/values-fa/strings.xml b/slices/view/src/main/res/values-fa/strings.xml
index 2b2abd2..cb94e5d 100644
--- a/slices/view/src/main/res/values-fa/strings.xml
+++ b/slices/view/src/main/res/values-fa/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"بیشتر"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"نمایش موارد بیشتر"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"زمان بهروزرسانی <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> دقیقه قبل</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> دقیقه قبل</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> سال قبل</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> سال قبل</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> روز قبل</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> روز قبل</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"متصل نشد"</string>
</resources>
diff --git a/slices/view/src/main/res/values-fi/strings.xml b/slices/view/src/main/res/values-fi/strings.xml
index ea5ab25..bc37611 100644
--- a/slices/view/src/main/res/values-fi/strings.xml
+++ b/slices/view/src/main/res/values-fi/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Lisää"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Näytä lisää"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Päivitetty <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"Ei yhteyttä"</string>
</resources>
diff --git a/slices/view/src/main/res/values-fr-rCA/strings.xml b/slices/view/src/main/res/values-fr-rCA/strings.xml
index ea5ab25..1563136 100644
--- a/slices/view/src/main/res/values-fr-rCA/strings.xml
+++ b/slices/view/src/main/res/values-fr-rCA/strings.xml
@@ -17,5 +17,12 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Plus"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Plus"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Mise à jour : <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"Impossible de se connecter"</string>
</resources>
diff --git a/slices/view/src/main/res/values-fr/strings.xml b/slices/view/src/main/res/values-fr/strings.xml
index 73685ce..6762544 100644
--- a/slices/view/src/main/res/values-fr/strings.xml
+++ b/slices/view/src/main/res/values-fr/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"<xliff:g id="NUMBER">%1$d</xliff:g> autres"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Plus"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Afficher plus"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Dernière mise à jour : <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"Impossible de se connecter"</string>
</resources>
diff --git a/slices/view/src/main/res/values-gl/strings.xml b/slices/view/src/main/res/values-gl/strings.xml
index 573eae9..70b31bb 100644
--- a/slices/view/src/main/res/values-gl/strings.xml
+++ b/slices/view/src/main/res/values-gl/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"<xliff:g id="NUMBER">%1$d</xliff:g> máis"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Máis"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Amosar máis"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Contido actualizado (<xliff:g id="TIME">%1$s</xliff:g>)"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="other">Hai <xliff:g id="ID_2">%d</xliff:g> min</item>
+ <item quantity="one">Hai <xliff:g id="ID_1">%d</xliff:g> min</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="other">Hai <xliff:g id="ID_2">%d</xliff:g> anos</item>
+ <item quantity="one">Hai <xliff:g id="ID_1">%d</xliff:g> ano</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="other">Hai <xliff:g id="ID_2">%d</xliff:g> días</item>
+ <item quantity="one">Hai <xliff:g id="ID_1">%d</xliff:g> día</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Non se puido establecer conexión"</string>
</resources>
diff --git a/slices/view/src/main/res/values-gu/strings.xml b/slices/view/src/main/res/values-gu/strings.xml
index ea5ab25..d6a7ee8 100644
--- a/slices/view/src/main/res/values-gu/strings.xml
+++ b/slices/view/src/main/res/values-gu/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"વધુ"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"વધુ બતાવો"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> અપડેટ થયું"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"કનેક્ટ કરી શકાયું નથી"</string>
</resources>
diff --git a/slices/view/src/main/res/values-hi/strings.xml b/slices/view/src/main/res/values-hi/strings.xml
index ea5ab25..380025e 100644
--- a/slices/view/src/main/res/values-hi/strings.xml
+++ b/slices/view/src/main/res/values-hi/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"ज़्यादा देखें"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"ज़्यादा देखें"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> बजे अपडेट किया गया"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"कनेक्ट नहीं हो पाया"</string>
</resources>
diff --git a/slices/view/src/main/res/values-hr/strings.xml b/slices/view/src/main/res/values-hr/strings.xml
index 7ecedf6..f44b06c 100644
--- a/slices/view/src/main/res/values-hr/strings.xml
+++ b/slices/view/src/main/res/values-hr/strings.xml
@@ -18,4 +18,23 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"još <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Više"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Prikaži više"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Ažurirano <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="one">Prije <xliff:g id="ID_2">%d</xliff:g> min</item>
+ <item quantity="few">Prije <xliff:g id="ID_2">%d</xliff:g> min</item>
+ <item quantity="other">Prije <xliff:g id="ID_2">%d</xliff:g> min</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="one">Prije <xliff:g id="ID_2">%d</xliff:g> godinu</item>
+ <item quantity="few">Prije <xliff:g id="ID_2">%d</xliff:g> godine</item>
+ <item quantity="other">Prije <xliff:g id="ID_2">%d</xliff:g> godina</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="one">Prije <xliff:g id="ID_2">%d</xliff:g> dan</item>
+ <item quantity="few">Prije <xliff:g id="ID_2">%d</xliff:g> dana</item>
+ <item quantity="other">Prije <xliff:g id="ID_2">%d</xliff:g> dana</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Povezivanje nije moguće"</string>
</resources>
diff --git a/slices/view/src/main/res/values-hu/strings.xml b/slices/view/src/main/res/values-hu/strings.xml
index ea5ab25..c56143b 100644
--- a/slices/view/src/main/res/values-hu/strings.xml
+++ b/slices/view/src/main/res/values-hu/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Több"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Több megjelenítése"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Frissítve: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"Nem sikerült kapcsolódni"</string>
</resources>
diff --git a/slices/view/src/main/res/values-hy/strings.xml b/slices/view/src/main/res/values-hy/strings.xml
index ea5ab25..bb6b812 100644
--- a/slices/view/src/main/res/values-hy/strings.xml
+++ b/slices/view/src/main/res/values-hy/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Ավելին"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Ցուցադրել ավելի շատ"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Թարմացվել է <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"Չհաջողվեց միանալ"</string>
</resources>
diff --git a/slices/view/src/main/res/values-in/strings.xml b/slices/view/src/main/res/values-in/strings.xml
index ea5ab25..5641ebb 100644
--- a/slices/view/src/main/res/values-in/strings.xml
+++ b/slices/view/src/main/res/values-in/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Lainnya"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Tampilkan lainnya"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Diupdate <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> menit lalu</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> menit lalu</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> tahun lalu</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> tahun lalu</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> hari lalu</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> hari lalu</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Tidak dapat terhubung"</string>
</resources>
diff --git a/slices/view/src/main/res/values-is/strings.xml b/slices/view/src/main/res/values-is/strings.xml
index ea5ab25..c059c03 100644
--- a/slices/view/src/main/res/values-is/strings.xml
+++ b/slices/view/src/main/res/values-is/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Meira"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Sýna meira"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Uppfært <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"Tenging mistókst"</string>
</resources>
diff --git a/slices/view/src/main/res/values-it/strings.xml b/slices/view/src/main/res/values-it/strings.xml
index ea5ab25..05410e8 100644
--- a/slices/view/src/main/res/values-it/strings.xml
+++ b/slices/view/src/main/res/values-it/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Altro"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Mostra altro"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Aggiornamento: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"Impossibile collegarsi"</string>
</resources>
diff --git a/slices/view/src/main/res/values-iw/strings.xml b/slices/view/src/main/res/values-iw/strings.xml
index ea5ab25..5c55fe2 100644
--- a/slices/view/src/main/res/values-iw/strings.xml
+++ b/slices/view/src/main/res/values-iw/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"עוד"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"הצג יותר"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"עודכן ב-<xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"לא ניתן היה להתחבר"</string>
</resources>
diff --git a/slices/view/src/main/res/values-ja/strings.xml b/slices/view/src/main/res/values-ja/strings.xml
index ed51803..286949f 100644
--- a/slices/view/src/main/res/values-ja/strings.xml
+++ b/slices/view/src/main/res/values-ja/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"他 <xliff:g id="NUMBER">%1$d</xliff:g> 件"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"もっと見る"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"もっと見る"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"更新時刻: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"接続できませんでした"</string>
</resources>
diff --git a/slices/view/src/main/res/values-ka/strings.xml b/slices/view/src/main/res/values-ka/strings.xml
index ea5ab25..542c6cc 100644
--- a/slices/view/src/main/res/values-ka/strings.xml
+++ b/slices/view/src/main/res/values-ka/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"მეტი"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"მეტის ჩვენება"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"განახლების დრო: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"დაკავშირება ვერ მოხერხდა"</string>
</resources>
diff --git a/slices/view/src/main/res/values-kk/strings.xml b/slices/view/src/main/res/values-kk/strings.xml
index ea5ab25..231a499 100644
--- a/slices/view/src/main/res/values-kk/strings.xml
+++ b/slices/view/src/main/res/values-kk/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Тағы"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Толығырақ көрсету"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Жаңартылған уақыты: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"Байланыс орнатылмады"</string>
</resources>
diff --git a/slices/view/src/main/res/values-km/strings.xml b/slices/view/src/main/res/values-km/strings.xml
index ea5ab25..1e78acb 100644
--- a/slices/view/src/main/res/values-km/strings.xml
+++ b/slices/view/src/main/res/values-km/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"ច្រើនទៀត"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"បង្ហាញច្រើនទៀត"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"បានធ្វើបច្ចុប្បន្នភាពកាលពី <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> នាទីមុន</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> នាទីមុន</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> ឆ្នាំមុន</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> ឆ្នាំមុន</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> ថ្ងៃមុន</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> ថ្ងៃមុន</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"មិនអាចភ្ជាប់បានទេ"</string>
</resources>
diff --git a/slices/view/src/main/res/values-kn/strings.xml b/slices/view/src/main/res/values-kn/strings.xml
index ea5ab25..ed7dcd6 100644
--- a/slices/view/src/main/res/values-kn/strings.xml
+++ b/slices/view/src/main/res/values-kn/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"ಇನ್ನಷ್ಟು"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"ಹೆಚ್ಚು ತೋರಿಸಿ"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> ಅನ್ನು ಅಪ್ಡೇಟ್ ಮಾಡಲಾಗಿದೆ"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> ನಿಮಿಷದ ಹಿಂದೆ</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> ನಿಮಿಷದ ಹಿಂದೆ</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> ವರ್ಷದ ಹಿಂದೆ</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> ವರ್ಷದ ಹಿಂದೆ</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> ದಿನಗಳ ಹಿಂದೆ</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> ದಿನಗಳ ಹಿಂದೆ</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"ಸಂಪರ್ಕಿಸಲು ಸಾಧ್ಯವಾಗುತ್ತಿಲ್ಲ"</string>
</resources>
diff --git a/slices/view/src/main/res/values-ko/strings.xml b/slices/view/src/main/res/values-ko/strings.xml
index 2dc0279..1ec58f0 100644
--- a/slices/view/src/main/res/values-ko/strings.xml
+++ b/slices/view/src/main/res/values-ko/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"<xliff:g id="NUMBER">%1$d</xliff:g>개 더보기"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"더보기"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"더보기"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g>에 업데이트됨"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"연결할 수 없음"</string>
</resources>
diff --git a/slices/view/src/main/res/values-ky/strings.xml b/slices/view/src/main/res/values-ky/strings.xml
index ea5ab25..06244ab 100644
--- a/slices/view/src/main/res/values-ky/strings.xml
+++ b/slices/view/src/main/res/values-ky/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Дагы"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Дагы көрсөтүү"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> жаңыртылды"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"Туташпай койду"</string>
</resources>
diff --git a/slices/view/src/main/res/values-lo/strings.xml b/slices/view/src/main/res/values-lo/strings.xml
index ea5ab25..44111ea 100644
--- a/slices/view/src/main/res/values-lo/strings.xml
+++ b/slices/view/src/main/res/values-lo/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"ເພີ່ມເຕີມ"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"ສະແດງເພີ່ມເຕີມ"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"ອັບເດດເມື່ອ <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"ບໍ່ສາມາດເຊື່ອມຕໍ່ໄດ້"</string>
</resources>
diff --git a/slices/view/src/main/res/values-lt/strings.xml b/slices/view/src/main/res/values-lt/strings.xml
index bb6bcb3..e4094b4 100644
--- a/slices/view/src/main/res/values-lt/strings.xml
+++ b/slices/view/src/main/res/values-lt/strings.xml
@@ -18,4 +18,26 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"Dar <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Daugiau"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Rodyti daugiau"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Atnaujinta <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="one">Prieš <xliff:g id="ID_2">%d</xliff:g> min.</item>
+ <item quantity="few">Prieš <xliff:g id="ID_2">%d</xliff:g> min.</item>
+ <item quantity="many">Prieš <xliff:g id="ID_2">%d</xliff:g> min.</item>
+ <item quantity="other">Prieš <xliff:g id="ID_2">%d</xliff:g> min.</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="one">Prieš <xliff:g id="ID_2">%d</xliff:g> m.</item>
+ <item quantity="few">Prieš <xliff:g id="ID_2">%d</xliff:g> m.</item>
+ <item quantity="many">Prieš <xliff:g id="ID_2">%d</xliff:g> m.</item>
+ <item quantity="other">Prieš <xliff:g id="ID_2">%d</xliff:g> m.</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="one">Prieš <xliff:g id="ID_2">%d</xliff:g> d.</item>
+ <item quantity="few">Prieš <xliff:g id="ID_2">%d</xliff:g> d.</item>
+ <item quantity="many">Prieš <xliff:g id="ID_2">%d</xliff:g> d.</item>
+ <item quantity="other">Prieš <xliff:g id="ID_2">%d</xliff:g> d.</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Prisijungti nepavyko"</string>
</resources>
diff --git a/slices/view/src/main/res/values-lv/strings.xml b/slices/view/src/main/res/values-lv/strings.xml
index 79ccb99..45be57c 100644
--- a/slices/view/src/main/res/values-lv/strings.xml
+++ b/slices/view/src/main/res/values-lv/strings.xml
@@ -18,4 +18,23 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"Vēl <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Vēl"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Rādīt vairāk"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Atjaunināts <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="zero">Pirms <xliff:g id="ID_2">%d</xliff:g> minūtēm</item>
+ <item quantity="one">Pirms <xliff:g id="ID_2">%d</xliff:g> minūtes</item>
+ <item quantity="other">Pirms <xliff:g id="ID_2">%d</xliff:g> minūtēm</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="zero">Pirms <xliff:g id="ID_2">%d</xliff:g> gadiem</item>
+ <item quantity="one">Pirms <xliff:g id="ID_2">%d</xliff:g> gada</item>
+ <item quantity="other">Pirms <xliff:g id="ID_2">%d</xliff:g> gadiem</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="zero">Pirms <xliff:g id="ID_2">%d</xliff:g> dienām</item>
+ <item quantity="one">Pirms <xliff:g id="ID_2">%d</xliff:g> dienas</item>
+ <item quantity="other">Pirms <xliff:g id="ID_2">%d</xliff:g> dienām</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Nevarēja izveidot savienojumu"</string>
</resources>
diff --git a/slices/view/src/main/res/values-mk/strings.xml b/slices/view/src/main/res/values-mk/strings.xml
index ea5ab25..ff88cd7 100644
--- a/slices/view/src/main/res/values-mk/strings.xml
+++ b/slices/view/src/main/res/values-mk/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Повеќе"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Прикажи повеќе"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Ажурирано <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="one">Пред <xliff:g id="ID_2">%d</xliff:g> мин.</item>
+ <item quantity="other">Пред <xliff:g id="ID_2">%d</xliff:g> мин.</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="one">Пред <xliff:g id="ID_2">%d</xliff:g> год.</item>
+ <item quantity="other">Пред <xliff:g id="ID_2">%d</xliff:g> год.</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="one">Пред <xliff:g id="ID_2">%d</xliff:g> ден</item>
+ <item quantity="other">Пред <xliff:g id="ID_2">%d</xliff:g> дена</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Не може да се поврзе"</string>
</resources>
diff --git a/slices/view/src/main/res/values-ml/strings.xml b/slices/view/src/main/res/values-ml/strings.xml
index ea5ab25..b67ae80 100644
--- a/slices/view/src/main/res/values-ml/strings.xml
+++ b/slices/view/src/main/res/values-ml/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"കൂടുതൽ"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"കൂടുതൽ കാണിക്കുക"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> അപ്ഡേറ്റ് ചെയ്തു"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> മിനിറ്റ് മുൻപ്</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> മിനിറ്റ് മുൻപ്</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> വർഷം മുൻപ്</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> വർഷം മുൻപ്</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> ദിവസം മുൻപ്</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> ദിവസം മുൻപ്</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"കണക്റ്റ് ചെയ്യാനായില്ല"</string>
</resources>
diff --git a/slices/view/src/main/res/values-mn/strings.xml b/slices/view/src/main/res/values-mn/strings.xml
index ea5ab25..beaa1a3 100644
--- a/slices/view/src/main/res/values-mn/strings.xml
+++ b/slices/view/src/main/res/values-mn/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Бусад"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Дэлгэрэнгүй үзэх"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> шинэчилсэн"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"Холбогдож чадсангүй"</string>
</resources>
diff --git a/slices/view/src/main/res/values-mr/strings.xml b/slices/view/src/main/res/values-mr/strings.xml
index ea5ab25..693c821 100644
--- a/slices/view/src/main/res/values-mr/strings.xml
+++ b/slices/view/src/main/res/values-mr/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"आणखी"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"आणखी दाखवा"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> ला अपडेट केले"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"कनेक्ट करता आले नाही"</string>
</resources>
diff --git a/slices/view/src/main/res/values-ms/strings.xml b/slices/view/src/main/res/values-ms/strings.xml
index ea5ab25..eced3de 100644
--- a/slices/view/src/main/res/values-ms/strings.xml
+++ b/slices/view/src/main/res/values-ms/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Lagi"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Tunjukkan lagi"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Dikemas kini pada <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"Tidak dapat menyambung"</string>
</resources>
diff --git a/slices/view/src/main/res/values-my/strings.xml b/slices/view/src/main/res/values-my/strings.xml
index ea5ab25..4a3e3d4 100644
--- a/slices/view/src/main/res/values-my/strings.xml
+++ b/slices/view/src/main/res/values-my/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"နောက်ထပ်"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"နောက်ထပ် ပြပါ"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> က အပ်ဒိတ်လုပ်ထားသည်"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="other">ပြီးခဲ့သော<xliff:g id="ID_2">%d</xliff:g>မိနစ်</item>
+ <item quantity="one">ပြီးခဲ့သော<xliff:g id="ID_1">%d</xliff:g>မိနစ်</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="other">ပြီးခဲ့သော <xliff:g id="ID_2">%d</xliff:g>နှစ်</item>
+ <item quantity="one">ပြီးခဲ့သော <xliff:g id="ID_1">%d</xliff:g>နှစ်</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="other">ပြီးခဲ့သော <xliff:g id="ID_2">%d</xliff:g> ရက်</item>
+ <item quantity="one">ပြီးခဲ့သော <xliff:g id="ID_1">%d</xliff:g> ရက်</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"ချိတ်ဆက်၍ မရပါ"</string>
</resources>
diff --git a/slices/view/src/main/res/values-nb/strings.xml b/slices/view/src/main/res/values-nb/strings.xml
index ea5ab25..6576626 100644
--- a/slices/view/src/main/res/values-nb/strings.xml
+++ b/slices/view/src/main/res/values-nb/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Mer"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Vis mer"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Oppdatert <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"Kunne ikke koble til"</string>
</resources>
diff --git a/slices/view/src/main/res/values-ne/strings.xml b/slices/view/src/main/res/values-ne/strings.xml
index ea5ab25..beb8b31 100644
--- a/slices/view/src/main/res/values-ne/strings.xml
+++ b/slices/view/src/main/res/values-ne/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"थप"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"थप देखाउनुहोस्"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"अद्यावधिक गरिएको समय: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> मिनेटअघि</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> मिनेटअघि</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> वर्षअघि</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> वर्षअघि</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> दिनअघि</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> दिनअघि</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"जडान गर्न सकिएन"</string>
</resources>
diff --git a/slices/view/src/main/res/values-nl/strings.xml b/slices/view/src/main/res/values-nl/strings.xml
index ea5ab25..1ba58e9 100644
--- a/slices/view/src/main/res/values-nl/strings.xml
+++ b/slices/view/src/main/res/values-nl/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Meer"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Meer weergeven"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Geüpdatet: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"Kan geen verbinding maken"</string>
</resources>
diff --git a/slices/view/src/main/res/values-or/strings.xml b/slices/view/src/main/res/values-or/strings.xml
new file mode 100644
index 0000000..1099e80
--- /dev/null
+++ b/slices/view/src/main/res/values-or/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"ଅଧିକ"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"ଅଧିକ ଦେଖାନ୍ତୁ"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g>ରେ ଅପଡେଟ୍ ହୋଇଥିଲା"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"କନେକ୍ଟ ହେଲାନାହିଁ"</string>
+</resources>
diff --git a/slices/view/src/main/res/values-pa/strings.xml b/slices/view/src/main/res/values-pa/strings.xml
index ea5ab25..8926d0f 100644
--- a/slices/view/src/main/res/values-pa/strings.xml
+++ b/slices/view/src/main/res/values-pa/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"ਹੋਰ"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"ਹੋਰ ਦਿਖਾਓ"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> ਅੱਪਡੇਟ ਕੀਤੀ ਗਈ"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"ਕਨੈਕਟ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ"</string>
</resources>
diff --git a/slices/view/src/main/res/values-pl/strings.xml b/slices/view/src/main/res/values-pl/strings.xml
index 9d9bede..fa565f3 100644
--- a/slices/view/src/main/res/values-pl/strings.xml
+++ b/slices/view/src/main/res/values-pl/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Więcej"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Pokaż więcej"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Aktualizacja: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"Nie udało się połączyć"</string>
</resources>
diff --git a/slices/view/src/main/res/values-pt-rBR/strings.xml b/slices/view/src/main/res/values-pt-rBR/strings.xml
index 629ba48..79cc7b7 100644
--- a/slices/view/src/main/res/values-pt-rBR/strings.xml
+++ b/slices/view/src/main/res/values-pt-rBR/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"Mais <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Mais"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Mostrar mais"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Atualizado às <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> min atrás</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> min atrás</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> ano atrás</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> anos atrás</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> dia atrás</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> dias atrás</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Não foi possível conectar"</string>
</resources>
diff --git a/slices/view/src/main/res/values-pt-rPT/strings.xml b/slices/view/src/main/res/values-pt-rPT/strings.xml
index ea5ab25..b81ec60 100644
--- a/slices/view/src/main/res/values-pt-rPT/strings.xml
+++ b/slices/view/src/main/res/values-pt-rPT/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Mais"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Mostrar mais"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Atualizado: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="other">Há <xliff:g id="ID_2">%d</xliff:g> minutos.</item>
+ <item quantity="one">Há <xliff:g id="ID_1">%d</xliff:g> minuto.</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="other">Há <xliff:g id="ID_2">%d</xliff:g> anos.</item>
+ <item quantity="one">Há <xliff:g id="ID_1">%d</xliff:g> ano.</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="other">Há <xliff:g id="ID_2">%d</xliff:g> dias.</item>
+ <item quantity="one">Há <xliff:g id="ID_1">%d</xliff:g> dia.</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Não foi possível ligar."</string>
</resources>
diff --git a/slices/view/src/main/res/values-pt/strings.xml b/slices/view/src/main/res/values-pt/strings.xml
index 629ba48..79cc7b7 100644
--- a/slices/view/src/main/res/values-pt/strings.xml
+++ b/slices/view/src/main/res/values-pt/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"Mais <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Mais"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Mostrar mais"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Atualizado às <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> min atrás</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> min atrás</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> ano atrás</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> anos atrás</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> dia atrás</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> dias atrás</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Não foi possível conectar"</string>
</resources>
diff --git a/slices/view/src/main/res/values-ro/strings.xml b/slices/view/src/main/res/values-ro/strings.xml
index ea5ab25..a3f9ff2 100644
--- a/slices/view/src/main/res/values-ro/strings.xml
+++ b/slices/view/src/main/res/values-ro/strings.xml
@@ -18,4 +18,23 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Mai mult"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Vedeți mai multe"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Actualizat la <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="few">Acum <xliff:g id="ID_2">%d</xliff:g> min.</item>
+ <item quantity="other">Acum <xliff:g id="ID_2">%d</xliff:g> de min.</item>
+ <item quantity="one">Acum <xliff:g id="ID_1">%d</xliff:g> min.</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="few">Acum <xliff:g id="ID_2">%d</xliff:g> ani</item>
+ <item quantity="other">Acum <xliff:g id="ID_2">%d</xliff:g> de ani</item>
+ <item quantity="one">Acum <xliff:g id="ID_1">%d</xliff:g> an</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="few">Acum <xliff:g id="ID_2">%d</xliff:g> zile</item>
+ <item quantity="other">Acum <xliff:g id="ID_2">%d</xliff:g> de zile</item>
+ <item quantity="one">Acum <xliff:g id="ID_1">%d</xliff:g> zi</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Nu s-a putut conecta"</string>
</resources>
diff --git a/slices/view/src/main/res/values-ru/strings.xml b/slices/view/src/main/res/values-ru/strings.xml
index 9d9bede..85953aa 100644
--- a/slices/view/src/main/res/values-ru/strings.xml
+++ b/slices/view/src/main/res/values-ru/strings.xml
@@ -18,4 +18,26 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Ещё"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Ещё"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Обновлено <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> мин. назад</item>
+ <item quantity="few"><xliff:g id="ID_2">%d</xliff:g> мин. назад</item>
+ <item quantity="many"><xliff:g id="ID_2">%d</xliff:g> мин. назад</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> мин. назад</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> г. назад</item>
+ <item quantity="few"><xliff:g id="ID_2">%d</xliff:g> г. назад</item>
+ <item quantity="many"><xliff:g id="ID_2">%d</xliff:g> лет назад</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> г. назад</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> дн. назад</item>
+ <item quantity="few"><xliff:g id="ID_2">%d</xliff:g> дн. назад</item>
+ <item quantity="many"><xliff:g id="ID_2">%d</xliff:g> дн. назад</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> дн. назад</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Ошибка подключения"</string>
</resources>
diff --git a/slices/view/src/main/res/values-si/strings.xml b/slices/view/src/main/res/values-si/strings.xml
index ea5ab25..be85112 100644
--- a/slices/view/src/main/res/values-si/strings.xml
+++ b/slices/view/src/main/res/values-si/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"තව"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"තව පෙන්වන්න"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> යාවත්කාලීන කරන ලදී"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"සම්බන්ධ වීමට නොහැකි විය"</string>
</resources>
diff --git a/slices/view/src/main/res/values-sk/strings.xml b/slices/view/src/main/res/values-sk/strings.xml
index ea5ab25..a153760 100644
--- a/slices/view/src/main/res/values-sk/strings.xml
+++ b/slices/view/src/main/res/values-sk/strings.xml
@@ -18,4 +18,26 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Viac"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Zobraziť viac"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Aktualizované <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="few">Pred <xliff:g id="ID_2">%d</xliff:g> min</item>
+ <item quantity="many">Pred <xliff:g id="ID_2">%d</xliff:g> min</item>
+ <item quantity="other">Pred <xliff:g id="ID_2">%d</xliff:g> min</item>
+ <item quantity="one">Pred <xliff:g id="ID_1">%d</xliff:g> min</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="few">Pred <xliff:g id="ID_2">%d</xliff:g> rokmi</item>
+ <item quantity="many">Pred <xliff:g id="ID_2">%d</xliff:g> rokom</item>
+ <item quantity="other">Pred <xliff:g id="ID_2">%d</xliff:g> rokmi</item>
+ <item quantity="one">Pred <xliff:g id="ID_1">%d</xliff:g> rokom</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="few">Pred <xliff:g id="ID_2">%d</xliff:g> dňami</item>
+ <item quantity="many">Pred <xliff:g id="ID_2">%d</xliff:g> dňami</item>
+ <item quantity="other">Pred <xliff:g id="ID_2">%d</xliff:g> dňami</item>
+ <item quantity="one">Pred <xliff:g id="ID_1">%d</xliff:g> dňom</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Nepodarilo sa pripojiť"</string>
</resources>
diff --git a/slices/view/src/main/res/values-sl/strings.xml b/slices/view/src/main/res/values-sl/strings.xml
index 59bf101..0389500 100644
--- a/slices/view/src/main/res/values-sl/strings.xml
+++ b/slices/view/src/main/res/values-sl/strings.xml
@@ -18,4 +18,26 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"in še <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Več"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Pokaži več"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Posodobljeno: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="one">pred <xliff:g id="ID_2">%d</xliff:g> minuto</item>
+ <item quantity="two">pred <xliff:g id="ID_2">%d</xliff:g> minutama</item>
+ <item quantity="few">pred <xliff:g id="ID_2">%d</xliff:g> minutami</item>
+ <item quantity="other">pred <xliff:g id="ID_2">%d</xliff:g> minutami</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="one">pred <xliff:g id="ID_2">%d</xliff:g> letom</item>
+ <item quantity="two">pred <xliff:g id="ID_2">%d</xliff:g> letoma</item>
+ <item quantity="few">pred <xliff:g id="ID_2">%d</xliff:g> leti</item>
+ <item quantity="other">pred <xliff:g id="ID_2">%d</xliff:g> leti</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="one">pred <xliff:g id="ID_2">%d</xliff:g> dnem</item>
+ <item quantity="two">pred <xliff:g id="ID_2">%d</xliff:g> dnevoma</item>
+ <item quantity="few">pred <xliff:g id="ID_2">%d</xliff:g> dnevi</item>
+ <item quantity="other">pred <xliff:g id="ID_2">%d</xliff:g> dnevi</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Povezava ni mogoča"</string>
</resources>
diff --git a/slices/view/src/main/res/values-sq/strings.xml b/slices/view/src/main/res/values-sq/strings.xml
index ea5ab25..0359c86 100644
--- a/slices/view/src/main/res/values-sq/strings.xml
+++ b/slices/view/src/main/res/values-sq/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Më shumë"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Shfaq më shumë"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Përditësuar <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> minuta më parë</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> minutë më parë</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> vite më parë</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> vit më parë</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> ditë më parë</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> ditë më parë</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Nuk mund të lidhej"</string>
</resources>
diff --git a/slices/view/src/main/res/values-sr/strings.xml b/slices/view/src/main/res/values-sr/strings.xml
index a66220a..718f3d7 100644
--- a/slices/view/src/main/res/values-sr/strings.xml
+++ b/slices/view/src/main/res/values-sr/strings.xml
@@ -18,4 +18,23 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"и још <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Још"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Прикажи више"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Ажурирано <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="one">пре <xliff:g id="ID_2">%d</xliff:g> мин</item>
+ <item quantity="few">пре <xliff:g id="ID_2">%d</xliff:g> мин</item>
+ <item quantity="other">пре <xliff:g id="ID_2">%d</xliff:g> мин</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="one">пре <xliff:g id="ID_2">%d</xliff:g> год</item>
+ <item quantity="few">пре <xliff:g id="ID_2">%d</xliff:g> год</item>
+ <item quantity="other">пре <xliff:g id="ID_2">%d</xliff:g> год</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="one">пре <xliff:g id="ID_2">%d</xliff:g> дан</item>
+ <item quantity="few">пре <xliff:g id="ID_2">%d</xliff:g> дана</item>
+ <item quantity="other">пре <xliff:g id="ID_2">%d</xliff:g> дана</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Повезивање није успело"</string>
</resources>
diff --git a/slices/view/src/main/res/values-sv/strings.xml b/slices/view/src/main/res/values-sv/strings.xml
index cbcec4f..bc6947d 100644
--- a/slices/view/src/main/res/values-sv/strings.xml
+++ b/slices/view/src/main/res/values-sv/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"<xliff:g id="NUMBER">%1$d</xliff:g> till"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Mer"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Visa mer"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Uppdaterades <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"Det gick inte att ansluta"</string>
</resources>
diff --git a/slices/view/src/main/res/values-sw/strings.xml b/slices/view/src/main/res/values-sw/strings.xml
index ea5ab25..609c51f 100644
--- a/slices/view/src/main/res/values-sw/strings.xml
+++ b/slices/view/src/main/res/values-sw/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Mengine"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Onyesha mengine"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Ilisasishwa <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="other">Dakika <xliff:g id="ID_2">%d</xliff:g> zilizopita</item>
+ <item quantity="one">Dakika <xliff:g id="ID_1">%d</xliff:g> iliyopita</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="other">Miaka <xliff:g id="ID_2">%d</xliff:g> iliyopita</item>
+ <item quantity="one">Mwaka <xliff:g id="ID_1">%d</xliff:g> uliopita</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="other">Siku <xliff:g id="ID_2">%d</xliff:g> zilizopita</item>
+ <item quantity="one">Siku <xliff:g id="ID_1">%d</xliff:g> iliyopita</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Imeshindwa kuunganisha"</string>
</resources>
diff --git a/slices/view/src/main/res/values-ta/strings.xml b/slices/view/src/main/res/values-ta/strings.xml
index ea5ab25..5b36b79 100644
--- a/slices/view/src/main/res/values-ta/strings.xml
+++ b/slices/view/src/main/res/values-ta/strings.xml
@@ -18,4 +18,13 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"மேலும்"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"மேலும் காட்டு"</string>
+ <!-- no translation found for abc_slice_updated (8155085405396453848) -->
+ <skip />
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <!-- no translation found for abc_slice_error (4188371422904147368) -->
+ <skip />
</resources>
diff --git a/slices/view/src/main/res/values-te/strings.xml b/slices/view/src/main/res/values-te/strings.xml
index ea5ab25..82137b8 100644
--- a/slices/view/src/main/res/values-te/strings.xml
+++ b/slices/view/src/main/res/values-te/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"మరింత"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"మరింత చూపు"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"అప్డేట్ చేసిన సమయం <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> నిమి క్రితం</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> నిమి క్రితం</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> సం క్రితం</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> సం క్రితం</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> రోజుల క్రితం</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> రోజు క్రితం</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"కనెక్ట్ చేయడం సాధ్యపడలేదు"</string>
</resources>
diff --git a/slices/view/src/main/res/values-th/strings.xml b/slices/view/src/main/res/values-th/strings.xml
index ea5ab25..3ae19da 100644
--- a/slices/view/src/main/res/values-th/strings.xml
+++ b/slices/view/src/main/res/values-th/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"เพิ่มเติม"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"แสดงเพิ่ม"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"อัปเดตเมื่อ <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> นาทีที่แล้ว</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> นาทีที่แล้ว</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> ปีที่แล้ว</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> ปีที่แล้ว</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> วันที่แล้ว</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> วันที่แล้ว</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"เชื่อมต่อไม่ได้"</string>
</resources>
diff --git a/slices/view/src/main/res/values-tl/strings.xml b/slices/view/src/main/res/values-tl/strings.xml
index ea5ab25..d2d5526 100644
--- a/slices/view/src/main/res/values-tl/strings.xml
+++ b/slices/view/src/main/res/values-tl/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Higit pa"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Magpakita pa"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Na-update noong <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> minuto ang nakalipas</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> na minuto ang nakalipas</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> taon ang nakalipas</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> na taon ang nakalipas</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> araw ang nakalipas</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> na araw ang nakalipas</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Hindi makakonekta"</string>
</resources>
diff --git a/slices/view/src/main/res/values-tr/strings.xml b/slices/view/src/main/res/values-tr/strings.xml
index ea5ab25..84da5fb 100644
--- a/slices/view/src/main/res/values-tr/strings.xml
+++ b/slices/view/src/main/res/values-tr/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Diğer"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Daha fazla göster"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Güncellenme zamanı: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> dk. önce</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> dk. önce</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> yıl önce</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> yıl önce</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> gün önce</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> gün önce</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Bağlanılamadı"</string>
</resources>
diff --git a/slices/view/src/main/res/values-uk/strings.xml b/slices/view/src/main/res/values-uk/strings.xml
index ea5ab25..0706d80 100644
--- a/slices/view/src/main/res/values-uk/strings.xml
+++ b/slices/view/src/main/res/values-uk/strings.xml
@@ -18,4 +18,26 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Більше"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Показати більше"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Оновлено: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> хвилину тому</item>
+ <item quantity="few"><xliff:g id="ID_2">%d</xliff:g> хвилини тому</item>
+ <item quantity="many"><xliff:g id="ID_2">%d</xliff:g> хвилин тому</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> хвилини тому</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> рік тому</item>
+ <item quantity="few"><xliff:g id="ID_2">%d</xliff:g> роки тому</item>
+ <item quantity="many"><xliff:g id="ID_2">%d</xliff:g> років тому</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> року тому</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> день тому</item>
+ <item quantity="few"><xliff:g id="ID_2">%d</xliff:g> дні тому</item>
+ <item quantity="many"><xliff:g id="ID_2">%d</xliff:g> днів тому</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> дня тому</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Не вдалося під’єднатися"</string>
</resources>
diff --git a/slices/view/src/main/res/values-ur/strings.xml b/slices/view/src/main/res/values-ur/strings.xml
index ce427c0..4f51700 100644
--- a/slices/view/src/main/res/values-ur/strings.xml
+++ b/slices/view/src/main/res/values-ur/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"مزید"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"مزید دکھائیں"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> اپ ڈیٹ کیا گیا"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"منسلک نہیں ہو سکا"</string>
</resources>
diff --git a/slices/view/src/main/res/values-uz/strings.xml b/slices/view/src/main/res/values-uz/strings.xml
index ea5ab25..2250c91 100644
--- a/slices/view/src/main/res/values-uz/strings.xml
+++ b/slices/view/src/main/res/values-uz/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Yana"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Yana"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Yangilandi: <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"Ulanib bo‘lmadi"</string>
</resources>
diff --git a/slices/view/src/main/res/values-vi/strings.xml b/slices/view/src/main/res/values-vi/strings.xml
index ea5ab25..c9129be 100644
--- a/slices/view/src/main/res/values-vi/strings.xml
+++ b/slices/view/src/main/res/values-vi/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Thêm"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Hiển thị thêm"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Đã cập nhật lúc <xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"Không thể kết nối"</string>
</resources>
diff --git a/slices/view/src/main/res/values-zh-rCN/strings.xml b/slices/view/src/main/res/values-zh-rCN/strings.xml
index ea5ab25..2ecb835 100644
--- a/slices/view/src/main/res/values-zh-rCN/strings.xml
+++ b/slices/view/src/main/res/values-zh-rCN/strings.xml
@@ -18,4 +18,11 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"更多"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"显示更多"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"更新时间:<xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <!-- no translation found for abc_slice_duration_min (6996334305156847955) -->
+ <!-- no translation found for abc_slice_duration_years (6212691832333991589) -->
+ <!-- no translation found for abc_slice_duration_days (6241698511167107334) -->
+ <string name="abc_slice_error" msgid="4188371422904147368">"无法连接"</string>
</resources>
diff --git a/slices/view/src/main/res/values-zh-rHK/strings.xml b/slices/view/src/main/res/values-zh-rHK/strings.xml
index ea5ab25..d1ac276 100644
--- a/slices/view/src/main/res/values-zh-rHK/strings.xml
+++ b/slices/view/src/main/res/values-zh-rHK/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"更多"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"顯示更多"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"更新時間:<xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> 分鐘前</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> 分鐘前</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> 年前</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> 年前</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> 天前</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> 天前</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"無法連線"</string>
</resources>
diff --git a/slices/view/src/main/res/values-zh-rTW/strings.xml b/slices/view/src/main/res/values-zh-rTW/strings.xml
index ea5ab25..d1ac276 100644
--- a/slices/view/src/main/res/values-zh-rTW/strings.xml
+++ b/slices/view/src/main/res/values-zh-rTW/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"更多"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"顯示更多"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"更新時間:<xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> 分鐘前</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> 分鐘前</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> 年前</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> 年前</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> 天前</item>
+ <item quantity="one"><xliff:g id="ID_1">%d</xliff:g> 天前</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"無法連線"</string>
</resources>
diff --git a/slices/view/src/main/res/values-zu/strings.xml b/slices/view/src/main/res/values-zu/strings.xml
index ea5ab25..966bb1b 100644
--- a/slices/view/src/main/res/values-zu/strings.xml
+++ b/slices/view/src/main/res/values-zu/strings.xml
@@ -18,4 +18,20 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
+ <string name="abc_slice_more" msgid="1983560225998630901">"Okuningi"</string>
+ <string name="abc_slice_show_more" msgid="1567717014004692768">"Bonisa okuningi"</string>
+ <string name="abc_slice_updated" msgid="8155085405396453848">"Kubuyekezwe ngo-<xliff:g id="TIME">%1$s</xliff:g>"</string>
+ <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> iminithi eledlule</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> iminithi eledlule</item>
+ </plurals>
+ <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> unyaka owedlule</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> unyaka owedlule</item>
+ </plurals>
+ <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">
+ <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> izinsuku ezedlule</item>
+ <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> izinsuku ezedlule</item>
+ </plurals>
+ <string name="abc_slice_error" msgid="4188371422904147368">"Ayikwazanga ukuxhuma"</string>
</resources>
diff --git a/slices/view/src/main/res/values/attrs.xml b/slices/view/src/main/res/values/attrs.xml
index f3e621b..b73c5a5 100644
--- a/slices/view/src/main/res/values/attrs.xml
+++ b/slices/view/src/main/res/values/attrs.xml
@@ -30,17 +30,33 @@
<attr name="headerTitleSize" format="dimension" />
<!-- Text size to use for subtitle text in the header of the slice. -->
<attr name="headerSubtitleSize" format="dimension"/>
+ <!-- Vertical padding to use between header title text and header subtitle text. -->
+ <attr name="headerTextVerticalPadding" format="dimension" />
+
<!-- Text size to use for title text in a non-header row of the slice. -->
<attr name="titleSize" format="dimension" />
<!-- Text size to use for subtitle text in a non-header row of the slice. -->
<attr name="subtitleSize" format="dimension" />
+ <!-- Vertical padding to use between title text and subtitle text. -->
+ <attr name="textVerticalPadding" format="dimension" />
+
<!-- Text size to use for title text in a grid row of the slice. -->
<attr name="gridTitleSize" format="dimension" />
<!-- Text size to use for the subtitle text in a grid row of the slice. -->
<attr name="gridSubtitleSize" format="dimension" />
+ <!-- Vertical padding to use between title text and subtitle text in a grid cell. -->
+ <attr name="gridTextVerticalPadding" format="dimension" />
+ <!-- A grid row with all images goes right to the edge of the view if it's the first or
+ last row of a slice. Use this to specify padding to apply to the top of the grid row in
+ this situation. -->
+ <attr name="gridTopPadding" format="dimension" />
+ <!-- A grid row with all images goes right to the edge of the view if it's the first or
+ last row of a slice. Use this to specify padding to apply to the bottom of the grid row in
+ this situation. -->
+ <attr name="gridBottomPadding" format="dimension" />
</declare-styleable>
<!-- To apply a style for all slices shown within an activity or app you
may set this in your app theme pointing to your custom SliceView style.-->
- <attr name="sliceViewStyle" format="reference"/>
+ <attr name="sliceViewStyle" format="reference" />
</resources>
\ No newline at end of file
diff --git a/slices/view/src/main/res/values/dimens.xml b/slices/view/src/main/res/values/dimens.xml
index f3313bf..2894fcc 100644
--- a/slices/view/src/main/res/values/dimens.xml
+++ b/slices/view/src/main/res/values/dimens.xml
@@ -58,6 +58,10 @@
<dimen name="abc_slice_grid_small_image_text_height">120dp</dimen>
<!-- Gutter between cells in a grid row-->
<dimen name="abc_slice_grid_gutter">4dp</dimen>
+ <!-- Space between image and text items in grid row -->
+ <dimen name="abc_slice_grid_text_padding">10dp</dimen>
+ <!-- Space between text and text items in grid row -->
+ <dimen name="abc_slice_grid_text_inner_padding">2dp</dimen>
<!-- Big picture -->
<!-- Min height of row showing a single large image -->
diff --git a/swiperefreshlayout/api/current.txt b/swiperefreshlayout/api/current.txt
index 018d30e..49e8991 100644
--- a/swiperefreshlayout/api/current.txt
+++ b/swiperefreshlayout/api/current.txt
@@ -58,7 +58,9 @@
method public void setProgressViewOffset(boolean, int, int);
method public void setRefreshing(boolean);
method public void setSize(int);
+ method public void setSlingshotDistance(int);
field public static final int DEFAULT = 1; // 0x1
+ field public static final int DEFAULT_SLINGSHOT_DISTANCE = -1; // 0xffffffff
field public static final int LARGE = 0; // 0x0
field protected int mFrom;
field protected int mOriginalOffsetTop;
diff --git a/swiperefreshlayout/src/main/java/androidx/swiperefreshlayout/widget/SwipeRefreshLayout.java b/swiperefreshlayout/src/main/java/androidx/swiperefreshlayout/widget/SwipeRefreshLayout.java
index cd99498..5df8cdd 100644
--- a/swiperefreshlayout/src/main/java/androidx/swiperefreshlayout/widget/SwipeRefreshLayout.java
+++ b/swiperefreshlayout/src/main/java/androidx/swiperefreshlayout/widget/SwipeRefreshLayout.java
@@ -36,6 +36,7 @@
import androidx.annotation.ColorRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.Px;
import androidx.annotation.VisibleForTesting;
import androidx.core.content.ContextCompat;
import androidx.core.view.NestedScrollingChild;
@@ -73,6 +74,8 @@
// Maps to ProgressBar default style
public static final int DEFAULT = CircularProgressDrawable.DEFAULT;
+ public static final int DEFAULT_SLINGSHOT_DISTANCE = -1;
+
@VisibleForTesting
static final int CIRCLE_DIAMETER = 40;
@VisibleForTesting
@@ -149,6 +152,8 @@
int mSpinnerOffsetEnd;
+ int mCustomSlingshotDistance;
+
CircularProgressDrawable mProgress;
private Animation mScaleAnimation;
@@ -294,6 +299,18 @@
}
/**
+ * Sets a custom slingshot distance.
+ *
+ * @param slingshotDistance The distance in pixels that the refresh indicator can be pulled
+ * beyond its resting position. Use
+ * {@link #DEFAULT_SLINGSHOT_DISTANCE} to reset to the default value.
+ *
+ */
+ public void setSlingshotDistance(@Px int slingshotDistance) {
+ mCustomSlingshotDistance = slingshotDistance;
+ }
+
+ /**
* One of DEFAULT, or LARGE.
*/
public void setSize(int size) {
@@ -902,8 +919,11 @@
float dragPercent = Math.min(1f, Math.abs(originalDragPercent));
float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3;
float extraOS = Math.abs(overscrollTop) - mTotalDragDistance;
- float slingshotDist = mUsingCustomStart ? mSpinnerOffsetEnd - mOriginalOffsetTop
- : mSpinnerOffsetEnd;
+ float slingshotDist = mCustomSlingshotDistance > 0
+ ? mCustomSlingshotDistance
+ : (mUsingCustomStart
+ ? mSpinnerOffsetEnd - mOriginalOffsetTop
+ : mSpinnerOffsetEnd);
float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, slingshotDist * 2)
/ slingshotDist);
float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow(
diff --git a/testutils-ktx/NO_DOCS b/testutils-ktx/NO_DOCS
new file mode 100644
index 0000000..4dad694
--- /dev/null
+++ b/testutils-ktx/NO_DOCS
@@ -0,0 +1,17 @@
+# Copyright (C) 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+Having this file, named NO_DOCS, in a directory will prevent
+Android javadocs from being generated for java files under
+the directory. This is especially useful for test projects.
diff --git a/testutils-ktx/OWNERS b/testutils-ktx/OWNERS
new file mode 100644
index 0000000..e450f4c
--- /dev/null
+++ b/testutils-ktx/OWNERS
@@ -0,0 +1 @@
+jakew@google.com
diff --git a/testutils-ktx/build.gradle b/testutils-ktx/build.gradle
new file mode 100644
index 0000000..5f33da1
--- /dev/null
+++ b/testutils-ktx/build.gradle
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+ id("SupportKotlinLibraryPlugin")
+}
+
+dependencies {
+ compile(KOTLIN_STDLIB)
+ compile(TRUTH)
+}
diff --git a/testutils-ktx/src/main/java/androidx/testutils/assertions.kt b/testutils-ktx/src/main/java/androidx/testutils/assertions.kt
new file mode 100644
index 0000000..b67948a
--- /dev/null
+++ b/testutils-ktx/src/main/java/androidx/testutils/assertions.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.testutils
+
+import com.google.common.truth.ThrowableSubject
+import com.google.common.truth.Truth.assertThat
+
+inline fun <reified T : Throwable> assertThrows(body: () -> Unit): ThrowableSubject {
+ try {
+ body()
+ } catch (e: Throwable) {
+ if (e is T) {
+ return assertThat(e)
+ }
+ throw e
+ }
+ throw AssertionError("Body completed successfully. Expected ${T::class.java.simpleName}.")
+}
+
+fun fail(message: String? = null): Nothing = throw AssertionError(message)
diff --git a/transition/src/main/java/androidx/transition/TransitionManager.java b/transition/src/main/java/androidx/transition/TransitionManager.java
index 517d2e3..579f4b9 100644
--- a/transition/src/main/java/androidx/transition/TransitionManager.java
+++ b/transition/src/main/java/androidx/transition/TransitionManager.java
@@ -193,12 +193,16 @@
static ArrayMap<ViewGroup, ArrayList<Transition>> getRunningTransitions() {
WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>> runningTransitions =
sRunningTransitions.get();
- if (runningTransitions == null || runningTransitions.get() == null) {
- ArrayMap<ViewGroup, ArrayList<Transition>> transitions = new ArrayMap<>();
- runningTransitions = new WeakReference<>(transitions);
- sRunningTransitions.set(runningTransitions);
+ if (runningTransitions != null) {
+ ArrayMap<ViewGroup, ArrayList<Transition>> transitions = runningTransitions.get();
+ if (transitions != null) {
+ return transitions;
+ }
}
- return runningTransitions.get();
+ ArrayMap<ViewGroup, ArrayList<Transition>> transitions = new ArrayMap<>();
+ runningTransitions = new WeakReference<>(transitions);
+ sRunningTransitions.set(runningTransitions);
+ return transitions;
}
private static void sceneChangeRunTransition(final ViewGroup sceneRoot,
diff --git a/tv-provider/src/main/java/androidx/tvprovider/media/tv/TvContractUtils.java b/tv-provider/src/main/java/androidx/tvprovider/media/tv/TvContractUtils.java
index 3483d0f..99383c0 100644
--- a/tv-provider/src/main/java/androidx/tvprovider/media/tv/TvContractUtils.java
+++ b/tv-provider/src/main/java/androidx/tvprovider/media/tv/TvContractUtils.java
@@ -50,7 +50,7 @@
if (TextUtils.isEmpty(commaSeparatedRatings)) {
return EMPTY;
}
- String[] ratings = commaSeparatedRatings.split("\\s*,\\s*");
+ String[] ratings = commaSeparatedRatings.split("\\s*,\\s*", -1);
List<TvContentRating> contentRatings = new ArrayList<>(ratings.length);
for (String rating : ratings) {
try {
diff --git a/v7/appcompat/res/values-af/strings.xml b/v7/appcompat/res/values-af/strings.xml
index 15ed3b2..b7dd9bc 100644
--- a/v7/appcompat/res/values-af/strings.xml
+++ b/v7/appcompat/res/values-af/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"AAN"</string>
<string name="abc_capital_off" msgid="121134116657445385">"AF"</string>
<string name="search_menu_title" msgid="146198913615257606">"Soek"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Kieslys+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Simbool+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Funksie+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"spasiebalk"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-am/strings.xml b/v7/appcompat/res/values-am/strings.xml
index 42bbc06..485ffad 100644
--- a/v7/appcompat/res/values-am/strings.xml
+++ b/v7/appcompat/res/values-am/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"በርቷል"</string>
<string name="abc_capital_off" msgid="121134116657445385">"ጠፍቷል"</string>
<string name="search_menu_title" msgid="146198913615257606">"ፈልግ"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"ምናሌ+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"ሰርዝ"</string>
</resources>
diff --git a/v7/appcompat/res/values-ar/strings.xml b/v7/appcompat/res/values-ar/strings.xml
index 3278162..a7683ce 100644
--- a/v7/appcompat/res/values-ar/strings.xml
+++ b/v7/appcompat/res/values-ar/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"تشغيل"</string>
<string name="abc_capital_off" msgid="121134116657445385">"إيقاف"</string>
<string name="search_menu_title" msgid="146198913615257606">"البحث"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"القائمة+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-as/strings.xml b/v7/appcompat/res/values-as/strings.xml
index 1f90618..a3574d7 100644
--- a/v7/appcompat/res/values-as/strings.xml
+++ b/v7/appcompat/res/values-as/strings.xml
@@ -29,13 +29,19 @@
<string name="abc_searchview_description_voice" msgid="893419373245838918">"কণ্ঠধ্বনিৰ যোগেৰে সন্ধান কৰক"</string>
<string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"এটা এপ্ বাছনি কৰক"</string>
<string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"সকলো চাওক"</string>
- <!-- no translation found for abc_shareactionprovider_share_with_application (3300176832234831527) -->
- <skip />
+ <string name="abc_shareactionprovider_share_with_application" msgid="3300176832234831527">"<xliff:g id="APPLICATION_NAME">%s</xliff:g>ৰ জৰিয়তে শ্বেয়াৰ কৰক"</string>
<string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"ইয়াৰ জৰিয়তে শ্বেয়াৰ কৰক"</string>
- <!-- no translation found for abc_capital_on (3405795526292276155) -->
- <skip />
- <!-- no translation found for abc_capital_off (121134116657445385) -->
- <skip />
- <!-- no translation found for search_menu_title (146198913615257606) -->
- <skip />
+ <string name="abc_capital_on" msgid="3405795526292276155">"অন কৰক"</string>
+ <string name="abc_capital_off" msgid="121134116657445385">"অফ কৰক"</string>
+ <string name="search_menu_title" msgid="146198913615257606">"অনুসন্ধান কৰক"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"মেনু+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"মেটা+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"CTRL+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"শ্বিফ্ট+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"ফাংশ্বন+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"স্পেচ"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"এণ্টাৰ"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"মচক"</string>
</resources>
diff --git a/v7/appcompat/res/values-az/strings.xml b/v7/appcompat/res/values-az/strings.xml
index 29e00dd..aae3b94 100644
--- a/v7/appcompat/res/values-az/strings.xml
+++ b/v7/appcompat/res/values-az/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"AKTİV"</string>
<string name="abc_capital_off" msgid="121134116657445385">"DEAKTİV"</string>
<string name="search_menu_title" msgid="146198913615257606">"Axtarış"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menyu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Funksiya+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"kosmos"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"daxil olun"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"silin"</string>
</resources>
diff --git a/v7/appcompat/res/values-b+sr+Latn/strings.xml b/v7/appcompat/res/values-b+sr+Latn/strings.xml
index c3462f6..734525b 100644
--- a/v7/appcompat/res/values-b+sr+Latn/strings.xml
+++ b/v7/appcompat/res/values-b+sr+Latn/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"UKLJUČI"</string>
<string name="abc_capital_off" msgid="121134116657445385">"ISKLJUČI"</string>
<string name="search_menu_title" msgid="146198913615257606">"Pretraži"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"taster za razmak"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-be/strings.xml b/v7/appcompat/res/values-be/strings.xml
index 99ee19f..f04076a 100644
--- a/v7/appcompat/res/values-be/strings.xml
+++ b/v7/appcompat/res/values-be/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"УКЛ."</string>
<string name="abc_capital_off" msgid="121134116657445385">"ВЫКЛ."</string>
<string name="search_menu_title" msgid="146198913615257606">"Пошук"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Меню +"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta +"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl +"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt +"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift +"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym +"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fn +"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"Прабел"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-bg/strings.xml b/v7/appcompat/res/values-bg/strings.xml
index 1d37d0d..2c607a1 100644
--- a/v7/appcompat/res/values-bg/strings.xml
+++ b/v7/appcompat/res/values-bg/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ВКЛ."</string>
<string name="abc_capital_off" msgid="121134116657445385">"ИЗКЛ."</string>
<string name="search_menu_title" msgid="146198913615257606">"Търсене"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"клавиша за интервал"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-bn/strings.xml b/v7/appcompat/res/values-bn/strings.xml
index 2ea7591..a6be993 100644
--- a/v7/appcompat/res/values-bn/strings.xml
+++ b/v7/appcompat/res/values-bn/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"চালু"</string>
<string name="abc_capital_off" msgid="121134116657445385">"বন্ধ"</string>
<string name="search_menu_title" msgid="146198913615257606">"খুঁজুন"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"মেনু+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"স্পেস"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"মুছুন"</string>
</resources>
diff --git a/v7/appcompat/res/values-bs/strings.xml b/v7/appcompat/res/values-bs/strings.xml
index 07d1411..91281fc 100644
--- a/v7/appcompat/res/values-bs/strings.xml
+++ b/v7/appcompat/res/values-bs/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"UKLJUČI"</string>
<string name="abc_capital_off" msgid="121134116657445385">"ISKLJUČI"</string>
<string name="search_menu_title" msgid="146198913615257606">"Pretraži"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"razmaknica"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-ca/strings.xml b/v7/appcompat/res/values-ca/strings.xml
index 03ebec3..c4f3b4d 100644
--- a/v7/appcompat/res/values-ca/strings.xml
+++ b/v7/appcompat/res/values-ca/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ACTIVAT"</string>
<string name="abc_capital_off" msgid="121134116657445385">"DESACTIVAT"</string>
<string name="search_menu_title" msgid="146198913615257606">"Cerca"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menú+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Maj+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Funció+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"Espai"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Retorn"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Supr"</string>
</resources>
diff --git a/v7/appcompat/res/values-cs/strings.xml b/v7/appcompat/res/values-cs/strings.xml
index 05cd4e0..9111883 100644
--- a/v7/appcompat/res/values-cs/strings.xml
+++ b/v7/appcompat/res/values-cs/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ZAPNUTO"</string>
<string name="abc_capital_off" msgid="121134116657445385">"VYPNUTO"</string>
<string name="search_menu_title" msgid="146198913615257606">"Hledat"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fn+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"mezerník"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-da/strings.xml b/v7/appcompat/res/values-da/strings.xml
index 813885a..f05e142 100644
--- a/v7/appcompat/res/values-da/strings.xml
+++ b/v7/appcompat/res/values-da/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"TIL"</string>
<string name="abc_capital_off" msgid="121134116657445385">"FRA"</string>
<string name="search_menu_title" msgid="146198913615257606">"Søg"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fn+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"mellemrum"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-de/strings.xml b/v7/appcompat/res/values-de/strings.xml
index 0b57259..fb0dd6a 100644
--- a/v7/appcompat/res/values-de/strings.xml
+++ b/v7/appcompat/res/values-de/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"An"</string>
<string name="abc_capital_off" msgid="121134116657445385">"Aus"</string>
<string name="search_menu_title" msgid="146198913615257606">"Suchen"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menütaste +"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta-Taste +"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Strg +"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt +"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Umschalttaste +"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym-Taste +"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Funktionstaste +"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"Leertaste +"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Eingabetaste"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Löschen"</string>
</resources>
diff --git a/v7/appcompat/res/values-el/strings.xml b/v7/appcompat/res/values-el/strings.xml
index ec7a666..b7da605 100644
--- a/v7/appcompat/res/values-el/strings.xml
+++ b/v7/appcompat/res/values-el/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ΕΝΕΡΓΟΠΟΙΗΣΗ"</string>
<string name="abc_capital_off" msgid="121134116657445385">"ΑΠΕΝΕΡΓΟΠΟΙΗΣΗ"</string>
<string name="search_menu_title" msgid="146198913615257606">"Αναζήτηση"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"διάστημα"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-en-rAU/strings.xml b/v7/appcompat/res/values-en-rAU/strings.xml
index a4d048c..a315670 100644
--- a/v7/appcompat/res/values-en-rAU/strings.xml
+++ b/v7/appcompat/res/values-en-rAU/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ON"</string>
<string name="abc_capital_off" msgid="121134116657445385">"OFF"</string>
<string name="search_menu_title" msgid="146198913615257606">"Search"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-en-rCA/strings.xml b/v7/appcompat/res/values-en-rCA/strings.xml
index a4d048c..a315670 100644
--- a/v7/appcompat/res/values-en-rCA/strings.xml
+++ b/v7/appcompat/res/values-en-rCA/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ON"</string>
<string name="abc_capital_off" msgid="121134116657445385">"OFF"</string>
<string name="search_menu_title" msgid="146198913615257606">"Search"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-en-rGB/strings.xml b/v7/appcompat/res/values-en-rGB/strings.xml
index a4d048c..a315670 100644
--- a/v7/appcompat/res/values-en-rGB/strings.xml
+++ b/v7/appcompat/res/values-en-rGB/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ON"</string>
<string name="abc_capital_off" msgid="121134116657445385">"OFF"</string>
<string name="search_menu_title" msgid="146198913615257606">"Search"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-en-rIN/strings.xml b/v7/appcompat/res/values-en-rIN/strings.xml
index a4d048c..a315670 100644
--- a/v7/appcompat/res/values-en-rIN/strings.xml
+++ b/v7/appcompat/res/values-en-rIN/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ON"</string>
<string name="abc_capital_off" msgid="121134116657445385">"OFF"</string>
<string name="search_menu_title" msgid="146198913615257606">"Search"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-en-rXC/strings.xml b/v7/appcompat/res/values-en-rXC/strings.xml
index b1d5f93..2e8e581 100644
--- a/v7/appcompat/res/values-en-rXC/strings.xml
+++ b/v7/appcompat/res/values-en-rXC/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ON"</string>
<string name="abc_capital_off" msgid="121134116657445385">"OFF"</string>
<string name="search_menu_title" msgid="146198913615257606">"Search"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-es-rUS/strings.xml b/v7/appcompat/res/values-es-rUS/strings.xml
index 0cc8a70..5d58df0 100644
--- a/v7/appcompat/res/values-es-rUS/strings.xml
+++ b/v7/appcompat/res/values-es-rUS/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ACTIVADO"</string>
<string name="abc_capital_off" msgid="121134116657445385">"DESACTIVADO"</string>
<string name="search_menu_title" msgid="146198913615257606">"Buscar"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menú+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Mayúscula+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Función+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"espacio"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"intro"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"borrar"</string>
</resources>
diff --git a/v7/appcompat/res/values-es/strings.xml b/v7/appcompat/res/values-es/strings.xml
index 3e0828b..5238229 100644
--- a/v7/appcompat/res/values-es/strings.xml
+++ b/v7/appcompat/res/values-es/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ACTIVADO"</string>
<string name="abc_capital_off" msgid="121134116657445385">"DESACTIVADO"</string>
<string name="search_menu_title" msgid="146198913615257606">"Buscar"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menú +"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta +"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl +"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt +"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Mayús +"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym +"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Función +"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"Espacio"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Intro"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Eliminar"</string>
</resources>
diff --git a/v7/appcompat/res/values-et/strings.xml b/v7/appcompat/res/values-et/strings.xml
index 3a3dcbf..b944f77 100644
--- a/v7/appcompat/res/values-et/strings.xml
+++ b/v7/appcompat/res/values-et/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"SEES"</string>
<string name="abc_capital_off" msgid="121134116657445385">"VÄLJAS"</string>
<string name="search_menu_title" msgid="146198913615257606">"Otsing"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menüü +"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta +"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl +"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt +"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Tõstuklahv +"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym +"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Funktsiooniklahv +"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"tühik"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"sisestusklahv"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"kustutamisklahv"</string>
</resources>
diff --git a/v7/appcompat/res/values-eu/strings.xml b/v7/appcompat/res/values-eu/strings.xml
index a651036..5a45e61 100644
--- a/v7/appcompat/res/values-eu/strings.xml
+++ b/v7/appcompat/res/values-eu/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"AKTIBATUTA"</string>
<string name="abc_capital_off" msgid="121134116657445385">"DESAKTIBATUTA"</string>
<string name="search_menu_title" msgid="146198913615257606">"Bilatu"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menua +"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta +"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ktrl +"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt +"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Maius +"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym +"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Funtzioa +"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"Zuriunea"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Sartu"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Ezabatu"</string>
</resources>
diff --git a/v7/appcompat/res/values-fa/strings.xml b/v7/appcompat/res/values-fa/strings.xml
index 9b844b0..35e6596 100644
--- a/v7/appcompat/res/values-fa/strings.xml
+++ b/v7/appcompat/res/values-fa/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"روشن"</string>
<string name="abc_capital_off" msgid="121134116657445385">"خاموش"</string>
<string name="search_menu_title" msgid="146198913615257606">"جستجو"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"منو+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"کلید فاصله"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-fi/strings.xml b/v7/appcompat/res/values-fi/strings.xml
index e9bd952..829b6bb 100644
--- a/v7/appcompat/res/values-fi/strings.xml
+++ b/v7/appcompat/res/values-fi/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"KÄYTÖSSÄ"</string>
<string name="abc_capital_off" msgid="121134116657445385">"POIS KÄYTÖSTÄ"</string>
<string name="search_menu_title" msgid="146198913615257606">"Haku"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Valikko+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Vaihto+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fn+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"välilyönti"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-fr-rCA/strings.xml b/v7/appcompat/res/values-fr-rCA/strings.xml
index a3e763b..9e83462 100644
--- a/v7/appcompat/res/values-fr-rCA/strings.xml
+++ b/v7/appcompat/res/values-fr-rCA/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ACTIVÉ"</string>
<string name="abc_capital_off" msgid="121134116657445385">"DÉSACTIVÉ"</string>
<string name="search_menu_title" msgid="146198913615257606">"Rechercher"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Méta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Maj+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fonction+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"espace"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"entrée"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"supprimer"</string>
</resources>
diff --git a/v7/appcompat/res/values-fr/strings.xml b/v7/appcompat/res/values-fr/strings.xml
index 1e412ec..5df86b5 100644
--- a/v7/appcompat/res/values-fr/strings.xml
+++ b/v7/appcompat/res/values-fr/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ACTIVÉ"</string>
<string name="abc_capital_off" msgid="121134116657445385">"DÉSACTIVÉ"</string>
<string name="search_menu_title" msgid="146198913615257606">"Rechercher"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Méta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Maj+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fonction+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"espace"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"entrée"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"supprimer"</string>
</resources>
diff --git a/v7/appcompat/res/values-gl/strings.xml b/v7/appcompat/res/values-gl/strings.xml
index 2af80a1..c500af5 100644
--- a/v7/appcompat/res/values-gl/strings.xml
+++ b/v7/appcompat/res/values-gl/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ACTIVAR"</string>
<string name="abc_capital_off" msgid="121134116657445385">"DESACTIVAR"</string>
<string name="search_menu_title" msgid="146198913615257606">"Buscar"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menú +"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta +"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl +"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt +"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Maiús +"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sim +"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Función +"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"espazo"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Intro"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"eliminar"</string>
</resources>
diff --git a/v7/appcompat/res/values-gu/strings.xml b/v7/appcompat/res/values-gu/strings.xml
index 7a243ed..390d59b 100644
--- a/v7/appcompat/res/values-gu/strings.xml
+++ b/v7/appcompat/res/values-gu/strings.xml
@@ -18,7 +18,7 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="abc_action_mode_done" msgid="4076576682505996667">"થઈ ગયું"</string>
<string name="abc_action_bar_home_description" msgid="4600421777120114993">"હોમ પર નેવિગેટ કરો"</string>
- <string name="abc_action_bar_up_description" msgid="1594238315039666878">"ઉપર નેવિગેટ કરો"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"ઉપર નૅવિગેટ કરો"</string>
<string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"વધુ વિકલ્પો"</string>
<string name="abc_toolbar_collapse_description" msgid="1603543279005712093">"સંકુચિત કરો"</string>
<string name="abc_searchview_description_search" msgid="8264924765203268293">"શોધો"</string>
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ચાલુ"</string>
<string name="abc_capital_off" msgid="121134116657445385">"બંધ"</string>
<string name="search_menu_title" msgid="146198913615257606">"શોધો"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"મેનૂ+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"Spacebar"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"ડિલીટ કરો"</string>
</resources>
diff --git a/v7/appcompat/res/values-hi/strings.xml b/v7/appcompat/res/values-hi/strings.xml
index a31ab90..16dc7ce 100644
--- a/v7/appcompat/res/values-hi/strings.xml
+++ b/v7/appcompat/res/values-hi/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"चालू"</string>
<string name="abc_capital_off" msgid="121134116657445385">"बंद"</string>
<string name="search_menu_title" msgid="146198913615257606">"सर्च"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-hr/strings.xml b/v7/appcompat/res/values-hr/strings.xml
index 27a1c2e..9253fb9 100644
--- a/v7/appcompat/res/values-hr/strings.xml
+++ b/v7/appcompat/res/values-hr/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"UKLJUČENO"</string>
<string name="abc_capital_off" msgid="121134116657445385">"ISKLJUČENO"</string>
<string name="search_menu_title" msgid="146198913615257606">"Pretraživanje"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"razmaknica"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-hu/strings.xml b/v7/appcompat/res/values-hu/strings.xml
index d3e413f..714ef47 100644
--- a/v7/appcompat/res/values-hu/strings.xml
+++ b/v7/appcompat/res/values-hu/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"BE"</string>
<string name="abc_capital_off" msgid="121134116657445385">"KI"</string>
<string name="search_menu_title" msgid="146198913615257606">"Keresés"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"Szóköz"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-hy/strings.xml b/v7/appcompat/res/values-hy/strings.xml
index 1c41ef6..59c9ee5 100644
--- a/v7/appcompat/res/values-hy/strings.xml
+++ b/v7/appcompat/res/values-hy/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ՄԻԱՑՎԱԾ"</string>
<string name="abc_capital_off" msgid="121134116657445385">"ԱՆՋԱՏՎԱԾ"</string>
<string name="search_menu_title" msgid="146198913615257606">"Որոնել"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"բացատ"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-in/strings.xml b/v7/appcompat/res/values-in/strings.xml
index 2e9fbb7..1ae3f90 100644
--- a/v7/appcompat/res/values-in/strings.xml
+++ b/v7/appcompat/res/values-in/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"AKTIF"</string>
<string name="abc_capital_off" msgid="121134116657445385">"NONAKTIF"</string>
<string name="search_menu_title" msgid="146198913615257606">"Telusuri"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"spasi"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-is/strings.xml b/v7/appcompat/res/values-is/strings.xml
index 3f61d84..3ae59da 100644
--- a/v7/appcompat/res/values-is/strings.xml
+++ b/v7/appcompat/res/values-is/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"KVEIKT"</string>
<string name="abc_capital_off" msgid="121134116657445385">"SLÖKKT"</string>
<string name="search_menu_title" msgid="146198913615257606">"Leita"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Valmynd+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Aðgerðarlykill+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"bilslá"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-it/strings.xml b/v7/appcompat/res/values-it/strings.xml
index fbd2c58..04b64ce 100644
--- a/v7/appcompat/res/values-it/strings.xml
+++ b/v7/appcompat/res/values-it/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ON"</string>
<string name="abc_capital_off" msgid="121134116657445385">"OFF"</string>
<string name="search_menu_title" msgid="146198913615257606">"Ricerca"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"MENU +"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"META +"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"CTRL +"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"ALT +"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"MAIUSC +"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"SYM +"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"FUNZIONE +"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"barra spaziatrice"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"INVIO"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"CANC"</string>
</resources>
diff --git a/v7/appcompat/res/values-iw/strings.xml b/v7/appcompat/res/values-iw/strings.xml
index 0a7d0bb..d0f2211 100644
--- a/v7/appcompat/res/values-iw/strings.xml
+++ b/v7/appcompat/res/values-iw/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"פועל"</string>
<string name="abc_capital_off" msgid="121134116657445385">"כבוי"</string>
<string name="search_menu_title" msgid="146198913615257606">"חיפוש"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"תפריט+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"מקש רווח"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-ja/strings.xml b/v7/appcompat/res/values-ja/strings.xml
index c4d0e20..db163f3 100644
--- a/v7/appcompat/res/values-ja/strings.xml
+++ b/v7/appcompat/res/values-ja/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ON"</string>
<string name="abc_capital_off" msgid="121134116657445385">"OFF"</string>
<string name="search_menu_title" msgid="146198913615257606">"検索"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"Space"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-ka/strings.xml b/v7/appcompat/res/values-ka/strings.xml
index 3b077a3..6d9c08b 100644
--- a/v7/appcompat/res/values-ka/strings.xml
+++ b/v7/appcompat/res/values-ka/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ჩართულია"</string>
<string name="abc_capital_off" msgid="121134116657445385">"გამორთულია"</string>
<string name="search_menu_title" msgid="146198913615257606">"ძიება"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"შეყვანა"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"წაშლა"</string>
</resources>
diff --git a/v7/appcompat/res/values-kk/strings.xml b/v7/appcompat/res/values-kk/strings.xml
index c32045b..9d206bc 100644
--- a/v7/appcompat/res/values-kk/strings.xml
+++ b/v7/appcompat/res/values-kk/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ҚОСУЛЫ"</string>
<string name="abc_capital_off" msgid="121134116657445385">"ӨШІРУЛІ"</string>
<string name="search_menu_title" msgid="146198913615257606">"Іздеу"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Mәзір+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"бос орын"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-km/strings.xml b/v7/appcompat/res/values-km/strings.xml
index ffe289a..f42da7a 100644
--- a/v7/appcompat/res/values-km/strings.xml
+++ b/v7/appcompat/res/values-km/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"បើក"</string>
<string name="abc_capital_off" msgid="121134116657445385">"បិទ"</string>
<string name="search_menu_title" msgid="146198913615257606">"ស្វែងរក"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-kn/strings.xml b/v7/appcompat/res/values-kn/strings.xml
index 4218bd6..ce28303 100644
--- a/v7/appcompat/res/values-kn/strings.xml
+++ b/v7/appcompat/res/values-kn/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ಆನ್"</string>
<string name="abc_capital_off" msgid="121134116657445385">"ಆಫ್"</string>
<string name="search_menu_title" msgid="146198913615257606">"ಹುಡುಕಿ"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-ko/strings.xml b/v7/appcompat/res/values-ko/strings.xml
index 6c84a2a..db0903f 100644
--- a/v7/appcompat/res/values-ko/strings.xml
+++ b/v7/appcompat/res/values-ko/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"사용"</string>
<string name="abc_capital_off" msgid="121134116657445385">"사용 안함"</string>
<string name="search_menu_title" msgid="146198913615257606">"검색"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"스페이스바"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"입력"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"삭제"</string>
</resources>
diff --git a/v7/appcompat/res/values-ky/strings.xml b/v7/appcompat/res/values-ky/strings.xml
index 66202f7..1aba4f0 100644
--- a/v7/appcompat/res/values-ky/strings.xml
+++ b/v7/appcompat/res/values-ky/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"КҮЙҮК"</string>
<string name="abc_capital_off" msgid="121134116657445385">"ӨЧҮК"</string>
<string name="search_menu_title" msgid="146198913615257606">"Издөө"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"боштук"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-lo/strings.xml b/v7/appcompat/res/values-lo/strings.xml
index 1b92df0..c53a987 100644
--- a/v7/appcompat/res/values-lo/strings.xml
+++ b/v7/appcompat/res/values-lo/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ເປີດ"</string>
<string name="abc_capital_off" msgid="121134116657445385">"ປິດ"</string>
<string name="search_menu_title" msgid="146198913615257606">"ຊອກຫາ"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-lt/strings.xml b/v7/appcompat/res/values-lt/strings.xml
index 5793069..ab9f25c 100644
--- a/v7/appcompat/res/values-lt/strings.xml
+++ b/v7/appcompat/res/values-lt/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ĮJUNGTI"</string>
<string name="abc_capital_off" msgid="121134116657445385">"IŠJUNGTA"</string>
<string name="search_menu_title" msgid="146198913615257606">"Paieška"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"„Menu“ +"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"„Meta“ +"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"„Ctrl“ +"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"„Alt“ +"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"„Shift“ +"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"„Sym“ +"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"„Function“ +"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"tarpo klavišas"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"„Enter“"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"„Delete“"</string>
</resources>
diff --git a/v7/appcompat/res/values-lv/strings.xml b/v7/appcompat/res/values-lv/strings.xml
index 67e18d3..67904a1 100644
--- a/v7/appcompat/res/values-lv/strings.xml
+++ b/v7/appcompat/res/values-lv/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"IESLĒGTS"</string>
<string name="abc_capital_off" msgid="121134116657445385">"IZSLĒGTS"</string>
<string name="search_menu_title" msgid="146198913615257606">"Meklēt"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Poga Izvēlne +"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta taustiņš +"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Vadīšanas taustiņš +"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alternēšanas taustiņš +"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Pārslēgšanas taustiņš +"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Simbolu taustiņš +"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Funkcijas taustiņš +"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"atstarpes taustiņš"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"ievadīšanas taustiņš"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"dzēšanas taustiņš"</string>
</resources>
diff --git a/v7/appcompat/res/values-mk/strings.xml b/v7/appcompat/res/values-mk/strings.xml
index b12a235..408edb1 100644
--- a/v7/appcompat/res/values-mk/strings.xml
+++ b/v7/appcompat/res/values-mk/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ВКЛУЧЕНО"</string>
<string name="abc_capital_off" msgid="121134116657445385">"ИСКЛУЧЕНО"</string>
<string name="search_menu_title" msgid="146198913615257606">"Пребарај"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Мени+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"копче Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"копче Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"копче Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"копче Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"копче Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"копче Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"вселена"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"копче enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"избриши"</string>
</resources>
diff --git a/v7/appcompat/res/values-ml/strings.xml b/v7/appcompat/res/values-ml/strings.xml
index 9033f8a..e880ebb 100644
--- a/v7/appcompat/res/values-ml/strings.xml
+++ b/v7/appcompat/res/values-ml/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ഓൺ"</string>
<string name="abc_capital_off" msgid="121134116657445385">"ഓഫ്"</string>
<string name="search_menu_title" msgid="146198913615257606">"തിരയുക"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"മെനു+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"മെറ്റ+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"ഫംഗ്ഷന്+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"സ്പെയ്സ്"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"ഇല്ലാതാക്കുക"</string>
</resources>
diff --git a/v7/appcompat/res/values-mn/strings.xml b/v7/appcompat/res/values-mn/strings.xml
index 56036ea..856e9f6 100644
--- a/v7/appcompat/res/values-mn/strings.xml
+++ b/v7/appcompat/res/values-mn/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ИДЭВХТЭЙ"</string>
<string name="abc_capital_off" msgid="121134116657445385">"ИДЭВХГҮЙ"</string>
<string name="search_menu_title" msgid="146198913615257606">"Хайлт"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Цэс+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Мета+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Функц+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"зай"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"оруулах"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"устгах"</string>
</resources>
diff --git a/v7/appcompat/res/values-mr/strings.xml b/v7/appcompat/res/values-mr/strings.xml
index 26ada80..ed3463c 100644
--- a/v7/appcompat/res/values-mr/strings.xml
+++ b/v7/appcompat/res/values-mr/strings.xml
@@ -30,8 +30,18 @@
<string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"एक अॅप निवडा"</string>
<string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"सर्व पहा"</string>
<string name="abc_shareactionprovider_share_with_application" msgid="3300176832234831527">"<xliff:g id="APPLICATION_NAME">%s</xliff:g> सह शेअर करा"</string>
- <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"यांच्यासह सामायिक करा"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"यांच्यासह शेअर करा"</string>
<string name="abc_capital_on" msgid="3405795526292276155">"चालू"</string>
<string name="abc_capital_off" msgid="121134116657445385">"बंद"</string>
<string name="search_menu_title" msgid="146198913615257606">"शोधा"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"मेनू+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"spacebar"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"एंटर करा"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"हटवा"</string>
</resources>
diff --git a/v7/appcompat/res/values-ms/strings.xml b/v7/appcompat/res/values-ms/strings.xml
index 18f84ce..8af4c13 100644
--- a/v7/appcompat/res/values-ms/strings.xml
+++ b/v7/appcompat/res/values-ms/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"HIDUP"</string>
<string name="abc_capital_off" msgid="121134116657445385">"MATI"</string>
<string name="search_menu_title" msgid="146198913615257606">"Cari"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fungsi+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"ruang"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"padam"</string>
</resources>
diff --git a/v7/appcompat/res/values-my/strings.xml b/v7/appcompat/res/values-my/strings.xml
index cbc8791..cfd6625 100644
--- a/v7/appcompat/res/values-my/strings.xml
+++ b/v7/appcompat/res/values-my/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ဖွင့်"</string>
<string name="abc_capital_off" msgid="121134116657445385">"ပိတ်"</string>
<string name="search_menu_title" msgid="146198913615257606">"ရှာဖွေပါ"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-nb/strings.xml b/v7/appcompat/res/values-nb/strings.xml
index 6005234..f9fd80e 100644
--- a/v7/appcompat/res/values-nb/strings.xml
+++ b/v7/appcompat/res/values-nb/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"PÅ"</string>
<string name="abc_capital_off" msgid="121134116657445385">"AV"</string>
<string name="search_menu_title" msgid="146198913615257606">"Søk"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Meny+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Funksjon+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"mellomrom"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-ne/strings.xml b/v7/appcompat/res/values-ne/strings.xml
index 96b1042..0a0de14 100644
--- a/v7/appcompat/res/values-ne/strings.xml
+++ b/v7/appcompat/res/values-ne/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"सक्रिय गर्नुहोस्"</string>
<string name="abc_capital_off" msgid="121134116657445385">"निष्क्रिय पार्नुहोस्"</string>
<string name="search_menu_title" msgid="146198913615257606">"खोज्नुहोस्"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-nl/strings.xml b/v7/appcompat/res/values-nl/strings.xml
index e0d2044..ffcb3d9 100644
--- a/v7/appcompat/res/values-nl/strings.xml
+++ b/v7/appcompat/res/values-nl/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"AAN"</string>
<string name="abc_capital_off" msgid="121134116657445385">"UIT"</string>
<string name="search_menu_title" msgid="146198913615257606">"Zoeken"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu +"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta +"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl +"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt +"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift +"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym +"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Functie +"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"spatie"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"verwijderen"</string>
</resources>
diff --git a/v7/appcompat/res/values-or/strings.xml b/v7/appcompat/res/values-or/strings.xml
index 4dc6eb4..86e4fb7 100644
--- a/v7/appcompat/res/values-or/strings.xml
+++ b/v7/appcompat/res/values-or/strings.xml
@@ -29,13 +29,19 @@
<string name="abc_searchview_description_voice" msgid="893419373245838918">"ଭଏସ୍ ସର୍ଚ୍ଚ"</string>
<string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"ଗୋଟିଏ ଆପ୍ ବାଛନ୍ତୁ"</string>
<string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"ସବୁ ଦେଖନ୍ତୁ"</string>
- <!-- no translation found for abc_shareactionprovider_share_with_application (3300176832234831527) -->
- <skip />
+ <string name="abc_shareactionprovider_share_with_application" msgid="3300176832234831527">"<xliff:g id="APPLICATION_NAME">%s</xliff:g> ସହ ଶେୟାର୍ କରନ୍ତୁ"</string>
<string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"ଏହାଙ୍କ ସହ ଶେୟାର୍ କରନ୍ତୁ"</string>
- <!-- no translation found for abc_capital_on (3405795526292276155) -->
- <skip />
- <!-- no translation found for abc_capital_off (121134116657445385) -->
- <skip />
- <!-- no translation found for search_menu_title (146198913615257606) -->
- <skip />
+ <string name="abc_capital_on" msgid="3405795526292276155">"ଅନ୍"</string>
+ <string name="abc_capital_off" msgid="121134116657445385">"ଅଫ୍"</string>
+ <string name="search_menu_title" msgid="146198913615257606">"ସର୍ଚ୍ଚ କରନ୍ତୁ"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"ମେନୁ"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"ସ୍ପେସ୍"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"ଏଣ୍ଟର୍"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"ଡିଲିଟ୍"</string>
</resources>
diff --git a/v7/appcompat/res/values-pa/strings.xml b/v7/appcompat/res/values-pa/strings.xml
index 7f28ac8..fc1fdba 100644
--- a/v7/appcompat/res/values-pa/strings.xml
+++ b/v7/appcompat/res/values-pa/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ਤੇ"</string>
<string name="abc_capital_off" msgid="121134116657445385">"ਬੰਦ"</string>
<string name="search_menu_title" msgid="146198913615257606">"ਖੋਜੋ"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-pl/strings.xml b/v7/appcompat/res/values-pl/strings.xml
index d706241..af26367 100644
--- a/v7/appcompat/res/values-pl/strings.xml
+++ b/v7/appcompat/res/values-pl/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"WŁ."</string>
<string name="abc_capital_off" msgid="121134116657445385">"WYŁ."</string>
<string name="search_menu_title" msgid="146198913615257606">"Szukaj"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Funkcyjny+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"spacja"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-pt-rBR/strings.xml b/v7/appcompat/res/values-pt-rBR/strings.xml
index 90461ec..17d8593 100644
--- a/v7/appcompat/res/values-pt-rBR/strings.xml
+++ b/v7/appcompat/res/values-pt-rBR/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ATIVAR"</string>
<string name="abc_capital_off" msgid="121134116657445385">"DESATIVAR"</string>
<string name="search_menu_title" msgid="146198913615257606">"Pesquisar"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"espaço"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-pt-rPT/strings.xml b/v7/appcompat/res/values-pt-rPT/strings.xml
index 40f6499..f8c0fa7 100644
--- a/v7/appcompat/res/values-pt-rPT/strings.xml
+++ b/v7/appcompat/res/values-pt-rPT/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ATIVADO"</string>
<string name="abc_capital_off" msgid="121134116657445385">"DESATIVADO"</string>
<string name="search_menu_title" msgid="146198913615257606">"Pesquisar"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu +"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta +"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl +"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt +"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift +"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym +"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Função +"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"espaço"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"eliminar"</string>
</resources>
diff --git a/v7/appcompat/res/values-pt/strings.xml b/v7/appcompat/res/values-pt/strings.xml
index 90461ec..17d8593 100644
--- a/v7/appcompat/res/values-pt/strings.xml
+++ b/v7/appcompat/res/values-pt/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ATIVAR"</string>
<string name="abc_capital_off" msgid="121134116657445385">"DESATIVAR"</string>
<string name="search_menu_title" msgid="146198913615257606">"Pesquisar"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"espaço"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-ro/strings.xml b/v7/appcompat/res/values-ro/strings.xml
index 6d04be9..86495fd 100644
--- a/v7/appcompat/res/values-ro/strings.xml
+++ b/v7/appcompat/res/values-ro/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ACTIVAT"</string>
<string name="abc_capital_off" msgid="121134116657445385">"DEZACTIVAȚI"</string>
<string name="search_menu_title" msgid="146198913615257606">"Căutați"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Meniu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Funcție+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"spațiu"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-ru/strings.xml b/v7/appcompat/res/values-ru/strings.xml
index 2b28958..c505bf5 100644
--- a/v7/appcompat/res/values-ru/strings.xml
+++ b/v7/appcompat/res/values-ru/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ВКЛ."</string>
<string name="abc_capital_off" msgid="121134116657445385">"ОТКЛ."</string>
<string name="search_menu_title" msgid="146198913615257606">"Поиск"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Меню +"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta +"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl +"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt +"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift +"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym +"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fn +"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"Пробел"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Ввод"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-si/strings.xml b/v7/appcompat/res/values-si/strings.xml
index 7631288..c154686 100644
--- a/v7/appcompat/res/values-si/strings.xml
+++ b/v7/appcompat/res/values-si/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ක්රියාත්මකයි"</string>
<string name="abc_capital_off" msgid="121134116657445385">"ක්රියාවිරහිතයි"</string>
<string name="search_menu_title" msgid="146198913615257606">"සොයන්න"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"මකන්න"</string>
</resources>
diff --git a/v7/appcompat/res/values-sk/strings.xml b/v7/appcompat/res/values-sk/strings.xml
index 03faf14..67184b0 100644
--- a/v7/appcompat/res/values-sk/strings.xml
+++ b/v7/appcompat/res/values-sk/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ZAPNUTÉ"</string>
<string name="abc_capital_off" msgid="121134116657445385">"VYPNUTÉ"</string>
<string name="search_menu_title" msgid="146198913615257606">"Vyhľadávanie"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"medzerník"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"odstrániť"</string>
</resources>
diff --git a/v7/appcompat/res/values-sl/strings.xml b/v7/appcompat/res/values-sl/strings.xml
index 22b8bd4..e38e5ea 100644
--- a/v7/appcompat/res/values-sl/strings.xml
+++ b/v7/appcompat/res/values-sl/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"VKLOPLJENO"</string>
<string name="abc_capital_off" msgid="121134116657445385">"IZKLOPLJENO"</string>
<string name="search_menu_title" msgid="146198913615257606">"Iskanje"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Meni +"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta +"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl +"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt +"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift +"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym +"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fn +"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"preslednica"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-sq/strings.xml b/v7/appcompat/res/values-sq/strings.xml
index 1a1f02e..8b958a2 100644
--- a/v7/appcompat/res/values-sq/strings.xml
+++ b/v7/appcompat/res/values-sq/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"AKTIV"</string>
<string name="abc_capital_off" msgid="121134116657445385">"JOAKTIV"</string>
<string name="search_menu_title" msgid="146198913615257606">"Kërko"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menyja+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Funksioni+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"hapësirë"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-sr/strings.xml b/v7/appcompat/res/values-sr/strings.xml
index 5678341..4c702bc 100644
--- a/v7/appcompat/res/values-sr/strings.xml
+++ b/v7/appcompat/res/values-sr/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"УКЉУЧИ"</string>
<string name="abc_capital_off" msgid="121134116657445385">"ИСКЉУЧИ"</string>
<string name="search_menu_title" msgid="146198913615257606">"Претражи"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"тастер за размак"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-sv/strings.xml b/v7/appcompat/res/values-sv/strings.xml
index 62d470f..72b7f88 100644
--- a/v7/appcompat/res/values-sv/strings.xml
+++ b/v7/appcompat/res/values-sv/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"PÅ"</string>
<string name="abc_capital_off" msgid="121134116657445385">"AV"</string>
<string name="search_menu_title" msgid="146198913615257606">"Sök"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Meny + "</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta + "</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl + "</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt + "</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Skift + "</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Symbol + "</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Funktion + "</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"blanksteg"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"retur"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-sw/strings.xml b/v7/appcompat/res/values-sw/strings.xml
index c575ae0..1c623b7 100644
--- a/v7/appcompat/res/values-sw/strings.xml
+++ b/v7/appcompat/res/values-sw/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"IMEWASHWA"</string>
<string name="abc_capital_off" msgid="121134116657445385">"IMEZIMWA"</string>
<string name="search_menu_title" msgid="146198913615257606">"Tafuta"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menyu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"nafasi"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"futa"</string>
</resources>
diff --git a/v7/appcompat/res/values-ta/strings.xml b/v7/appcompat/res/values-ta/strings.xml
index 971a3db..7aa031a 100644
--- a/v7/appcompat/res/values-ta/strings.xml
+++ b/v7/appcompat/res/values-ta/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ஆன்"</string>
<string name="abc_capital_off" msgid="121134116657445385">"ஆஃப்"</string>
<string name="search_menu_title" msgid="146198913615257606">"தேடு"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"மெனு மற்றும்"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"மெட்டா மற்றும்"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"கண்ட்ரோல் மற்றும்"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"ஆல்ட் மற்றும்"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"ஷிஃப்ட் மற்றும்"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"சிம்பல் மற்றும்"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"ஃபங்ஷன் மற்றும்"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"ஸ்பேஸ்"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"எண்டர்"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"டெலிட்"</string>
</resources>
diff --git a/v7/appcompat/res/values-te/strings.xml b/v7/appcompat/res/values-te/strings.xml
index f7d7577..818f0e3 100644
--- a/v7/appcompat/res/values-te/strings.xml
+++ b/v7/appcompat/res/values-te/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"ఆన్ చేయి"</string>
<string name="abc_capital_off" msgid="121134116657445385">"ఆఫ్ చేయి"</string>
<string name="search_menu_title" msgid="146198913615257606">"వెతుకు"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"స్పేస్"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-th/strings.xml b/v7/appcompat/res/values-th/strings.xml
index f8ea1cd..5ecec86 100644
--- a/v7/appcompat/res/values-th/strings.xml
+++ b/v7/appcompat/res/values-th/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"เปิด"</string>
<string name="abc_capital_off" msgid="121134116657445385">"ปิด"</string>
<string name="search_menu_title" msgid="146198913615257606">"ค้นหา"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"เมนู+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"Space"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-tl/strings.xml b/v7/appcompat/res/values-tl/strings.xml
index 1ad2689..3ef5df6 100644
--- a/v7/appcompat/res/values-tl/strings.xml
+++ b/v7/appcompat/res/values-tl/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"I-ON"</string>
<string name="abc_capital_off" msgid="121134116657445385">"I-OFF"</string>
<string name="search_menu_title" msgid="146198913615257606">"Maghanap"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-tr/strings.xml b/v7/appcompat/res/values-tr/strings.xml
index fae41d3..153e2c2 100644
--- a/v7/appcompat/res/values-tr/strings.xml
+++ b/v7/appcompat/res/values-tr/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"AÇ"</string>
<string name="abc_capital_off" msgid="121134116657445385">"KAPAT"</string>
<string name="search_menu_title" msgid="146198913615257606">"Ara"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menü+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Üst Karakter+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"İşlev+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"boşluk"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"sil"</string>
</resources>
diff --git a/v7/appcompat/res/values-uk/strings.xml b/v7/appcompat/res/values-uk/strings.xml
index afc74ff..7aed219 100644
--- a/v7/appcompat/res/values-uk/strings.xml
+++ b/v7/appcompat/res/values-uk/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"УВІМК."</string>
<string name="abc_capital_off" msgid="121134116657445385">"ВИМК."</string>
<string name="search_menu_title" msgid="146198913615257606">"Пошук"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"пробіл"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-ur/strings.xml b/v7/appcompat/res/values-ur/strings.xml
index 60ec34a..997d594 100644
--- a/v7/appcompat/res/values-ur/strings.xml
+++ b/v7/appcompat/res/values-ur/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"آن"</string>
<string name="abc_capital_off" msgid="121134116657445385">"آف"</string>
<string name="search_menu_title" msgid="146198913615257606">"تلاش"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-uz/strings.xml b/v7/appcompat/res/values-uz/strings.xml
index 0417cba..f925bd6 100644
--- a/v7/appcompat/res/values-uz/strings.xml
+++ b/v7/appcompat/res/values-uz/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"YONIQ"</string>
<string name="abc_capital_off" msgid="121134116657445385">"O‘CHIQ"</string>
<string name="search_menu_title" msgid="146198913615257606">"Qidirish"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menyu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fn+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"Probel"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-vi/strings.xml b/v7/appcompat/res/values-vi/strings.xml
index 4560b4b..e07efed 100644
--- a/v7/appcompat/res/values-vi/strings.xml
+++ b/v7/appcompat/res/values-vi/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"BẬT"</string>
<string name="abc_capital_off" msgid="121134116657445385">"TẮT"</string>
<string name="search_menu_title" msgid="146198913615257606">"Tìm kiếm"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"phím cách"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"delete"</string>
</resources>
diff --git a/v7/appcompat/res/values-zh-rCN/strings.xml b/v7/appcompat/res/values-zh-rCN/strings.xml
index 7b23457..6dc1ea2 100644
--- a/v7/appcompat/res/values-zh-rCN/strings.xml
+++ b/v7/appcompat/res/values-zh-rCN/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"开启"</string>
<string name="abc_capital_off" msgid="121134116657445385">"关闭"</string>
<string name="search_menu_title" msgid="146198913615257606">"搜索"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fn+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"空格键"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter 键"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Delete 键"</string>
</resources>
diff --git a/v7/appcompat/res/values-zh-rHK/strings.xml b/v7/appcompat/res/values-zh-rHK/strings.xml
index fc32117..ce753fd 100644
--- a/v7/appcompat/res/values-zh-rHK/strings.xml
+++ b/v7/appcompat/res/values-zh-rHK/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"開啟"</string>
<string name="abc_capital_off" msgid="121134116657445385">"關閉"</string>
<string name="search_menu_title" msgid="146198913615257606">"搜尋"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu +"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta +"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl +"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt +"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift +"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym +"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fn +"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"空白鍵"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter 鍵"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"刪除"</string>
</resources>
diff --git a/v7/appcompat/res/values-zh-rTW/strings.xml b/v7/appcompat/res/values-zh-rTW/strings.xml
index 35be873..0f29b8e 100644
--- a/v7/appcompat/res/values-zh-rTW/strings.xml
+++ b/v7/appcompat/res/values-zh-rTW/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"開啟"</string>
<string name="abc_capital_off" msgid="121134116657445385">"關閉"</string>
<string name="search_menu_title" msgid="146198913615257606">"搜尋"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Menu +"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta +"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl +"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt +"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift +"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym +"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Fn +"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"空格鍵"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"Enter 鍵"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"Delete 鍵"</string>
</resources>
diff --git a/v7/appcompat/res/values-zu/strings.xml b/v7/appcompat/res/values-zu/strings.xml
index e84ba7a..e3032f1 100644
--- a/v7/appcompat/res/values-zu/strings.xml
+++ b/v7/appcompat/res/values-zu/strings.xml
@@ -34,4 +34,14 @@
<string name="abc_capital_on" msgid="3405795526292276155">"VULIWE"</string>
<string name="abc_capital_off" msgid="121134116657445385">"VALIWE"</string>
<string name="search_menu_title" msgid="146198913615257606">"Sesha"</string>
+ <string name="abc_prepend_shortcut_label" msgid="1351762916121158029">"Imenyu+"</string>
+ <string name="abc_menu_meta_shortcut_label" msgid="7643535737296831317">"Meta+"</string>
+ <string name="abc_menu_ctrl_shortcut_label" msgid="1324831542140195728">"Ctrl+"</string>
+ <string name="abc_menu_alt_shortcut_label" msgid="1302280443949172191">"Alt+"</string>
+ <string name="abc_menu_shift_shortcut_label" msgid="8126296154200614004">"Shift+"</string>
+ <string name="abc_menu_sym_shortcut_label" msgid="9002602288060866689">"Sym+"</string>
+ <string name="abc_menu_function_shortcut_label" msgid="4792426091847145555">"Function+"</string>
+ <string name="abc_menu_space_shortcut_label" msgid="2378550843553983978">"space"</string>
+ <string name="abc_menu_enter_shortcut_label" msgid="8341180395196749340">"enter"</string>
+ <string name="abc_menu_delete_shortcut_label" msgid="8362206064229013510">"susa"</string>
</resources>
diff --git a/v7/appcompat/res/values/bools.xml b/v7/appcompat/res/values/bools.xml
index c38c0ee..793719b 100644
--- a/v7/appcompat/res/values/bools.xml
+++ b/v7/appcompat/res/values/bools.xml
@@ -18,8 +18,6 @@
<bool name="abc_action_bar_embed_tabs">true</bool>
- <bool name="abc_config_showMenuShortcutsWhenKeyboardPresent">false</bool>
-
<!-- Whether to allow vertically stacked button bars. This is disabled for
configurations with a small (e.g. less than 320dp) screen height. -->
<bool name="abc_allow_stacked_button_bar">false</bool>
diff --git a/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java b/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java
index 7991c4e..c32beab 100644
--- a/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java
+++ b/v7/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegate.java
@@ -38,6 +38,7 @@
import androidx.annotation.RestrictTo;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.Toolbar;
+import androidx.appcompat.widget.VectorEnabledTintResources;
import androidx.core.view.WindowCompat;
import androidx.fragment.app.FragmentActivity;
@@ -128,8 +129,6 @@
@NightMode
private static int sDefaultNightMode = MODE_NIGHT_FOLLOW_SYSTEM;
- private static boolean sCompatVectorFromResourcesEnabled = false;
-
/** @hide */
@RestrictTo(LIBRARY_GROUP)
@IntDef({MODE_NIGHT_NO, MODE_NIGHT_YES, MODE_NIGHT_AUTO, MODE_NIGHT_FOLLOW_SYSTEM,
@@ -510,7 +509,7 @@
* <p>Please note: this only takes effect in Activities created after this call.</p>
*/
public static void setCompatVectorFromResourcesEnabled(boolean enabled) {
- sCompatVectorFromResourcesEnabled = enabled;
+ VectorEnabledTintResources.setCompatVectorFromResourcesEnabled(enabled);
}
/**
@@ -520,6 +519,6 @@
* @see #setCompatVectorFromResourcesEnabled(boolean)
*/
public static boolean isCompatVectorFromResourcesEnabled() {
- return sCompatVectorFromResourcesEnabled;
+ return VectorEnabledTintResources.isCompatVectorFromResourcesEnabled();
}
}
diff --git a/v7/appcompat/src/main/java/androidx/appcompat/view/menu/MenuBuilder.java b/v7/appcompat/src/main/java/androidx/appcompat/view/menu/MenuBuilder.java
index a1a20e5..2b5e653 100644
--- a/v7/appcompat/src/main/java/androidx/appcompat/view/menu/MenuBuilder.java
+++ b/v7/appcompat/src/main/java/androidx/appcompat/view/menu/MenuBuilder.java
@@ -35,14 +35,15 @@
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
+import android.view.ViewConfiguration;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
-import androidx.appcompat.R;
import androidx.core.content.ContextCompat;
import androidx.core.internal.view.SupportMenu;
import androidx.core.internal.view.SupportMenuItem;
import androidx.core.view.ActionProvider;
+import androidx.core.view.ViewConfigurationCompat;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
@@ -816,7 +817,8 @@
private void setShortcutsVisibleInner(boolean shortcutsVisible) {
mShortcutsVisible = shortcutsVisible
&& mResources.getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS
- && mResources.getBoolean(R.bool.abc_config_showMenuShortcutsWhenKeyboardPresent);
+ && ViewConfigurationCompat.shouldShowMenuShortcutsWhenKeyboardPresent(
+ ViewConfiguration.get(mContext), mContext);
}
/**
diff --git a/v7/appcompat/src/main/java/androidx/appcompat/widget/VectorEnabledTintResources.java b/v7/appcompat/src/main/java/androidx/appcompat/widget/VectorEnabledTintResources.java
index 15fcf88..af61860 100644
--- a/v7/appcompat/src/main/java/androidx/appcompat/widget/VectorEnabledTintResources.java
+++ b/v7/appcompat/src/main/java/androidx/appcompat/widget/VectorEnabledTintResources.java
@@ -25,7 +25,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
-import androidx.appcompat.app.AppCompatDelegate;
import java.lang.ref.WeakReference;
@@ -37,9 +36,10 @@
*/
@RestrictTo(LIBRARY_GROUP)
public class VectorEnabledTintResources extends Resources {
+ private static boolean sCompatVectorFromResourcesEnabled = false;
public static boolean shouldBeUsed() {
- return AppCompatDelegate.isCompatVectorFromResourcesEnabled()
+ return isCompatVectorFromResourcesEnabled()
&& Build.VERSION.SDK_INT <= MAX_SDK_WHERE_REQUIRED;
}
@@ -74,4 +74,22 @@
final Drawable superGetDrawable(int id) {
return super.getDrawable(id);
}
+
+ /**
+ * Sets whether vector drawables on older platforms (< API 21) can be used within
+ * {@link android.graphics.drawable.DrawableContainer} resources.
+ */
+ public static void setCompatVectorFromResourcesEnabled(boolean enabled) {
+ sCompatVectorFromResourcesEnabled = enabled;
+ }
+
+ /**
+ * Returns whether vector drawables on older platforms (< API 21) can be accessed from within
+ * resources.
+ *
+ * @see #setCompatVectorFromResourcesEnabled(boolean)
+ */
+ public static boolean isCompatVectorFromResourcesEnabled() {
+ return sCompatVectorFromResourcesEnabled;
+ }
}
\ No newline at end of file
diff --git a/wear/res/values-as/strings.xml b/wear/res/values-as/strings.xml
new file mode 100644
index 0000000..2a51efa
--- /dev/null
+++ b/wear/res/values-as/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="ws_navigation_drawer_content_description" msgid="7216697245762194759">"নেভিগেশ্বন ড্ৰৱাৰ"</string>
+ <string name="ws_action_drawer_content_description" msgid="1837365417701148489">"কাৰ্য ড্ৰৱাৰ"</string>
+</resources>
diff --git a/wear/res/values-or/strings.xml b/wear/res/values-or/strings.xml
new file mode 100644
index 0000000..53e75b3
--- /dev/null
+++ b/wear/res/values-or/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="ws_navigation_drawer_content_description" msgid="7216697245762194759">"ନେଭିଗେଶନ୍ ପ୍ୟାନେଲ୍"</string>
+ <string name="ws_action_drawer_content_description" msgid="1837365417701148489">"କାର୍ଯ୍ୟକାରୀ ପ୍ୟାନେଲ୍"</string>
+</resources>
diff --git a/webkit-codegen/src/test/java/androidx/webkit/internal/codegen/BoundaryInterfaceTest.java b/webkit-codegen/src/test/java/androidx/webkit/internal/codegen/BoundaryInterfaceTest.java
index 00736a7..ba5a9de 100644
--- a/webkit-codegen/src/test/java/androidx/webkit/internal/codegen/BoundaryInterfaceTest.java
+++ b/webkit-codegen/src/test/java/androidx/webkit/internal/codegen/BoundaryInterfaceTest.java
@@ -18,6 +18,9 @@
import static org.junit.Assert.assertEquals;
+import androidx.webkit.internal.codegen.representations.ClassRepr;
+import androidx.webkit.internal.codegen.representations.MethodRepr;
+
import com.android.tools.lint.LintCoreProjectEnvironment;
import com.intellij.psi.PsiClass;
@@ -27,15 +30,13 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.Arrays;
-import androidx.webkit.internal.codegen.representations.ClassRepr;
-import androidx.webkit.internal.codegen.representations.MethodRepr;
-
@RunWith(JUnit4.class)
public class BoundaryInterfaceTest {
private LintCoreProjectEnvironment mProjectEnv;
@@ -58,10 +59,12 @@
testBoundaryInterfaceGeneration("SingleClassAndMethod");
}
+ @Ignore
@Test public void testWebkitReturnTypeGeneratesInvocationHandler() {
testBoundaryInterfaceGeneration("WebKitTypeAsMethodParameter");
}
+ @Ignore
@Test public void testWebkitMethodParameterTypeGeneratesInvocationHandler() {
testBoundaryInterfaceGeneration("WebKitTypeAsMethodReturn");
}
diff --git a/webkit/api/current.txt b/webkit/api/current.txt
new file mode 100644
index 0000000..e4e9b01
--- /dev/null
+++ b/webkit/api/current.txt
@@ -0,0 +1,118 @@
+package androidx.webkit {
+
+ public abstract class SafeBrowsingResponseCompat {
+ method public abstract void backToSafety(boolean);
+ method public abstract void proceed(boolean);
+ method public abstract void showInterstitial(boolean);
+ }
+
+ public abstract class ServiceWorkerClientCompat {
+ ctor public ServiceWorkerClientCompat();
+ method public abstract android.webkit.WebResourceResponse shouldInterceptRequest(android.webkit.WebResourceRequest);
+ }
+
+ public abstract class ServiceWorkerControllerCompat {
+ method public static androidx.webkit.ServiceWorkerControllerCompat getInstance();
+ method public abstract androidx.webkit.ServiceWorkerWebSettingsCompat getServiceWorkerWebSettings();
+ method public abstract void setServiceWorkerClient(androidx.webkit.ServiceWorkerClientCompat);
+ }
+
+ public abstract class ServiceWorkerWebSettingsCompat {
+ method public abstract boolean getAllowContentAccess();
+ method public abstract boolean getAllowFileAccess();
+ method public abstract boolean getBlockNetworkLoads();
+ method public abstract int getCacheMode();
+ method public abstract void setAllowContentAccess(boolean);
+ method public abstract void setAllowFileAccess(boolean);
+ method public abstract void setBlockNetworkLoads(boolean);
+ method public abstract void setCacheMode(int);
+ }
+
+ public class WebMessageCompat {
+ ctor public WebMessageCompat(java.lang.String);
+ ctor public WebMessageCompat(java.lang.String, androidx.webkit.WebMessagePortCompat[]);
+ method public java.lang.String getData();
+ method public androidx.webkit.WebMessagePortCompat[] getPorts();
+ }
+
+ public abstract class WebMessagePortCompat {
+ method public abstract void close();
+ method public abstract void postMessage(androidx.webkit.WebMessageCompat);
+ method public abstract void setWebMessageCallback(androidx.webkit.WebMessagePortCompat.WebMessageCallbackCompat);
+ method public abstract void setWebMessageCallback(android.os.Handler, androidx.webkit.WebMessagePortCompat.WebMessageCallbackCompat);
+ }
+
+ public static abstract class WebMessagePortCompat.WebMessageCallbackCompat {
+ ctor public WebMessagePortCompat.WebMessageCallbackCompat();
+ method public void onMessage(androidx.webkit.WebMessagePortCompat, androidx.webkit.WebMessageCompat);
+ }
+
+ public abstract class WebResourceErrorCompat {
+ method public abstract java.lang.CharSequence getDescription();
+ method public abstract int getErrorCode();
+ }
+
+ public class WebResourceRequestCompat {
+ method public static boolean isRedirect(android.webkit.WebResourceRequest);
+ }
+
+ public class WebSettingsCompat {
+ method public static int getDisabledActionModeMenuItems(android.webkit.WebSettings);
+ method public static boolean getOffscreenPreRaster(android.webkit.WebSettings);
+ method public static boolean getSafeBrowsingEnabled(android.webkit.WebSettings);
+ method public static void setDisabledActionModeMenuItems(android.webkit.WebSettings, int);
+ method public static void setOffscreenPreRaster(android.webkit.WebSettings, boolean);
+ method public static void setSafeBrowsingEnabled(android.webkit.WebSettings, boolean);
+ }
+
+ public class WebViewClientCompat extends android.webkit.WebViewClient {
+ ctor public WebViewClientCompat();
+ method public final void onReceivedError(android.webkit.WebView, android.webkit.WebResourceRequest, android.webkit.WebResourceError);
+ method public void onReceivedError(android.webkit.WebView, android.webkit.WebResourceRequest, androidx.webkit.WebResourceErrorCompat);
+ method public final void onSafeBrowsingHit(android.webkit.WebView, android.webkit.WebResourceRequest, int, android.webkit.SafeBrowsingResponse);
+ method public void onSafeBrowsingHit(android.webkit.WebView, android.webkit.WebResourceRequest, int, androidx.webkit.SafeBrowsingResponseCompat);
+ }
+
+ public class WebViewCompat {
+ method public static androidx.webkit.WebMessagePortCompat[] createWebMessageChannel(android.webkit.WebView);
+ method public static android.content.pm.PackageInfo getCurrentWebViewPackage(android.content.Context);
+ method public static android.net.Uri getSafeBrowsingPrivacyPolicyUrl();
+ method public static void postVisualStateCallback(android.webkit.WebView, long, androidx.webkit.WebViewCompat.VisualStateCallback);
+ method public static void postWebMessage(android.webkit.WebView, androidx.webkit.WebMessageCompat, android.net.Uri);
+ method public static void setSafeBrowsingWhitelist(java.util.List<java.lang.String>, android.webkit.ValueCallback<java.lang.Boolean>);
+ method public static void startSafeBrowsing(android.content.Context, android.webkit.ValueCallback<java.lang.Boolean>);
+ }
+
+ public static abstract interface WebViewCompat.VisualStateCallback {
+ method public abstract void onComplete(long);
+ }
+
+ public class WebViewFeature {
+ method public static boolean isFeatureSupported(java.lang.String);
+ field public static final java.lang.String DISABLED_ACTION_MODE_MENU_ITEMS = "DISABLED_ACTION_MODE_MENU_ITEMS";
+ field public static final java.lang.String OFF_SCREEN_PRERASTER = "OFF_SCREEN_PRERASTER";
+ field public static final java.lang.String RECEIVE_HTTP_ERROR = "RECEIVE_HTTP_ERROR";
+ field public static final java.lang.String RECEIVE_WEB_RESOURCE_ERROR = "RECEIVE_WEB_RESOURCE_ERROR";
+ field public static final java.lang.String SAFE_BROWSING_ENABLE = "SAFE_BROWSING_ENABLE";
+ field public static final java.lang.String SAFE_BROWSING_HIT = "SAFE_BROWSING_HIT";
+ field public static final java.lang.String SAFE_BROWSING_PRIVACY_POLICY_URL = "SAFE_BROWSING_PRIVACY_POLICY_URL";
+ field public static final java.lang.String SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY = "SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY";
+ field public static final java.lang.String SAFE_BROWSING_RESPONSE_PROCEED = "SAFE_BROWSING_RESPONSE_PROCEED";
+ field public static final java.lang.String SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL = "SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL";
+ field public static final java.lang.String SAFE_BROWSING_WHITELIST = "SAFE_BROWSING_WHITELIST";
+ field public static final java.lang.String SERVICE_WORKER_BASIC_USAGE = "SERVICE_WORKER_BASIC_USAGE";
+ field public static final java.lang.String SERVICE_WORKER_BLOCK_NETWORK_LOADS = "SERVICE_WORKER_BLOCK_NETWORK_LOADS";
+ field public static final java.lang.String SERVICE_WORKER_CACHE_MODE = "SERVICE_WORKER_CACHE_MODE";
+ field public static final java.lang.String SERVICE_WORKER_CONTENT_ACCESS = "SERVICE_WORKER_CONTENT_ACCESS";
+ field public static final java.lang.String SERVICE_WORKER_FILE_ACCESS = "SERVICE_WORKER_FILE_ACCESS";
+ field public static final java.lang.String SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST = "SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST";
+ field public static final java.lang.String SHOULD_OVERRIDE_WITH_REDIRECTS = "SHOULD_OVERRIDE_WITH_REDIRECTS";
+ field public static final java.lang.String START_SAFE_BROWSING = "START_SAFE_BROWSING";
+ field public static final java.lang.String VISUAL_STATE_CALLBACK = "VISUAL_STATE_CALLBACK";
+ field public static final java.lang.String WEB_RESOURCE_ERROR_GET_CODE = "WEB_RESOURCE_ERROR_GET_CODE";
+ field public static final java.lang.String WEB_RESOURCE_ERROR_GET_DESCRIPTION = "WEB_RESOURCE_ERROR_GET_DESCRIPTION";
+ field public static final java.lang.String WEB_RESOURCE_REQUEST_IS_REDIRECT = "WEB_RESOURCE_REQUEST_IS_REDIRECT";
+ }
+
+}
+
diff --git a/webkit/build.gradle b/webkit/build.gradle
index 06f3801..baed9cb 100644
--- a/webkit/build.gradle
+++ b/webkit/build.gradle
@@ -38,14 +38,17 @@
// Allow compiling the WebView support library boundary interfaces from this project.
main.java.srcDirs += new File(webviewBoundaryInterfacesDir, "src").getAbsolutePath()
}
+
+ buildTypes.all {
+ consumerProguardFiles new File(webviewBoundaryInterfacesDir, "proguard.flags") , 'proguard-rules.pro'
+ }
}
supportLibrary {
name = "WebView Support Library"
- publish = false
+ publish = true
mavenVersion = LibraryVersions.SUPPORT_LIBRARY
mavenGroup = LibraryGroups.WEBKIT
inceptionYear = "2017"
description = "The WebView Support Library is a static library you can add to your Android application in order to use android.webkit APIs that are not available for older platform versions."
- minSdkVersion = 21
}
diff --git a/webkit/proguard-rules.pro b/webkit/proguard-rules.pro
new file mode 100644
index 0000000..86756ab
--- /dev/null
+++ b/webkit/proguard-rules.pro
@@ -0,0 +1,16 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Prevent WebViewClientCompat from being renamed, since chromium depends on this name.
+-keep public class androidx.webkit.WebViewClientCompat { public *; }
diff --git a/webkit/src/androidTest/java/androidx/webkit/BoundaryInterfaceTest.java b/webkit/src/androidTest/java/androidx/webkit/BoundaryInterfaceTest.java
new file mode 100644
index 0000000..3f9857c
--- /dev/null
+++ b/webkit/src/androidTest/java/androidx/webkit/BoundaryInterfaceTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.webkit.internal.WebViewFeatureInternal;
+import androidx.webkit.internal.WebViewGlueCommunicator;
+import androidx.webkit.internal.WebkitToCompatConverter;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests related to the interface between the support library and the Chromium support library glue.
+ */
+@RunWith(AndroidJUnit4.class)
+public class BoundaryInterfaceTest {
+
+ /**
+ * Test ensuring that we can create a {@link androidx.webkit.internal.WebkitToCompatConverter}.
+ * This test catches cases where we try to pass post-L android.webkit classes across the
+ * boundary - doing so will fail when we (at run-time) create a {@link java.lang.reflect.Proxy}
+ * for {@link org.chromium.support_lib_boundary.WebkitToCompatConverterBoundaryInterface} since
+ * that proxy will need to look up all the classes referenced from
+ * {@link org.chromium.support_lib_boundary.WebkitToCompatConverterBoundaryInterface}.
+ */
+ @SmallTest
+ @Test
+ public void testCreateWebkitToCompatConverter() {
+ // Use the SERVICE_WORKER_BASIC_USAGE feature as a proxy for knowing whether the current
+ // WebView APK is compatible with the support library.
+ if (WebViewFeatureInternal.SERVICE_WORKER_BASIC_USAGE.isSupportedByWebView()) {
+ WebkitToCompatConverter converter = WebViewGlueCommunicator.getCompatConverter();
+ }
+ }
+
+}
diff --git a/webkit/src/androidTest/java/androidx/webkit/IncompatibilityTest.java b/webkit/src/androidTest/java/androidx/webkit/IncompatibilityTest.java
new file mode 100644
index 0000000..09424c6
--- /dev/null
+++ b/webkit/src/androidTest/java/androidx/webkit/IncompatibilityTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import android.support.test.filters.MediumTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.webkit.internal.WebViewFeatureInternal;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests ensuring that Android versions/setups that are incompatible with the WebView Support
+ * Library are handled gracefully.
+ *
+ * Only L+ Android versions are compatible with the WebView Support Library, so any tests in this
+ * class that guarantee certain behaviour for incompatible Android versions will only be run on
+ * pre-L devices.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class IncompatibilityTest {
+ @Test
+ @SdkSuppress(maxSdkVersion = 20)
+ public void testPreLDeviceHasNoWebViewFeatures() {
+ assertEquals(0, WebViewFeatureInternal.getWebViewApkFeaturesForTesting().length);
+ }
+
+ @Test
+ @SdkSuppress(maxSdkVersion = 20)
+ public void testPreLDeviceDoesNotSupportVisualStateCallback() {
+ assertFalse(WebViewFeature.isFeatureSupported(WebViewFeature.VISUAL_STATE_CALLBACK));
+ }
+}
diff --git a/webkit/src/androidTest/java/androidx/webkit/PollingCheck.java b/webkit/src/androidTest/java/androidx/webkit/PollingCheck.java
new file mode 100644
index 0000000..4c59a3e
--- /dev/null
+++ b/webkit/src/androidTest/java/androidx/webkit/PollingCheck.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import junit.framework.Assert;
+
+import java.util.concurrent.Callable;
+
+/**
+ * A class for checking a specific statement {@link #check()} through polling, either until the
+ * statement is true, or until timing out.
+ *
+ * Copy-pasted from CTS: com.android.compatibility.common.util.PollingCheck.
+ */
+public abstract class PollingCheck {
+ private static final long TIME_SLICE = 50;
+ private long mTimeout = 3000;
+
+ public PollingCheck(long timeout) {
+ mTimeout = timeout;
+ }
+
+ protected abstract boolean check();
+
+ public void run() {
+ if (check()) {
+ return;
+ }
+
+ long timeout = mTimeout;
+ while (timeout > 0) {
+ try {
+ Thread.sleep(TIME_SLICE);
+ } catch (InterruptedException e) {
+ Assert.fail("unexpected InterruptedException");
+ }
+
+ if (check()) {
+ return;
+ }
+
+ timeout -= TIME_SLICE;
+ }
+
+ Assert.fail("unexpected timeout");
+ }
+
+ public static void check(CharSequence message, long timeout, Callable<Boolean> condition)
+ throws Exception {
+ while (timeout > 0) {
+ if (condition.call()) {
+ return;
+ }
+
+ Thread.sleep(TIME_SLICE);
+ timeout -= TIME_SLICE;
+ }
+
+ Assert.fail(message.toString());
+ }
+}
diff --git a/webkit/src/androidTest/java/androidx/webkit/PostMessageTest.java b/webkit/src/androidTest/java/androidx/webkit/PostMessageTest.java
new file mode 100644
index 0000000..4e74419
--- /dev/null
+++ b/webkit/src/androidTest/java/androidx/webkit/PostMessageTest.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import static junit.framework.Assert.assertEquals;
+
+import android.net.Uri;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.runner.AndroidJUnit4;
+import android.webkit.WebView;
+
+import androidx.webkit.WebMessagePortCompat.WebMessageCallbackCompat;
+
+import junit.framework.Assert;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class PostMessageTest {
+ public static final long TIMEOUT = 6000L;
+
+ private WebView mWebView;
+ private WebViewOnUiThread mOnUiThread;
+
+ private static final String WEBVIEW_MESSAGE = "from_webview";
+ private static final String BASE_URI = "http://www.example.com";
+
+ @Before
+ public void setUp() throws Exception {
+ mOnUiThread = new WebViewOnUiThread();
+ mOnUiThread.getSettings().setJavaScriptEnabled(true);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (mOnUiThread != null) {
+ mOnUiThread.cleanUp();
+ }
+ }
+
+ private static final String TITLE_FROM_POST_MESSAGE =
+ "<!DOCTYPE html><html><body>"
+ + " <script>"
+ + " var received = '';"
+ + " onmessage = function (e) {"
+ + " received += e.data;"
+ + " document.title = received; };"
+ + " </script>"
+ + "</body></html>";
+
+ // Acks each received message from the message channel with a seq number.
+ private static final String CHANNEL_MESSAGE =
+ "<!DOCTYPE html><html><body>"
+ + " <script>"
+ + " var counter = 0;"
+ + " onmessage = function (e) {"
+ + " var myPort = e.ports[0];"
+ + " myPort.onmessage = function (f) {"
+ + " myPort.postMessage(f.data + counter++);"
+ + " }"
+ + " }"
+ + " </script>"
+ + "</body></html>";
+
+ private void loadPage(String data) {
+ mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(BASE_URI, data,
+ "text/html", "UTF-8", null);
+ }
+
+ private void waitForTitle(final String title) {
+ new PollingCheck(TIMEOUT) {
+ @Override
+ protected boolean check() {
+ return mOnUiThread.getTitle().equals(title);
+ }
+ }.run();
+ }
+
+ // Post a string message to main frame and make sure it is received.
+ @Test
+ @SdkSuppress(minSdkVersion = 23) // TODO(gsennton) activate this test for pre-M devices when we
+ // can pre-install a WebView APK containing support for the WebView Support Library, see
+ // b/73454652.
+ public void testSimpleMessageToMainFrame() throws Throwable {
+ verifyPostMessageToOrigin(Uri.parse(BASE_URI));
+ }
+
+ // Post a string message to main frame passing a wildcard as target origin
+ @Test
+ @SdkSuppress(minSdkVersion = 23) // TODO (gsennton) remove this restriction when we can
+ // pre-install a WebView APK containing support for the WebView Support Library, see b/73454652.
+ public void testWildcardOriginMatchesAnything() throws Throwable {
+ verifyPostMessageToOrigin(Uri.parse("*"));
+ }
+
+ // Post a string message to main frame passing an empty string as target origin
+ @Test
+ @SdkSuppress(minSdkVersion = 23) // TODO(gsennton) activate this test for pre-M devices when we
+ // can pre-install a WebView APK containing support for the WebView Support Library, see
+ // b/73454652.
+ public void testEmptyStringOriginMatchesAnything() throws Throwable {
+ verifyPostMessageToOrigin(Uri.parse(""));
+ }
+
+ private void verifyPostMessageToOrigin(Uri origin) throws Throwable {
+ loadPage(TITLE_FROM_POST_MESSAGE);
+ WebMessageCompat message = new WebMessageCompat(WEBVIEW_MESSAGE);
+ mOnUiThread.postWebMessageCompat(message, origin);
+ waitForTitle(WEBVIEW_MESSAGE);
+ }
+
+ // Post multiple messages to main frame and make sure they are received in
+ // correct order.
+ @Test
+ @SdkSuppress(minSdkVersion = 23) // TODO(gsennton) activate this test for pre-M devices when we
+ // can pre-install a WebView APK containing support for the WebView Support Library, see
+ // b/73454652.
+ public void testMultipleMessagesToMainFrame() throws Throwable {
+ loadPage(TITLE_FROM_POST_MESSAGE);
+ for (int i = 0; i < 10; i++) {
+ mOnUiThread.postWebMessageCompat(new WebMessageCompat(Integer.toString(i)),
+ Uri.parse(BASE_URI));
+ }
+ waitForTitle("0123456789");
+ }
+
+ // Create a message channel and make sure it can be used for data transfer to/from js.
+ @Test
+ @SdkSuppress(minSdkVersion = 23) // TODO(gsennton) activate this test for pre-M devices when we
+ // can pre-install a WebView APK containing support for the WebView Support Library, see
+ // b/73454652.
+ public void testMessageChannel() throws Throwable {
+ loadPage(CHANNEL_MESSAGE);
+ final WebMessagePortCompat[] channel = mOnUiThread.createWebMessageChannelCompat();
+ WebMessageCompat message =
+ new WebMessageCompat(WEBVIEW_MESSAGE, new WebMessagePortCompat[]{channel[1]});
+ mOnUiThread.postWebMessageCompat(message, Uri.parse(BASE_URI));
+ final int messageCount = 3;
+ final CountDownLatch latch = new CountDownLatch(messageCount);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ for (int i = 0; i < messageCount; i++) {
+ channel[0].postMessage(new WebMessageCompat(WEBVIEW_MESSAGE + i));
+ }
+ channel[0].setWebMessageCallback(new WebMessageCallbackCompat() {
+ @Override
+ public void onMessage(WebMessagePortCompat port, WebMessageCompat message) {
+ int i = messageCount - (int) latch.getCount();
+ assertEquals(WEBVIEW_MESSAGE + i + i, message.getData());
+ latch.countDown();
+ }
+ });
+ }
+ });
+ // Wait for all the responses to arrive.
+ boolean ignore = latch.await(TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS);
+ }
+
+ // Test that a message port that is closed cannot used to send a message
+ @Test
+ @SdkSuppress(minSdkVersion = 23) // TODO(gsennton) activate this test for pre-M devices when we
+ // can pre-install a WebView APK containing support for the WebView Support Library, see
+ // b/73454652.
+ public void testClose() throws Throwable {
+ loadPage(CHANNEL_MESSAGE);
+ final WebMessagePortCompat[] channel = mOnUiThread.createWebMessageChannelCompat();
+ WebMessageCompat message =
+ new WebMessageCompat(WEBVIEW_MESSAGE, new WebMessagePortCompat[]{channel[1]});
+ mOnUiThread.postWebMessageCompat(message, Uri.parse(BASE_URI));
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ channel[0].close();
+ channel[0].postMessage(new WebMessageCompat(WEBVIEW_MESSAGE));
+ } catch (IllegalStateException ex) {
+ // expect to receive an exception
+ return;
+ }
+ Assert.fail("A closed port cannot be used to transfer messages");
+ }
+ });
+ }
+
+ // Sends a new message channel from JS to Java.
+ private static final String CHANNEL_FROM_JS =
+ "<!DOCTYPE html><html><body>"
+ + " <script>"
+ + " var counter = 0;"
+ + " var mc = new MessageChannel();"
+ + " var received = '';"
+ + " mc.port1.onmessage = function (e) {"
+ + " received = e.data;"
+ + " document.title = e.data;"
+ + " };"
+ + " onmessage = function (e) {"
+ + " var myPort = e.ports[0];"
+ + " myPort.postMessage('', [mc.port2]);"
+ + " };"
+ + " </script>"
+ + "</body></html>";
+
+ // Test a message port created in JS can be received and used for message transfer.
+ @Test
+ @SdkSuppress(minSdkVersion = 23) // TODO(gsennton) activate this test for pre-M devices when we
+ // can pre-install a WebView APK containing support for the WebView Support Library, see
+ // b/73454652.
+ public void testReceiveMessagePort() throws Throwable {
+ final String hello = "HELLO";
+ loadPage(CHANNEL_FROM_JS);
+ final WebMessagePortCompat[] channel = mOnUiThread.createWebMessageChannelCompat();
+ WebMessageCompat message =
+ new WebMessageCompat(WEBVIEW_MESSAGE, new WebMessagePortCompat[]{channel[1]});
+ mOnUiThread.postWebMessageCompat(message, Uri.parse(BASE_URI));
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ channel[0].setWebMessageCallback(new WebMessageCallbackCompat() {
+ @Override
+ public void onMessage(WebMessagePortCompat port, WebMessageCompat message) {
+ message.getPorts()[0].postMessage(new WebMessageCompat(hello));
+ }
+ });
+ }
+ });
+ waitForTitle(hello);
+ }
+}
diff --git a/webkit/src/androidTest/java/androidx/webkit/ServiceWorkerClientCompatTest.java b/webkit/src/androidTest/java/androidx/webkit/ServiceWorkerClientCompatTest.java
new file mode 100644
index 0000000..d3a55ae
--- /dev/null
+++ b/webkit/src/androidTest/java/androidx/webkit/ServiceWorkerClientCompatTest.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
+
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.webkit.JavascriptInterface;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+import android.webkit.WebView;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class ServiceWorkerClientCompatTest {
+
+ // The BASE_URL does not matter since the tests will intercept the load, but it should be https
+ // for the Service Worker registration to succeed.
+ private static final String BASE_URL = "https://www.example.com/";
+ private static final String INDEX_URL = BASE_URL + "index.html";
+ private static final String SW_URL = BASE_URL + "sw.js";
+ private static final String FETCH_URL = BASE_URL + "fetch.html";
+
+ private static final String JS_INTERFACE_NAME = "Android";
+ private static final int POLLING_TIMEOUT = 10 * 1000;
+
+ // static HTML page always injected instead of the url loaded.
+ private static final String INDEX_RAW_HTML =
+ "<!DOCTYPE html>\n"
+ + "<html>\n"
+ + " <body>\n"
+ + " <script>\n"
+ + " navigator.serviceWorker.register('sw.js').then(function(reg) {\n"
+ + " " + JS_INTERFACE_NAME + ".registrationSuccess();\n"
+ + " }).catch(function(err) {\n"
+ + " console.error(err);\n"
+ + " });\n"
+ + " </script>\n"
+ + " </body>\n"
+ + "</html>\n";
+ private static final String SW_RAW_HTML = "fetch('fetch.html');";
+ private static final String SW_UNREGISTER_RAW_JS =
+ "navigator.serviceWorker.getRegistration().then(function(r) {"
+ + " r.unregister().then(function(success) {"
+ + " if (success) " + JS_INTERFACE_NAME + ".unregisterSuccess();"
+ + " else console.error('unregister() was not successful');"
+ + " });"
+ + "}).catch(function(err) {"
+ + " console.error(err);"
+ + "});";
+
+ private JavascriptStatusReceiver mJavascriptStatusReceiver;
+ private WebViewOnUiThread mOnUiThread;
+
+ // Both this test and WebViewOnUiThread need to override some of the methods on WebViewClient,
+ // so this test subclasses the WebViewClient from WebViewOnUiThread.
+ private static class InterceptClient extends WebViewOnUiThread.WaitForLoadedClient {
+
+ InterceptClient(WebViewOnUiThread webViewOnUiThread) throws Exception {
+ super(webViewOnUiThread);
+ }
+
+ @Override
+ public WebResourceResponse shouldInterceptRequest(WebView view,
+ WebResourceRequest request) {
+ // Only return content for INDEX_URL, deny all other requests.
+ try {
+ if (request.getUrl().toString().equals(INDEX_URL)) {
+ return new WebResourceResponse("text/html", "utf-8",
+ new ByteArrayInputStream(INDEX_RAW_HTML.getBytes("UTF-8")));
+ }
+ } catch (java.io.UnsupportedEncodingException e) { }
+ return new WebResourceResponse("text/html", "UTF-8", null);
+ }
+ }
+
+ public static class InterceptServiceWorkerClient extends ServiceWorkerClientCompat {
+ private List<WebResourceRequest> mInterceptedRequests = new ArrayList<WebResourceRequest>();
+
+ @Override
+ public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) {
+ // Records intercepted requests and only return content for SW_URL.
+ mInterceptedRequests.add(request);
+ try {
+ if (request.getUrl().toString().equals(SW_URL)) {
+ return new WebResourceResponse("application/javascript", "utf-8",
+ new ByteArrayInputStream(SW_RAW_HTML.getBytes("UTF-8")));
+ }
+ } catch (java.io.UnsupportedEncodingException e) { }
+ return new WebResourceResponse("text/html", "UTF-8", null);
+ }
+
+ List<WebResourceRequest> getInterceptedRequests() {
+ return mInterceptedRequests;
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ mOnUiThread = new WebViewOnUiThread();
+ mOnUiThread.getSettings().setJavaScriptEnabled(true);
+
+ mJavascriptStatusReceiver = new JavascriptStatusReceiver();
+ mOnUiThread.addJavascriptInterface(mJavascriptStatusReceiver, JS_INTERFACE_NAME);
+ mOnUiThread.setWebViewClient(new InterceptClient(mOnUiThread));
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (mOnUiThread != null) {
+ mOnUiThread.cleanUp();
+ }
+ }
+
+ // Test correct invocation of shouldInterceptRequest for Service Workers.
+ @Test
+ public void testServiceWorkerClientInterceptCallback() throws Exception {
+ assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.SERVICE_WORKER_BASIC_USAGE));
+ assumeTrue(WebViewFeature.isFeatureSupported(
+ WebViewFeature.SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST));
+
+ final InterceptServiceWorkerClient mInterceptServiceWorkerClient =
+ new InterceptServiceWorkerClient();
+ ServiceWorkerControllerCompat swController = ServiceWorkerControllerCompat.getInstance();
+ swController.setServiceWorkerClient(mInterceptServiceWorkerClient);
+
+ mOnUiThread.loadUrlAndWaitForCompletion(INDEX_URL);
+
+ Callable<Boolean> registrationSuccess = new Callable<Boolean>() {
+ @Override
+ public Boolean call() {
+ return mJavascriptStatusReceiver.mRegistrationSuccess;
+ }
+ };
+ PollingCheck.check("JS could not register Service Worker", POLLING_TIMEOUT,
+ registrationSuccess);
+
+ Callable<Boolean> receivedRequest = new Callable<Boolean>() {
+ @Override
+ public Boolean call() {
+ return mInterceptServiceWorkerClient.getInterceptedRequests().size() >= 2;
+ }
+ };
+ PollingCheck.check("Service Worker intercept callbacks not invoked", POLLING_TIMEOUT,
+ receivedRequest);
+
+ List<WebResourceRequest> requests = mInterceptServiceWorkerClient.getInterceptedRequests();
+ assertEquals(2, requests.size());
+ assertEquals(SW_URL, requests.get(0).getUrl().toString());
+ assertEquals(FETCH_URL, requests.get(1).getUrl().toString());
+
+ // Clean-up, make sure to unregister the Service Worker.
+ mOnUiThread.evaluateJavascript(SW_UNREGISTER_RAW_JS, null);
+ Callable<Boolean> unregisterSuccess = new Callable<Boolean>() {
+ @Override
+ public Boolean call() {
+ return mJavascriptStatusReceiver.mUnregisterSuccess;
+ }
+ };
+ PollingCheck.check("JS could not unregister Service Worker", POLLING_TIMEOUT,
+ unregisterSuccess);
+ }
+
+ // Object added to the page via AddJavascriptInterface() that is used by the test Javascript to
+ // notify back to Java if the Service Worker registration was successful.
+ public static final class JavascriptStatusReceiver {
+ public volatile boolean mRegistrationSuccess = false;
+ public volatile boolean mUnregisterSuccess = false;
+
+ @JavascriptInterface
+ public void registrationSuccess() {
+ mRegistrationSuccess = true;
+ }
+
+ @JavascriptInterface
+ public void unregisterSuccess() {
+ mUnregisterSuccess = true;
+ }
+ }
+}
diff --git a/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatTest.java b/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatTest.java
index 1e5c152..5dd9b54 100644
--- a/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatTest.java
+++ b/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatTest.java
@@ -19,8 +19,8 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
-import android.os.Build;
import android.support.test.filters.MediumTest;
import android.support.test.runner.AndroidJUnit4;
import android.webkit.WebSettings;
@@ -41,9 +41,7 @@
@Test
public void testOffscreenPreRaster() {
- // TODO(gsennton) activate this test for pre-M devices when we can pre-install a WebView APK
- // containing support for the WebView Support Library, see b/73454652.
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return;
+ assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.OFF_SCREEN_PRERASTER));
assertFalse(WebSettingsCompat.getOffscreenPreRaster(mWebViewOnUiThread.getSettings()));
@@ -53,9 +51,7 @@
@Test
public void testEnableSafeBrowsing() throws Throwable {
- // TODO(gsennton) activate this test for old devices when we can pre-install a WebView APK
- // containing support for the WebView Support Library, see b/73454652.
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
+ assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ENABLE));
WebSettingsCompat.setSafeBrowsingEnabled(mWebViewOnUiThread.getSettings(), false);
assertFalse(WebSettingsCompat.getSafeBrowsingEnabled(mWebViewOnUiThread.getSettings()));
@@ -63,9 +59,8 @@
@Test
public void testDisabledActionModeMenuItems() throws Throwable {
- // TODO(gsennton) activate this test for old devices when we can pre-install a WebView APK
- // containing support for the WebView Support Library, see b/73454652.
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return;
+ assumeTrue(
+ WebViewFeature.isFeatureSupported(WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS));
assertEquals(WebSettings.MENU_ITEM_NONE,
WebSettingsCompat.getDisabledActionModeMenuItems(mWebViewOnUiThread.getSettings()));
diff --git a/webkit/src/androidTest/java/androidx/webkit/WebViewClientCompatTest.java b/webkit/src/androidTest/java/androidx/webkit/WebViewClientCompatTest.java
new file mode 100644
index 0000000..1344f4b
--- /dev/null
+++ b/webkit/src/androidTest/java/androidx/webkit/WebViewClientCompatTest.java
@@ -0,0 +1,393 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.webkit.ValueCallback;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class WebViewClientCompatTest {
+ private WebViewOnUiThread mWebViewOnUiThread;
+
+ private static final long TEST_TIMEOUT = 20000L;
+ private static final String TEST_URL = "http://www.example.com/";
+ private static final String TEST_SAFE_BROWSING_URL =
+ "chrome://safe-browsing/match?type=malware";
+
+ @Before
+ public void setUp() {
+ mWebViewOnUiThread = new WebViewOnUiThread();
+ }
+
+ @Test
+ public void testShouldOverrideUrlLoadingDefault() {
+ // This never calls into chromium, so we don't need to do any feature checks.
+
+ final MockWebViewClient webViewClient = new MockWebViewClient();
+
+ // Create any valid WebResourceRequest, the return values don't matter much.
+ final WebResourceRequest resourceRequest = new WebResourceRequest() {
+ @Override
+ public Uri getUrl() {
+ return Uri.parse(TEST_URL);
+ }
+
+ @Override
+ public boolean isForMainFrame() {
+ return false;
+ }
+
+ @Override
+ public boolean isRedirect() {
+ return false;
+ }
+
+ @Override
+ public boolean hasGesture() {
+ return false;
+ }
+
+ @Override
+ public String getMethod() {
+ return "GET";
+ }
+
+ @Override
+ public Map<String, String> getRequestHeaders() {
+ return new HashMap<String, String>();
+ }
+ };
+
+ Assert.assertFalse(webViewClient.shouldOverrideUrlLoading(
+ mWebViewOnUiThread.getWebViewOnCurrentThread(), resourceRequest));
+ }
+
+ @Test
+ public void testShouldOverrideUrlLoading() throws InterruptedException {
+ Assume.assumeTrue(
+ WebViewFeature.isFeatureSupported(WebViewFeature.SHOULD_OVERRIDE_WITH_REDIRECTS));
+ Assume.assumeTrue(
+ WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_REQUEST_IS_REDIRECT));
+
+ String data = "<html><body>"
+ + "<a href=\"" + TEST_URL + "\" id=\"link\">new page</a>"
+ + "</body></html>";
+ mWebViewOnUiThread.loadDataAndWaitForCompletion(data, "text/html", null);
+ final CountDownLatch pageFinishedLatch = new CountDownLatch(1);
+ final MockWebViewClient webViewClient = new MockWebViewClient() {
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ super.onPageFinished(view, url);
+ pageFinishedLatch.countDown();
+ }
+ };
+ mWebViewOnUiThread.setWebViewClient(webViewClient);
+ mWebViewOnUiThread.getSettings().setJavaScriptEnabled(true);
+ clickOnLinkUsingJs("link", mWebViewOnUiThread);
+ pageFinishedLatch.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
+ Assert.assertEquals(TEST_URL,
+ webViewClient.getLastShouldOverrideResourceRequest().getUrl().toString());
+
+ WebResourceRequest request = webViewClient.getLastShouldOverrideResourceRequest();
+ Assert.assertNotNull(request);
+ Assert.assertTrue(request.isForMainFrame());
+ Assert.assertFalse(WebResourceRequestCompat.isRedirect(request));
+ Assert.assertFalse(request.hasGesture());
+ }
+
+ private void clickOnLinkUsingJs(final String linkId, WebViewOnUiThread webViewOnUiThread)
+ throws InterruptedException {
+ final CountDownLatch callbackLatch = new CountDownLatch(1);
+ ValueCallback<String> callback = new ValueCallback<String>() {
+ @Override
+ public void onReceiveValue(String value) {
+ callbackLatch.countDown();
+ }
+ };
+ webViewOnUiThread.evaluateJavascript(
+ "document.getElementById('" + linkId + "').click();"
+ + "console.log('element with id [" + linkId + "] clicked');", callback);
+ Assert.assertTrue(callbackLatch.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testOnReceivedError() throws Exception {
+ Assume.assumeTrue(
+ WebViewFeature.isFeatureSupported(WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR));
+ Assume.assumeTrue(
+ WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE));
+
+ final MockWebViewClient webViewClient = new MockWebViewClient();
+ mWebViewOnUiThread.setWebViewClient(webViewClient);
+
+ String wrongUri = "invalidscheme://some/resource";
+ Assert.assertNull(webViewClient.getOnReceivedResourceError());
+ mWebViewOnUiThread.loadUrlAndWaitForCompletion(wrongUri);
+ Assert.assertNotNull(webViewClient.getOnReceivedResourceError());
+ Assert.assertEquals(WebViewClient.ERROR_UNSUPPORTED_SCHEME,
+ webViewClient.getOnReceivedResourceError().getErrorCode());
+ }
+
+ @Test
+ public void testOnReceivedErrorForSubresource() throws Exception {
+ Assume.assumeTrue(
+ WebViewFeature.isFeatureSupported(WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR));
+
+ final MockWebViewClient webViewClient = new MockWebViewClient();
+ mWebViewOnUiThread.setWebViewClient(webViewClient);
+
+ Assert.assertNull(webViewClient.getOnReceivedResourceError());
+ String data = "<html>"
+ + " <body>"
+ + " <img src=\"invalidscheme://some/resource\" />"
+ + " </body>"
+ + "</html>";
+
+ mWebViewOnUiThread.loadDataAndWaitForCompletion(data, "text/html", null);
+ Assert.assertNotNull(webViewClient.getOnReceivedResourceError());
+ Assert.assertEquals(WebViewClient.ERROR_UNSUPPORTED_SCHEME,
+ webViewClient.getOnReceivedResourceError().getErrorCode());
+ }
+
+ @Test
+ public void testOnSafeBrowsingHitBackToSafety() throws Throwable {
+ Assume.assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_HIT));
+ Assume.assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ENABLE));
+ Assume.assumeTrue(WebViewFeature.isFeatureSupported(
+ WebViewFeature.SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY));
+ Assume.assumeTrue(
+ WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE));
+
+ final SafeBrowsingBackToSafetyClient backToSafetyWebViewClient =
+ new SafeBrowsingBackToSafetyClient();
+ mWebViewOnUiThread.setWebViewClient(backToSafetyWebViewClient);
+ WebSettingsCompat.setSafeBrowsingEnabled(mWebViewOnUiThread.getSettings(), true);
+
+ // Load any page
+ String data = "<html><body>some safe page</body></html>";
+ mWebViewOnUiThread.loadDataAndWaitForCompletion(data, "text/html", null);
+ final String originalUrl = mWebViewOnUiThread.getUrl();
+
+ enableSafeBrowsingAndLoadUnsafePage(backToSafetyWebViewClient);
+
+ // Back to safety should produce a network error
+ Assert.assertNotNull(backToSafetyWebViewClient.getOnReceivedResourceError());
+ Assert.assertEquals(WebViewClient.ERROR_UNSAFE_RESOURCE,
+ backToSafetyWebViewClient.getOnReceivedResourceError().getErrorCode());
+
+ // Check that we actually navigated backward
+ Assert.assertEquals(originalUrl, mWebViewOnUiThread.getUrl());
+ }
+
+ @Test
+ public void testOnSafeBrowsingHitProceed() throws Throwable {
+ Assume.assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_HIT));
+ Assume.assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ENABLE));
+ Assume.assumeTrue(WebViewFeature.isFeatureSupported(
+ WebViewFeature.SAFE_BROWSING_RESPONSE_PROCEED));
+
+ final SafeBrowsingProceedClient proceedWebViewClient = new SafeBrowsingProceedClient();
+ mWebViewOnUiThread.setWebViewClient(proceedWebViewClient);
+ WebSettingsCompat.setSafeBrowsingEnabled(mWebViewOnUiThread.getSettings(), true);
+
+ // Load any page
+ String data = "<html><body>some safe page</body></html>";
+ mWebViewOnUiThread.loadDataAndWaitForCompletion(data, "text/html", null);
+
+ enableSafeBrowsingAndLoadUnsafePage(proceedWebViewClient);
+
+ // Check that we actually proceeded
+ Assert.assertEquals(TEST_SAFE_BROWSING_URL, mWebViewOnUiThread.getUrl());
+ }
+
+ private void enableSafeBrowsingAndLoadUnsafePage(SafeBrowsingClient client) throws Throwable {
+ // Note: Safe Browsing depends on user opt-in as well, so we can't assume it's actually
+ // enabled. #getSafeBrowsingEnabled will tell us the true state of whether Safe Browsing is
+ // enabled.
+ boolean deviceSupportsSafeBrowsing =
+ WebSettingsCompat.getSafeBrowsingEnabled(mWebViewOnUiThread.getSettings());
+ Assume.assumeTrue(deviceSupportsSafeBrowsing);
+
+ Assert.assertNull(client.getOnReceivedResourceError());
+ mWebViewOnUiThread.loadUrlAndWaitForCompletion(TEST_SAFE_BROWSING_URL);
+
+ Assert.assertEquals(TEST_SAFE_BROWSING_URL,
+ client.getOnSafeBrowsingHitRequest().getUrl().toString());
+ Assert.assertTrue(client.getOnSafeBrowsingHitRequest().isForMainFrame());
+ }
+
+ @Test
+ public void testOnPageCommitVisibleCalled() throws Exception {
+ Assume.assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.VISUAL_STATE_CALLBACK));
+
+ final CountDownLatch callbackLatch = new CountDownLatch(1);
+
+ mWebViewOnUiThread.setWebViewClient(new WebViewClientCompat() {
+ @Override
+ public void onPageCommitVisible(WebView view, String url) {
+ Assert.assertEquals(url, "about:blank");
+ callbackLatch.countDown();
+ }
+ });
+
+ mWebViewOnUiThread.loadUrl("about:blank");
+ Assert.assertTrue(callbackLatch.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS));
+ }
+
+ private class MockWebViewClient extends WebViewOnUiThread.WaitForLoadedClient {
+ private boolean mOnPageStartedCalled;
+ private boolean mOnPageFinishedCalled;
+ private boolean mOnLoadResourceCalled;
+ private WebResourceErrorCompat mOnReceivedResourceError;
+ private WebResourceResponse mOnReceivedHttpError;
+ private WebResourceRequest mLastShouldOverrideResourceRequest;
+
+ MockWebViewClient() {
+ super(mWebViewOnUiThread);
+ }
+
+ public WebResourceErrorCompat getOnReceivedResourceError() {
+ return mOnReceivedResourceError;
+ }
+
+ public WebResourceResponse getOnReceivedHttpError() {
+ return mOnReceivedHttpError;
+ }
+
+ public WebResourceRequest getLastShouldOverrideResourceRequest() {
+ return mLastShouldOverrideResourceRequest;
+ }
+
+ @Override
+ public void onPageStarted(WebView view, String url, Bitmap favicon) {
+ super.onPageStarted(view, url, favicon);
+ mOnPageStartedCalled = true;
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ super.onPageFinished(view, url);
+ Assert.assertTrue(mOnPageStartedCalled);
+ Assert.assertTrue(mOnLoadResourceCalled);
+ mOnPageFinishedCalled = true;
+ }
+
+ @Override
+ public void onLoadResource(WebView view, String url) {
+ super.onLoadResource(view, url);
+ Assert.assertTrue(mOnPageStartedCalled);
+ mOnLoadResourceCalled = true;
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public void onReceivedError(WebView view, int errorCode,
+ String description, String failingUrl) {
+ // This can be called if a test runs for a WebView which does not support the {@link
+ // WebViewFeature#RECEIVE_WEB_RESOURCE_ERROR} feature.
+ }
+
+ @Override
+ public void onReceivedError(WebView view, WebResourceRequest request,
+ WebResourceErrorCompat error) {
+ mOnReceivedResourceError = error;
+ }
+
+ @Override
+ public void onReceivedHttpError(WebView view, WebResourceRequest request,
+ WebResourceResponse errorResponse) {
+ super.onReceivedHttpError(view, request, errorResponse);
+ mOnReceivedHttpError = errorResponse;
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ // This can be called if a test runs for a WebView which does not support the {@link
+ // WebViewFeature#SHOULD_OVERRIDE_WITH_REDIRECTS} feature.
+ return false;
+ }
+
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
+ mLastShouldOverrideResourceRequest = request;
+ return false;
+ }
+ }
+
+ private class SafeBrowsingClient extends MockWebViewClient {
+ private WebResourceRequest mOnSafeBrowsingHitRequest;
+ private int mOnSafeBrowsingHitThreatType;
+
+ public WebResourceRequest getOnSafeBrowsingHitRequest() {
+ return mOnSafeBrowsingHitRequest;
+ }
+
+ public void setOnSafeBrowsingHitRequest(WebResourceRequest request) {
+ mOnSafeBrowsingHitRequest = request;
+ }
+
+ public int getOnSafeBrowsingHitThreatType() {
+ return mOnSafeBrowsingHitThreatType;
+ }
+
+ public void setOnSafeBrowsingHitThreatType(int type) {
+ mOnSafeBrowsingHitThreatType = type;
+ }
+ }
+
+ private class SafeBrowsingBackToSafetyClient extends SafeBrowsingClient {
+ @Override
+ public void onSafeBrowsingHit(WebView view, WebResourceRequest request,
+ int threatType, SafeBrowsingResponseCompat response) {
+ // Immediately go back to safety to return the network error code
+ setOnSafeBrowsingHitRequest(request);
+ setOnSafeBrowsingHitThreatType(threatType);
+ response.backToSafety(/* report */ true);
+ }
+ }
+
+ private class SafeBrowsingProceedClient extends SafeBrowsingClient {
+ @Override
+ public void onSafeBrowsingHit(WebView view, WebResourceRequest request,
+ int threatType, SafeBrowsingResponseCompat response) {
+ // Proceed through Safe Browsing warnings
+ setOnSafeBrowsingHitRequest(request);
+ setOnSafeBrowsingHitThreatType(threatType);
+ response.proceed(/* report */ true);
+ }
+ }
+}
diff --git a/webkit/src/androidTest/java/androidx/webkit/WebViewCompatTest.java b/webkit/src/androidTest/java/androidx/webkit/WebViewCompatTest.java
index 3c64b9c..03aa946 100644
--- a/webkit/src/androidTest/java/androidx/webkit/WebViewCompatTest.java
+++ b/webkit/src/androidTest/java/androidx/webkit/WebViewCompatTest.java
@@ -19,8 +19,10 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
import android.content.Context;
import android.content.ContextWrapper;
@@ -29,13 +31,9 @@
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.MediumTest;
import android.support.test.runner.AndroidJUnit4;
-import android.webkit.SafeBrowsingResponse;
import android.webkit.ValueCallback;
import android.webkit.WebResourceRequest;
import android.webkit.WebView;
-import android.webkit.WebViewClient;
-
-import androidx.core.os.BuildCompat;
import org.junit.Assert;
import org.junit.Before;
@@ -63,9 +61,7 @@
@Test
public void testVisualStateCallbackCalled() throws Exception {
- // TODO(gsennton) activate this test for pre-P devices when we can pre-install a WebView APK
- // containing support for the WebView Support Library, see b/73454652.
- if (!BuildCompat.isAtLeastP()) return;
+ assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.VISUAL_STATE_CALLBACK));
final CountDownLatch callbackLatch = new CountDownLatch(1);
final long kRequest = 100;
@@ -85,6 +81,8 @@
@Test
public void testCheckThread() {
+ // Skip this test if VisualStateCallback is not supported.
+ assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.VISUAL_STATE_CALLBACK));
try {
WebViewCompat.postVisualStateCallback(mWebViewOnUiThread.getWebViewOnCurrentThread(), 5,
new WebViewCompat.VisualStateCallback() {
@@ -117,9 +115,7 @@
@Test
public void testStartSafeBrowsingUseApplicationContext() throws Exception {
- // TODO(gsennton) activate this test for pre-P devices when we can pre-install a WebView APK
- // containing support for the WebView Support Library, see b/73454652.
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) return;
+ assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.START_SAFE_BROWSING));
final MockContext ctx =
new MockContext(InstrumentationRegistry.getTargetContext().getApplicationContext());
@@ -137,18 +133,14 @@
@Test
public void testStartSafeBrowsingWithNullCallbackDoesntCrash() throws Exception {
- // TODO(gsennton) activate this test for pre-P devices when we can pre-install a WebView APK
- // containing support for the WebView Support Library, see b/73454652.
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) return;
+ assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.START_SAFE_BROWSING));
WebViewCompat.startSafeBrowsing(InstrumentationRegistry.getTargetContext(), null);
}
@Test
public void testStartSafeBrowsingInvokesCallback() throws Exception {
- // TODO(gsennton) activate this test for pre-P devices when we can pre-install a WebView APK
- // containing support for the WebView Support Library, see b/73454652.
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) return;
+ assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.START_SAFE_BROWSING));
final CountDownLatch resultLatch = new CountDownLatch(1);
WebViewCompat.startSafeBrowsing(
@@ -166,9 +158,7 @@
@Test
public void testSetSafeBrowsingWhitelistWithMalformedList() throws Exception {
- // TODO(gsennton) activate this test for pre-P devices when we can pre-install a WebView APK
- // containing support for the WebView Support Library, see b/73454652.
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) return;
+ assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_WHITELIST));
List whitelist = new ArrayList<String>();
// Protocols are not supported in the whitelist
@@ -186,9 +176,9 @@
@Test
public void testSetSafeBrowsingWhitelistWithValidList() throws Exception {
- // TODO(gsennton) activate this test for pre-P devices when we can pre-install a WebView APK
- // containing support for the WebView Support Library, see b/73454652.
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) return;
+ assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_WHITELIST));
+ // This test relies on the onSafeBrowsingHit callback to verify correctness.
+ assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_HIT));
List whitelist = new ArrayList<String>();
whitelist.add("safe-browsing");
@@ -203,7 +193,7 @@
assertTrue(resultLatch.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS));
final CountDownLatch resultLatch2 = new CountDownLatch(1);
- mWebViewOnUiThread.setWebViewClient(new WebViewClient() {
+ mWebViewOnUiThread.setWebViewClient(new WebViewClientCompat() {
@Override
public void onPageFinished(WebView view, String url) {
resultLatch2.countDown();
@@ -211,7 +201,7 @@
@Override
public void onSafeBrowsingHit(WebView view, WebResourceRequest request, int threatType,
- SafeBrowsingResponse callback) {
+ SafeBrowsingResponseCompat callback) {
Assert.fail("Should not invoke onSafeBrowsingHit");
}
});
@@ -224,9 +214,8 @@
@Test
public void testGetSafeBrowsingPrivacyPolicyUrl() throws Exception {
- // TODO(gsennton) activate this test for pre-P devices when we can pre-install a WebView APK
- // containing support for the WebView Support Library, see b/73454652.
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) return;
+ assumeTrue(
+ WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_PRIVACY_POLICY_URL));
assertNotNull(WebViewCompat.getSafeBrowsingPrivacyPolicyUrl());
try {
@@ -235,4 +224,22 @@
Assert.fail("The privacy policy URL should be a well-formed URL");
}
}
+
+ /**
+ * WebViewCompat.getCurrentWebViewPackage should be null on pre-L devices.
+ * On L+ devices WebViewCompat.getCurrentWebViewPackage should be null only in exceptional
+ * circumstances - like when the WebView APK is being updated, or for Wear devices. The L+
+ * devices used in support library testing should have a non-null WebView package.
+ */
+ @Test
+ public void testGetCurrentWebViewPackage() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ assertNull(WebViewCompat.getCurrentWebViewPackage(
+ InstrumentationRegistry.getTargetContext()));
+ } else {
+ assertNotNull(
+ WebViewCompat.getCurrentWebViewPackage(
+ InstrumentationRegistry.getTargetContext()));
+ }
+ }
}
diff --git a/webkit/src/androidTest/java/androidx/webkit/WebViewOnUiThread.java b/webkit/src/androidTest/java/androidx/webkit/WebViewOnUiThread.java
index 9b4c9e9..bd83752 100644
--- a/webkit/src/androidTest/java/androidx/webkit/WebViewOnUiThread.java
+++ b/webkit/src/androidTest/java/androidx/webkit/WebViewOnUiThread.java
@@ -16,12 +16,45 @@
package androidx.webkit;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Looper;
+import android.os.SystemClock;
import android.support.test.InstrumentationRegistry;
+import android.webkit.ValueCallback;
+import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
-import android.webkit.WebViewClient;
-public class WebViewOnUiThread {
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.concurrent.Callable;
+
+class WebViewOnUiThread {
+ /**
+ * The maximum time, in milliseconds (10 seconds) to wait for a load
+ * to be triggered.
+ */
+ private static final long LOAD_TIMEOUT = 10000;
+
+ /**
+ * Set to true after onPageFinished is called.
+ */
+ private boolean mLoaded;
+
+ /**
+ * The progress, in percentage, of the page load. Valid values are between
+ * 0 and 100.
+ */
+ private int mProgress;
+
+ /**
+ * The WebView that calls will be made on.
+ */
private WebView mWebView;
public WebViewOnUiThread() {
@@ -29,6 +62,100 @@
@Override
public void run() {
mWebView = new WebView(InstrumentationRegistry.getTargetContext());
+ mWebView.setWebViewClient(new WaitForLoadedClient(WebViewOnUiThread.this));
+ mWebView.setWebChromeClient(new WaitForProgressClient(WebViewOnUiThread.this));
+ }
+ });
+ }
+
+ /**
+ * Called after a test is complete and the WebView should be disengaged from
+ * the tests.
+ */
+ public void cleanUp() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mWebView.clearHistory();
+ mWebView.clearCache(true);
+ mWebView.setWebChromeClient(null);
+ mWebView.setWebViewClient(null);
+ mWebView.destroy();
+ }
+ });
+ }
+
+ /**
+ * Called from WaitForLoadedClient.
+ */
+ synchronized void onPageStarted() {}
+
+ /**
+ * Called from WaitForLoadedClient, this is used to indicate that
+ * the page is loaded, but not drawn yet.
+ */
+ synchronized void onPageFinished() {
+ mLoaded = true;
+ this.notifyAll();
+ }
+
+ /**
+ * Called from the WebChrome client, this sets the current progress
+ * for a page.
+ * @param progress The progress made so far between 0 and 100.
+ */
+ synchronized void onProgressChanged(int progress) {
+ mProgress = progress;
+ this.notifyAll();
+ }
+
+ public void setWebViewClient(final WebViewClientCompat webviewClient) {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mWebView.setWebViewClient(webviewClient);
+ }
+ });
+ }
+
+ public WebMessagePortCompat[] createWebMessageChannelCompat() {
+ return getValue(new ValueGetter<WebMessagePortCompat[]>() {
+ @Override
+ public WebMessagePortCompat[] capture() {
+ return WebViewCompat.createWebMessageChannel(mWebView);
+ }
+ });
+ }
+
+ public void postWebMessageCompat(final WebMessageCompat message, final Uri targetOrigin) {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ WebViewCompat.postWebMessage(mWebView, message, targetOrigin);
+ }
+ });
+ }
+
+ public void addJavascriptInterface(final Object object, final String name) {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mWebView.addJavascriptInterface(object, name);
+ }
+ });
+ }
+
+ /**
+ * Calls loadUrl on the WebView and then waits onPageFinished
+ * and onProgressChange to reach 100.
+ * Test fails if the load timeout elapses.
+ * @param url The URL to load.
+ */
+ void loadUrlAndWaitForCompletion(final String url) {
+ callAndWait(new Runnable() {
+ @Override
+ public void run() {
+ mWebView.loadUrl(url);
}
});
}
@@ -42,11 +169,83 @@
});
}
- public void setWebViewClient(final WebViewClient webviewClient) {
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ /**
+ * Calls {@link WebView#loadData} on the WebView and then waits onPageFinished
+ * and onProgressChange to reach 100.
+ * Test fails if the load timeout elapses.
+ * @param data The data to load.
+ * @param mimeType The mimeType to pass to loadData.
+ * @param encoding The encoding to pass to loadData.
+ */
+ public void loadDataAndWaitForCompletion(@NonNull final String data,
+ @Nullable final String mimeType, @Nullable final String encoding) {
+ callAndWait(new Runnable() {
@Override
public void run() {
- mWebView.setWebViewClient(webviewClient);
+ mWebView.loadData(data, mimeType, encoding);
+ }
+ });
+ }
+
+ public void loadDataWithBaseURLAndWaitForCompletion(final String baseUrl,
+ final String data, final String mimeType, final String encoding,
+ final String historyUrl) {
+ callAndWait(new Runnable() {
+ @Override
+ public void run() {
+ mWebView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding,
+ historyUrl);
+ }
+ });
+ }
+
+ /**
+ * Use this only when JavaScript causes a page load to wait for the
+ * page load to complete. Otherwise use loadUrlAndWaitForCompletion or
+ * similar functions.
+ */
+ void waitForLoadCompletion() {
+ waitForCriteria(LOAD_TIMEOUT,
+ new Callable<Boolean>() {
+ @Override
+ public Boolean call() {
+ return isLoaded();
+ }
+ });
+ clearLoad();
+ }
+
+ private void waitForCriteria(long timeout, Callable<Boolean> doneCriteria) {
+ if (isUiThread()) {
+ waitOnUiThread(timeout, doneCriteria);
+ } else {
+ waitOnTestThread(timeout, doneCriteria);
+ }
+ }
+
+ public String getTitle() {
+ return getValue(new ValueGetter<String>() {
+ @Override
+ public String capture() {
+ return mWebView.getTitle();
+ }
+ });
+ }
+
+ public WebSettings getSettings() {
+ return getValue(new ValueGetter<WebSettings>() {
+ @Override
+ public WebSettings capture() {
+ return mWebView.getSettings();
+ }
+ });
+ }
+
+ public String getUrl() {
+ return getValue(new ValueGetter<String>() {
+ @Override
+ public String capture() {
+ return mWebView.getUrl();
}
});
}
@@ -61,16 +260,16 @@
});
}
- public WebSettings getSettings() {
- return getValue(new ValueGetter<WebSettings>() {
+ void evaluateJavascript(final String script, final ValueCallback<String> result) {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
@Override
- public WebSettings capture() {
- return mWebView.getSettings();
+ public void run() {
+ mWebView.evaluateJavascript(script, result);
}
});
}
- public WebView getWebViewOnCurrentThread() {
+ WebView getWebViewOnCurrentThread() {
return mWebView;
}
@@ -93,4 +292,151 @@
return mValue;
}
}
+
+ /**
+ * Returns true if the current thread is the UI thread based on the
+ * Looper.
+ */
+ private static boolean isUiThread() {
+ return (Looper.myLooper() == Looper.getMainLooper());
+ }
+
+ /**
+ * @return Whether or not the load has finished.
+ */
+ private synchronized boolean isLoaded() {
+ return mLoaded && mProgress == 100;
+ }
+
+ /**
+ * Makes a WebView call, waits for completion and then resets the
+ * load state in preparation for the next load call.
+ * @param call The call to make on the UI thread prior to waiting.
+ */
+ private void callAndWait(Runnable call) {
+ assertTrue("WebViewOnUiThread.load*AndWaitForCompletion calls "
+ + "may not be mixed with load* calls directly on WebView "
+ + "without calling waitForLoadCompletion after the load",
+ !isLoaded());
+ clearLoad(); // clear any extraneous signals from a previous load.
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(call);
+ waitForLoadCompletion();
+ }
+
+ /**
+ * Called whenever a load has been completed so that a subsequent call to
+ * waitForLoadCompletion doesn't return immediately.
+ */
+ private synchronized void clearLoad() {
+ mLoaded = false;
+ mProgress = 0;
+ }
+
+ /**
+ * Uses a polling mechanism, while pumping messages to check when the
+ * criteria is met.
+ */
+ private void waitOnUiThread(long timeout, final Callable<Boolean> doneCriteria) {
+ new PollingCheck(timeout) {
+ @Override
+ protected boolean check() {
+ pumpMessages();
+ try {
+ return doneCriteria.call();
+ } catch (Exception e) {
+ fail("Unexpected error while checking the criteria: " + e.getMessage());
+ return true;
+ }
+ }
+ }.run();
+ }
+
+ /**
+ * Uses a wait/notify to check when the criteria is met.
+ */
+ private synchronized void waitOnTestThread(long timeout, Callable<Boolean> doneCriteria) {
+ try {
+ long waitEnd = SystemClock.uptimeMillis() + timeout;
+ long timeRemaining = timeout;
+ while (!doneCriteria.call() && timeRemaining > 0) {
+ this.wait(timeRemaining);
+ timeRemaining = waitEnd - SystemClock.uptimeMillis();
+ }
+ assertTrue("Action failed to complete before timeout", doneCriteria.call());
+ } catch (InterruptedException e) {
+ // We'll just drop out of the loop and fail
+ } catch (Exception e) {
+ fail("Unexpected error while checking the criteria: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Pumps all currently-queued messages in the UI thread and then exits.
+ * This is useful to force processing while running tests in the UI thread.
+ */
+ private void pumpMessages() {
+ class ExitLoopException extends RuntimeException {
+ }
+
+ // Force loop to exit when processing this. Loop.quit() doesn't
+ // work because this is the main Loop.
+ mWebView.getHandler().post(new Runnable() {
+ @Override
+ public void run() {
+ throw new ExitLoopException(); // exit loop!
+ }
+ });
+ try {
+ // Pump messages until our message gets through.
+ Looper.loop();
+ } catch (ExitLoopException e) {
+ }
+ }
+
+ /**
+ * A WebChromeClient used to capture the onProgressChanged for use
+ * in waitFor functions. If a test must override the WebChromeClient,
+ * it can derive from this class or call onProgressChanged
+ * directly.
+ */
+ public static class WaitForProgressClient extends WebChromeClient {
+ private WebViewOnUiThread mOnUiThread;
+
+ WaitForProgressClient(WebViewOnUiThread onUiThread) {
+ mOnUiThread = onUiThread;
+ }
+
+ @Override
+ public void onProgressChanged(WebView view, int newProgress) {
+ super.onProgressChanged(view, newProgress);
+ mOnUiThread.onProgressChanged(newProgress);
+ }
+ }
+
+ /**
+ * A WebViewClient that captures the onPageFinished for use in
+ * waitFor functions. Using initializeWebView sets the WaitForLoadedClient
+ * into the WebView. If a test needs to set a specific WebViewClient and
+ * needs the waitForCompletion capability then it should derive from
+ * WaitForLoadedClient or call WebViewOnUiThread.onPageFinished.
+ */
+ public static class WaitForLoadedClient extends WebViewClientCompat {
+ private WebViewOnUiThread mOnUiThread;
+
+ WaitForLoadedClient(WebViewOnUiThread onUiThread) {
+ mOnUiThread = onUiThread;
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ super.onPageFinished(view, url);
+ mOnUiThread.onPageFinished();
+ }
+
+ @Override
+ public void onPageStarted(WebView view, String url, Bitmap favicon) {
+ super.onPageStarted(view, url, favicon);
+ mOnUiThread.onPageStarted();
+ }
+ }
}
diff --git a/webkit/src/main/java/androidx/webkit/SafeBrowsingResponseCompat.java b/webkit/src/main/java/androidx/webkit/SafeBrowsingResponseCompat.java
new file mode 100644
index 0000000..76121f3
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/SafeBrowsingResponseCompat.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import android.webkit.SafeBrowsingResponse;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RequiresFeature;
+import androidx.webkit.internal.WebViewFeatureInternal;
+
+import org.chromium.support_lib_boundary.SafeBrowsingResponseBoundaryInterface;
+import org.chromium.support_lib_boundary.util.BoundaryInterfaceReflectionUtil;
+
+import java.lang.reflect.InvocationHandler;
+
+/**
+ * Compatibility version of {@link SafeBrowsingResponse}.
+ */
+public abstract class SafeBrowsingResponseCompat {
+ /**
+ * Display the default interstitial.
+ *
+ * @param allowReporting {@code true} if the interstitial should show a reporting checkbox.
+ */
+ @RequiresFeature(name = WebViewFeature.SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+ public abstract void showInterstitial(boolean allowReporting);
+
+ /**
+ * Act as if the user clicked "visit this unsafe site."
+ *
+ * @param report {@code true} to enable Safe Browsing reporting.
+ */
+ @RequiresFeature(name = WebViewFeature.SAFE_BROWSING_RESPONSE_PROCEED,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+ public abstract void proceed(boolean report);
+
+ /**
+ * Act as if the user clicked "back to safety."
+ *
+ * @param report {@code true} to enable Safe Browsing reporting.
+ */
+ @RequiresFeature(name = WebViewFeature.SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+ public abstract void backToSafety(boolean report);
+
+ /**
+ * This class cannot be created by applications. The support library should instantiate this
+ * with {@link #fromInvocationHandler} or {@link #fromSafeBrowsingResponse}.
+ */
+ private SafeBrowsingResponseCompat() {
+ }
+
+ /**
+ * Conversion helper to create a SafeBrowsingResponseCompat which delegates calls to {@param
+ * handler}. The InvocationHandler must be created by {@link
+ * BoundaryInterfaceReflectionUtil#createInvocationHandlerFor} using {@link
+ * SafeBrowsingResponseBoundaryInterface}.
+ *
+ * @param handler The InvocationHandler that chromium passed in the callback.
+ */
+ @NonNull
+ /* package */ static SafeBrowsingResponseCompat fromInvocationHandler(
+ @NonNull InvocationHandler handler) {
+ final SafeBrowsingResponseBoundaryInterface responseDelegate =
+ BoundaryInterfaceReflectionUtil.castToSuppLibClass(
+ SafeBrowsingResponseBoundaryInterface.class, handler);
+ return new SafeBrowsingResponseCompat() {
+ @Override
+ public void showInterstitial(boolean allowReporting) {
+ final WebViewFeatureInternal webViewFeature =
+ WebViewFeatureInternal.getFeature(
+ WebViewFeature.SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL);
+ if (!webViewFeature.isSupportedByWebView()) {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ responseDelegate.showInterstitial(allowReporting);
+ }
+
+ @Override
+ public void proceed(boolean report) {
+ final WebViewFeatureInternal webViewFeature =
+ WebViewFeatureInternal.getFeature(
+ WebViewFeature.SAFE_BROWSING_RESPONSE_PROCEED);
+ if (!webViewFeature.isSupportedByWebView()) {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ responseDelegate.proceed(report);
+ }
+
+ @Override
+ public void backToSafety(boolean report) {
+ final WebViewFeatureInternal webViewFeature =
+ WebViewFeatureInternal.getFeature(
+ WebViewFeature.SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY);
+ if (!webViewFeature.isSupportedByWebView()) {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ responseDelegate.backToSafety(report);
+ }
+ };
+ }
+
+ /**
+ * Conversion helper to create a SafeBrowsingResponseCompat which delegates calls to {@param
+ * response}.
+ *
+ * @param response The SafeBrowsingResponse that chromium passed in the callback.
+ */
+ @NonNull
+ @RequiresApi(27)
+ /* package */ static SafeBrowsingResponseCompat fromSafeBrowsingResponse(
+ @NonNull final SafeBrowsingResponse response) {
+ // Frameworks support is implied by the API level.
+ return new SafeBrowsingResponseCompat() {
+ @Override
+ public void showInterstitial(boolean allowReporting) {
+ response.showInterstitial(allowReporting);
+ }
+
+ @Override
+ public void proceed(boolean report) {
+ response.proceed(report);
+ }
+
+ @Override
+ public void backToSafety(boolean report) {
+ response.backToSafety(report);
+ }
+ };
+ }
+}
diff --git a/webkit/src/main/java/androidx/webkit/ServiceWorkerClientCompat.java b/webkit/src/main/java/androidx/webkit/ServiceWorkerClientCompat.java
new file mode 100644
index 0000000..5c3c5ff
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/ServiceWorkerClientCompat.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Base class for clients to capture Service Worker related callbacks,
+ * see {@link ServiceWorkerControllerCompat} for usage example.
+ */
+public abstract class ServiceWorkerClientCompat {
+ /**
+ *
+ * Notify the host application of a resource request and allow the
+ * application to return the data. If the return value is {@code null}, the
+ * Service Worker will continue to load the resource as usual.
+ * Otherwise, the return response and data will be used.
+ *
+ * <p class="note"><b>Note:</b> This method is called on a thread other than the UI thread so
+ * clients should exercise caution when accessing private data or the view system.
+ *
+ * @param request Object containing the details of the request.
+ * @return A {@link android.webkit.WebResourceResponse} containing the
+ * response information or {@code null} if the WebView should load the
+ * resource itself.
+ * @see android.webkit.WebViewClient#shouldInterceptRequest(android.webkit.WebView,
+ * WebResourceRequest)
+ *
+ * This method is called only if
+ * {@link WebViewFeature#SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST} is supported. You can check
+ * whether that flag is supported using {@link WebViewFeature#isFeatureSupported(String)}.
+ *
+ */
+ public abstract WebResourceResponse shouldInterceptRequest(@NonNull WebResourceRequest request);
+}
diff --git a/webkit/src/main/java/androidx/webkit/ServiceWorkerControllerCompat.java b/webkit/src/main/java/androidx/webkit/ServiceWorkerControllerCompat.java
new file mode 100644
index 0000000..79b714a
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/ServiceWorkerControllerCompat.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresFeature;
+import androidx.annotation.RestrictTo;
+import androidx.webkit.internal.ServiceWorkerControllerImpl;
+
+/**
+ * Manages Service Workers used by WebView.
+ *
+ * <p>Example usage:
+ * <pre class="prettyprint">
+ * ServiceWorkerControllerCompat swController = ServiceWorkerControllerCompat.getInstance();
+ * swController.setServiceWorkerClient(new ServiceWorkerClientCompat() {
+ * {@literal @}Override
+ * public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) {
+ * // Capture request here and generate response or allow pass-through
+ * // by returning null.
+ * return null;
+ * }
+ * });
+ * </pre>
+ */
+public abstract class ServiceWorkerControllerCompat {
+ /**
+ *
+ * @hide Don't allow apps to sub-class this class.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public ServiceWorkerControllerCompat() {}
+
+ /**
+ * Returns the default ServiceWorkerController instance. At present there is
+ * only one ServiceWorkerController instance for all WebView instances,
+ * however this restriction may be relaxed in the future.
+ *
+ * @return the default ServiceWorkerController instance
+ */
+ @NonNull
+ @RequiresFeature(name = WebViewFeature.SERVICE_WORKER_BASIC_USAGE,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+ public static ServiceWorkerControllerCompat getInstance() {
+ return LAZY_HOLDER.INSTANCE;
+ }
+
+ private static class LAZY_HOLDER {
+ static final ServiceWorkerControllerCompat INSTANCE = new ServiceWorkerControllerImpl();
+ }
+
+ /**
+ *
+ * Gets the settings for all service workers.
+ *
+ * @return the current {@link ServiceWorkerWebSettingsCompat}
+ *
+ */
+ @NonNull
+ public abstract ServiceWorkerWebSettingsCompat getServiceWorkerWebSettings();
+
+ /**
+ *
+ * Sets the client to capture service worker related callbacks.
+ *
+ * A {@link ServiceWorkerClientCompat} should be set before any service workers are
+ * active, e.g. a safe place is before any WebView instances are created or
+ * pages loaded.
+ *
+ */
+ public abstract void setServiceWorkerClient(@Nullable ServiceWorkerClientCompat client);
+}
diff --git a/webkit/src/main/java/androidx/webkit/ServiceWorkerWebSettingsCompat.java b/webkit/src/main/java/androidx/webkit/ServiceWorkerWebSettingsCompat.java
new file mode 100644
index 0000000..6763db4
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/ServiceWorkerWebSettingsCompat.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import android.webkit.WebSettings;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RequiresFeature;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Manages settings state for all Service Workers. These settings are not tied to
+ * the lifetime of any WebView because service workers can outlive WebView instances.
+ * The settings are similar to {@link WebSettings} but only settings relevant to
+ * Service Workers are supported.
+ */
+public abstract class ServiceWorkerWebSettingsCompat {
+ /**
+ * @hide Don't allow apps to sub-class this class.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public ServiceWorkerWebSettingsCompat() {}
+
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @IntDef(value = {
+ WebSettings.LOAD_DEFAULT,
+ WebSettings.LOAD_CACHE_ELSE_NETWORK,
+ WebSettings.LOAD_NO_CACHE,
+ WebSettings.LOAD_CACHE_ONLY
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CacheMode {}
+
+ /**
+ *
+ * Overrides the way the cache is used, see {@link WebSettings#setCacheMode}.
+ *
+ * @param mode the mode to use. One of {@link WebSettings#LOAD_DEFAULT},
+ * {@link WebSettings#LOAD_CACHE_ELSE_NETWORK}, {@link WebSettings#LOAD_NO_CACHE}
+ * or {@link WebSettings#LOAD_CACHE_ONLY}. The default value is
+ * {@link WebSettings#LOAD_DEFAULT}.
+ *
+ */
+ @RequiresFeature(name = WebViewFeature.SERVICE_WORKER_CACHE_MODE,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+ public abstract void setCacheMode(@CacheMode int mode);
+
+ /**
+ *
+ * Gets the current setting for overriding the cache mode.
+ *
+ * @return the current setting for overriding the cache mode
+ * @see #setCacheMode
+ *
+ */
+ @RequiresFeature(name = WebViewFeature.SERVICE_WORKER_CACHE_MODE,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+ public abstract @CacheMode int getCacheMode();
+
+ /**
+ *
+ * Enables or disables content URL access from Service Workers, see
+ * {@link WebSettings#setAllowContentAccess}.
+ *
+ */
+ @RequiresFeature(name = WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+ public abstract void setAllowContentAccess(boolean allow);
+
+ /**
+ *
+ * Gets whether Service Workers support content URL access.
+ *
+ * @see #setAllowContentAccess
+ *
+ */
+ @RequiresFeature(name = WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+ public abstract boolean getAllowContentAccess();
+
+ /**
+ *
+ * Enables or disables file access within Service Workers, see
+ * {@link WebSettings#setAllowFileAccess}.
+ *
+ */
+ @RequiresFeature(name = WebViewFeature.SERVICE_WORKER_FILE_ACCESS,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+ public abstract void setAllowFileAccess(boolean allow);
+
+ /**
+ *
+ * Gets whether Service Workers support file access.
+ *
+ * @see #setAllowFileAccess
+ *
+ */
+ @RequiresFeature(name = WebViewFeature.SERVICE_WORKER_FILE_ACCESS,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+ public abstract boolean getAllowFileAccess();
+
+ /**
+ *
+ * Sets whether Service Workers should not load resources from the network,
+ * see {@link WebSettings#setBlockNetworkLoads}.
+ *
+ * @param flag {@code true} means block network loads by the Service Workers
+ *
+ */
+ @RequiresFeature(name = WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+ public abstract void setBlockNetworkLoads(boolean flag);
+
+ /**
+ *
+ * Gets whether Service Workers are prohibited from loading any resources from the network.
+ *
+ * @return {@code true} if the Service Workers are not allowed to load any resources from the
+ * network
+ * @see #setBlockNetworkLoads
+ *
+ */
+ @RequiresFeature(name = WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+ public abstract boolean getBlockNetworkLoads();
+}
diff --git a/webkit/src/main/java/androidx/webkit/WebMessageCompat.java b/webkit/src/main/java/androidx/webkit/WebMessageCompat.java
new file mode 100644
index 0000000..e1b1193
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/WebMessageCompat.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import androidx.annotation.Nullable;
+
+/**
+ * The Java representation of the HTML5 PostMessage event. See
+ * https://html.spec.whatwg.org/multipage/comms.html#the-messageevent-interfaces
+ * for definition of a MessageEvent in HTML5.
+ */
+public class WebMessageCompat {
+
+ private String mData;
+ private WebMessagePortCompat[] mPorts;
+
+ /**
+ * Creates a WebMessage.
+ * @param data the data of the message.
+ */
+ public WebMessageCompat(@Nullable String data) {
+ mData = data;
+ }
+
+ /**
+ * Creates a WebMessage.
+ * @param data the data of the message.
+ * @param ports the ports that are sent with the message.
+ */
+ public WebMessageCompat(@Nullable String data, @Nullable WebMessagePortCompat[] ports) {
+ mData = data;
+ mPorts = ports;
+ }
+
+ /**
+ * Returns the data of the message.
+ */
+ public @Nullable String getData() {
+ return mData;
+ }
+
+ /**
+ * Returns the ports that are sent with the message, or {@code null} if no port
+ * is sent.
+ */
+ @Nullable
+ public WebMessagePortCompat[] getPorts() {
+ return mPorts;
+ }
+}
diff --git a/webkit/src/main/java/androidx/webkit/WebMessagePortCompat.java b/webkit/src/main/java/androidx/webkit/WebMessagePortCompat.java
new file mode 100644
index 0000000..f6efd0a
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/WebMessagePortCompat.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import android.os.Handler;
+import android.webkit.WebMessagePort;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+/**
+ * <p>The Java representation of the
+ * <a href="https://html.spec.whatwg.org/multipage/comms.html#messageport">
+ * HTML5 message ports.</a>
+ *
+ * <p>A Message port represents one endpoint of a Message Channel. In Android
+ * webview, there is no separate Message Channel object. When a message channel
+ * is created, both ports are tangled to each other and started, and then
+ * returned in a MessagePort array, see {@link WebViewCompat#createWebMessageChannel}
+ * for creating a message channel.
+ *
+ * <p>When a message port is first created or received via transfer, it does not
+ * have a WebMessageCallback to receive web messages. The messages are queued until
+ * a WebMessageCallback is set.
+ *
+ * <p>A message port should be closed when it is not used by the embedder application
+ * anymore. A closed port cannot be transferred or cannot be reopened to send
+ * messages. Close can be called multiple times.
+ *
+ * <p>When a port is transferred to JS, it cannot be used to send or receive messages
+ * at the Java side anymore. Different from HTML5 Spec, a port cannot be transferred
+ * if one of these has ever happened: i. a message callback was set, ii. a message was
+ * posted on it. A transferred port cannot be closed by the application, since
+ * the ownership is also transferred.
+ *
+ * <p>It is possible to transfer both ports of a channel to JS, for example for
+ * communication between subframes.
+ */
+public abstract class WebMessagePortCompat {
+ /**
+ * The listener for handling MessagePort events. The message callback
+ * methods are called on the main thread. If the embedder application
+ * wants to receive the messages on a different thread, it can do this
+ * by passing a Handler in
+ * {@link WebMessagePortCompat#setWebMessageCallback(Handler, WebMessageCallbackCompat)}.
+ * In the latter case, the application should be extra careful for thread safety
+ * since WebMessagePort methods should be called on main thread.
+ */
+ public abstract static class WebMessageCallbackCompat {
+ /**
+ * Message callback for receiving onMessage events.
+ *
+ * @param port the WebMessagePort that the message is destined for
+ * @param message the message from the entangled port.
+ */
+ public void onMessage(@NonNull WebMessagePortCompat port,
+ @Nullable WebMessageCompat message) { }
+ }
+
+ /**
+ * @hide disallow app devs to extend this class.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public WebMessagePortCompat() { }
+
+ /**
+ * Post a WebMessage to the entangled port.
+ *
+ * @param message the message from Java to JS.
+ *
+ * @throws IllegalStateException If message port is already transferred or closed.
+ */
+ public abstract void postMessage(@NonNull WebMessageCompat message);
+
+ /**
+ * Close the message port and free any resources associated with it.
+ */
+ public abstract void close();
+
+ /**
+ * Sets a callback to receive message events on the main thread.
+ *
+ * @param callback the message callback.
+ */
+ public abstract void setWebMessageCallback(@NonNull WebMessageCallbackCompat callback);
+
+ /**
+ * Sets a callback to receive message events on the handler that is provided
+ * by the application. If the handler is null the message events are received on the main
+ * thread.
+ *
+ * @param handler the handler to receive the message events.
+ * @param callback the message callback.
+ */
+ public abstract void setWebMessageCallback(@Nullable Handler handler,
+ @NonNull WebMessageCallbackCompat callback);
+
+ /**
+ * Internal getter returning the private {@link WebMessagePort} implementing this class.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public abstract WebMessagePort getFrameworkPort();
+}
diff --git a/webkit/src/main/java/androidx/webkit/WebResourceErrorCompat.java b/webkit/src/main/java/androidx/webkit/WebResourceErrorCompat.java
new file mode 100644
index 0000000..3d8b99a
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/WebResourceErrorCompat.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import android.webkit.WebResourceError;
+import android.webkit.WebViewClient;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresFeature;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Compatibility version of {@link WebResourceError}.
+ */
+public abstract class WebResourceErrorCompat {
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @IntDef(value = {
+ WebViewClient.ERROR_UNKNOWN,
+ WebViewClient.ERROR_HOST_LOOKUP,
+ WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME,
+ WebViewClient.ERROR_AUTHENTICATION,
+ WebViewClient.ERROR_PROXY_AUTHENTICATION,
+ WebViewClient.ERROR_CONNECT,
+ WebViewClient.ERROR_IO,
+ WebViewClient.ERROR_TIMEOUT,
+ WebViewClient.ERROR_REDIRECT_LOOP,
+ WebViewClient.ERROR_UNSUPPORTED_SCHEME,
+ WebViewClient.ERROR_FAILED_SSL_HANDSHAKE,
+ WebViewClient.ERROR_BAD_URL,
+ WebViewClient.ERROR_FILE,
+ WebViewClient.ERROR_FILE_NOT_FOUND,
+ WebViewClient.ERROR_TOO_MANY_REQUESTS,
+ WebViewClient.ERROR_UNSAFE_RESOURCE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface NetErrorCode {}
+
+ /**
+ * Gets the error code of the error. The code corresponds to one
+ * of the {@code ERROR_*} constants in {@link WebViewClient}.
+ *
+ * @return The error code of the error
+ */
+ @RequiresFeature(name = WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+ public abstract @NetErrorCode int getErrorCode();
+
+ /**
+ * Gets the string describing the error. Descriptions are localized,
+ * and thus can be used for communicating the problem to the user.
+ *
+ * @return The description of the error
+ */
+ @NonNull
+ @RequiresFeature(name = WebViewFeature.WEB_RESOURCE_ERROR_GET_DESCRIPTION,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+ public abstract CharSequence getDescription();
+
+ /**
+ * This class cannot be created by applications.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public WebResourceErrorCompat() {
+ }
+}
diff --git a/webkit/src/main/java/androidx/webkit/WebResourceRequestCompat.java b/webkit/src/main/java/androidx/webkit/WebResourceRequestCompat.java
new file mode 100644
index 0000000..783efca
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/WebResourceRequestCompat.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import android.annotation.SuppressLint;
+import android.webkit.WebResourceRequest;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresFeature;
+import androidx.webkit.internal.WebResourceRequestAdapter;
+import androidx.webkit.internal.WebViewFeatureInternal;
+import androidx.webkit.internal.WebViewGlueCommunicator;
+
+// TODO(gsennton) add a test for this class
+
+/**
+ * Compatibility version of {@link WebResourceRequest}.
+ */
+public class WebResourceRequestCompat {
+
+ // Developers should not be able to instantiate this class.
+ private WebResourceRequestCompat() {}
+
+ /**
+ * Gets whether the request was a result of a server-side redirect.
+ *
+ * @return whether the request was a result of a server-side redirect.
+ */
+ @SuppressLint("NewApi")
+ @RequiresFeature(name = WebViewFeature.WEB_RESOURCE_REQUEST_IS_REDIRECT,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+ public static boolean isRedirect(@NonNull WebResourceRequest request) {
+ WebViewFeatureInternal feature = WebViewFeatureInternal.WEB_RESOURCE_REQUEST_IS_REDIRECT;
+ if (feature.isSupportedByFramework()) {
+ return request.isRedirect();
+ } else if (feature.isSupportedByWebView()) {
+ return getAdapter(request).isRedirect();
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ private static WebResourceRequestAdapter getAdapter(WebResourceRequest request) {
+ return WebViewGlueCommunicator.getCompatConverter().convertWebResourceRequest(request);
+ }
+}
diff --git a/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java b/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
index c73cda6..bff6170 100644
--- a/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
+++ b/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
@@ -16,20 +16,27 @@
package androidx.webkit;
-import android.os.Build;
+import android.annotation.SuppressLint;
import android.webkit.WebSettings;
+import androidx.annotation.IntDef;
+import androidx.annotation.RequiresFeature;
+import androidx.annotation.RestrictTo;
import androidx.webkit.internal.WebSettingsAdapter;
+import androidx.webkit.internal.WebViewFeatureInternal;
import androidx.webkit.internal.WebViewGlueCommunicator;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
/**
* Compatibility version of {@link android.webkit.WebSettings}
*/
public class WebSettingsCompat {
private WebSettingsCompat() {}
- // TODO(gsennton): add feature detection
-
/**
* Sets whether this WebView should raster tiles when it is
* offscreen but attached to a window. Turning this on can avoid
@@ -43,11 +50,18 @@
* visible WebViews and WebViews about to be animated to visible.
* </ul>
*/
+ @SuppressLint("NewApi")
+ @RequiresFeature(name = WebViewFeature.OFF_SCREEN_PRERASTER,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
public static void setOffscreenPreRaster(WebSettings webSettings, boolean enabled) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ WebViewFeatureInternal webviewFeature =
+ WebViewFeatureInternal.getFeature(WebViewFeature.OFF_SCREEN_PRERASTER);
+ if (webviewFeature.isSupportedByFramework()) {
webSettings.setOffscreenPreRaster(enabled);
- } else {
+ } else if (webviewFeature.isSupportedByWebView()) {
getAdapter(webSettings).setOffscreenPreRaster(enabled);
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
}
}
@@ -57,11 +71,18 @@
* @return {@code true} if this WebView will raster tiles when it is
* offscreen but attached to a window.
*/
+ @SuppressLint("NewApi")
+ @RequiresFeature(name = WebViewFeature.OFF_SCREEN_PRERASTER,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
public static boolean getOffscreenPreRaster(WebSettings webSettings) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ WebViewFeatureInternal webviewFeature =
+ WebViewFeatureInternal.getFeature(WebViewFeature.OFF_SCREEN_PRERASTER);
+ if (webviewFeature.isSupportedByFramework()) {
return webSettings.getOffscreenPreRaster();
- } else {
+ } else if (webviewFeature.isSupportedByWebView()) {
return getAdapter(webSettings).getOffscreenPreRaster();
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
}
}
@@ -79,11 +100,18 @@
*
* @param enabled Whether Safe Browsing is enabled.
*/
+ @SuppressLint("NewApi")
+ @RequiresFeature(name = WebViewFeature.SAFE_BROWSING_ENABLE,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
public static void setSafeBrowsingEnabled(WebSettings webSettings, boolean enabled) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ WebViewFeatureInternal webviewFeature =
+ WebViewFeatureInternal.getFeature(WebViewFeature.SAFE_BROWSING_ENABLE);
+ if (webviewFeature.isSupportedByFramework()) {
webSettings.setSafeBrowsingEnabled(enabled);
- } else {
+ } else if (webviewFeature.isSupportedByWebView()) {
getAdapter(webSettings).setSafeBrowsingEnabled(enabled);
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
}
}
@@ -93,23 +121,52 @@
*
* @return {@code true} if Safe Browsing is enabled and {@code false} otherwise.
*/
+ @SuppressLint("NewApi")
+ @RequiresFeature(name = WebViewFeature.SAFE_BROWSING_ENABLE,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
public static boolean getSafeBrowsingEnabled(WebSettings webSettings) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ WebViewFeatureInternal webviewFeature =
+ WebViewFeatureInternal.getFeature(WebViewFeature.SAFE_BROWSING_ENABLE);
+ if (webviewFeature.isSupportedByFramework()) {
return webSettings.getSafeBrowsingEnabled();
- } else {
+ } else if (webviewFeature.isSupportedByWebView()) {
return getAdapter(webSettings).getSafeBrowsingEnabled();
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
}
}
/**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @IntDef(flag = true, value = {
+ WebSettings.MENU_ITEM_NONE,
+ WebSettings.MENU_ITEM_SHARE,
+ WebSettings.MENU_ITEM_WEB_SEARCH,
+ WebSettings.MENU_ITEM_PROCESS_TEXT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @Target({ElementType.PARAMETER, ElementType.METHOD})
+ public @interface MenuItemFlags {}
+
+ /**
* Disables the action mode menu items according to {@code menuItems} flag.
* @param menuItems an integer field flag for the menu items to be disabled.
*/
- public static void setDisabledActionModeMenuItems(WebSettings webSettings, int menuItems) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ @SuppressLint("NewApi")
+ @RequiresFeature(name = WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+ public static void setDisabledActionModeMenuItems(WebSettings webSettings,
+ @MenuItemFlags int menuItems) {
+ WebViewFeatureInternal webviewFeature =
+ WebViewFeatureInternal.getFeature(WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS);
+ if (webviewFeature.isSupportedByFramework()) {
webSettings.setDisabledActionModeMenuItems(menuItems);
- } else {
+ } else if (webviewFeature.isSupportedByWebView()) {
getAdapter(webSettings).setDisabledActionModeMenuItems(menuItems);
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
}
}
@@ -119,11 +176,18 @@
*
* @return all the disabled menu item flags combined with bitwise OR.
*/
- public static int getDisabledActionModeMenuItems(WebSettings webSettings) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ @SuppressLint("NewApi")
+ @RequiresFeature(name = WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+ public static @MenuItemFlags int getDisabledActionModeMenuItems(WebSettings webSettings) {
+ WebViewFeatureInternal webviewFeature =
+ WebViewFeatureInternal.getFeature(WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS);
+ if (webviewFeature.isSupportedByFramework()) {
return webSettings.getDisabledActionModeMenuItems();
- } else {
+ } else if (webviewFeature.isSupportedByWebView()) {
return getAdapter(webSettings).getDisabledActionModeMenuItems();
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
}
}
diff --git a/webkit/src/main/java/androidx/webkit/WebViewClientCompat.java b/webkit/src/main/java/androidx/webkit/WebViewClientCompat.java
new file mode 100644
index 0000000..3dcfcc5
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/WebViewClientCompat.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import android.os.Build;
+import android.webkit.SafeBrowsingResponse;
+import android.webkit.WebResourceError;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.webkit.internal.WebResourceErrorImpl;
+import androidx.webkit.internal.WebViewFeatureInternal;
+
+import org.chromium.support_lib_boundary.WebViewClientBoundaryInterface;
+import org.chromium.support_lib_boundary.util.Features;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.InvocationHandler;
+
+/**
+ * Compatibility version of {@link android.webkit.WebViewClient}.
+ */
+// Note: some methods are marked as RequiresApi 21, because only an up-to-date WebView APK would
+// ever invoke these methods (and WebView can only be updated on Lollipop and above). The app can
+// still construct a WebViewClientCompat on a pre-Lollipop devices, and explicitly invoke these
+// methods, so each of these methods must also handle this case.
+public class WebViewClientCompat extends WebViewClient implements WebViewClientBoundaryInterface {
+ private static final String[] sSupportedFeatures = new String[] {
+ Features.VISUAL_STATE_CALLBACK,
+ Features.RECEIVE_WEB_RESOURCE_ERROR,
+ Features.RECEIVE_HTTP_ERROR,
+ Features.SHOULD_OVERRIDE_WITH_REDIRECTS,
+ Features.SAFE_BROWSING_HIT,
+ };
+
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @IntDef(value = {
+ WebViewClient.SAFE_BROWSING_THREAT_UNKNOWN,
+ WebViewClient.SAFE_BROWSING_THREAT_MALWARE,
+ WebViewClient.SAFE_BROWSING_THREAT_PHISHING,
+ WebViewClient.SAFE_BROWSING_THREAT_UNWANTED_SOFTWARE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SafeBrowsingThreat {}
+
+ /**
+ * Returns the list of features this client supports. This feature list should always be a
+ * subset of the Features declared in WebViewFeature.
+ *
+ * @hide
+ */
+ @Override
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public final String[] getSupportedFeatures() {
+ return sSupportedFeatures;
+ }
+
+ /**
+ * Notify the host application that {@link android.webkit.WebView} content left over from
+ * previous page navigations will no longer be drawn.
+ *
+ * <p>This callback can be used to determine the point at which it is safe to make a recycled
+ * {@link android.webkit.WebView} visible, ensuring that no stale content is shown. It is called
+ * at the earliest point at which it can be guaranteed that {@link WebView#onDraw} will no
+ * longer draw any content from previous navigations. The next draw will display either the
+ * {@link WebView#setBackgroundColor background color} of the {@link WebView}, or some of the
+ * contents of the newly loaded page.
+ *
+ * <p>This method is called when the body of the HTTP response has started loading, is reflected
+ * in the DOM, and will be visible in subsequent draws. This callback occurs early in the
+ * document loading process, and as such you should expect that linked resources (for example,
+ * CSS and images) may not be available.
+ *
+ * <p>For more fine-grained notification of visual state updates, see {@link
+ * WebViewCompat#postVisualStateCallback}.
+ *
+ * <p>Please note that all the conditions and recommendations applicable to
+ * {@link WebViewCompat#postVisualStateCallback} also apply to this API.
+ *
+ * <p>This callback is only called for main frame navigations.
+ *
+ * <p>This method is called only if {@link WebViewFeature#VISUAL_STATE_CALLBACK} is supported.
+ * You can check whether that flag is supported using {@link
+ * WebViewFeature#isFeatureSupported(String)}.
+ *
+ * @param view The {@link android.webkit.WebView} for which the navigation occurred.
+ * @param url The URL corresponding to the page navigation that triggered this callback.
+ */
+ @Override
+ public void onPageCommitVisible(@NonNull WebView view, @NonNull String url) {
+ }
+
+ /**
+ * Invoked by chromium (for WebView APks 67+) for the {@code onReceivedError} event.
+ * Applications are not meant to override this, and should instead override the non-final {@link
+ * onReceivedError(WebView, WebResourceRequest, WebResourceErrorCompat)} method.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @Override
+ @RequiresApi(21)
+ public final void onReceivedError(@NonNull WebView view, @NonNull WebResourceRequest request,
+ /* WebResourceError */ @NonNull InvocationHandler handler) {
+ onReceivedError(view, request, new WebResourceErrorImpl(handler));
+ }
+
+ /**
+ * Invoked by chromium (in legacy WebView APKs) for the {@code onReceivedError} event on {@link
+ * Build.VERSION_CODES.M} and above. Applications are not meant to override this, and should
+ * instead override the non-final {@link onReceivedError(WebView, WebResourceRequest,
+ * WebResourceErrorCompat)} method.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @Override
+ @RequiresApi(23)
+ public final void onReceivedError(@NonNull WebView view, @NonNull WebResourceRequest request,
+ @NonNull WebResourceError error) {
+ if (Build.VERSION.SDK_INT < 23) return;
+ onReceivedError(view, request, new WebResourceErrorImpl(error));
+ }
+
+ /**
+ * Report web resource loading error to the host application. These errors usually indicate
+ * inability to connect to the server. Note that unlike the deprecated version of the callback,
+ * the new version will be called for any resource (iframe, image, etc.), not just for the main
+ * page. Thus, it is recommended to perform minimum required work in this callback.
+ *
+ * <p>This method is called only if {@link WebViewFeature#RECEIVE_WEB_RESOURCE_ERROR} is
+ * supported. You can check whether that flag is supported using {@link
+ * WebViewFeature#isFeatureSupported(String)}.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param request The originating request.
+ * @param error Information about the error occurred.
+ */
+ @SuppressWarnings("deprecation") // for invoking the old onReceivedError.
+ @RequiresApi(21)
+ public void onReceivedError(@NonNull WebView view, @NonNull WebResourceRequest request,
+ @NonNull WebResourceErrorCompat error) {
+ if (Build.VERSION.SDK_INT < 21) return;
+ if (!WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE)
+ || !WebViewFeature.isFeatureSupported(
+ WebViewFeature.WEB_RESOURCE_ERROR_GET_DESCRIPTION)) {
+ // If the WebView APK drops supports for these APIs in the future, simply do nothing.
+ return;
+ }
+ if (request.isForMainFrame()) {
+ onReceivedError(view,
+ error.getErrorCode(), error.getDescription().toString(),
+ request.getUrl().toString());
+ }
+ }
+
+ /**
+ * Notify the host application that an HTTP error has been received from the server while
+ * loading a resource. HTTP errors have status codes >= 400. This callback will be called
+ * for any resource (iframe, image, etc.), not just for the main page. Thus, it is recommended
+ * to perform minimum required work in this callback. Note that the content of the server
+ * response may not be provided within the {@code errorResponse} parameter.
+ *
+ * <p>This method is called only if {@link WebViewFeature#RECEIVE_HTTP_ERROR} is supported. You
+ * can check whether that flag is supported using {@link
+ * WebViewFeature#isFeatureSupported(String)}.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param request The originating request.
+ * @param errorResponse Information about the error occurred.
+ */
+ @Override
+ public void onReceivedHttpError(@NonNull WebView view, @NonNull WebResourceRequest request,
+ @NonNull WebResourceResponse errorResponse) {
+ }
+
+ /**
+ * Invoked by chromium (for WebView APks 67+) for the {@code onSafeBrowsingHit} event.
+ * Applications are not meant to override this, and should instead override the non-final {@link
+ * onSafeBrowsingHit(WebView, WebResourceRequest, int, SafeBrowsingResponseCompat)} method.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @Override
+ public final void onSafeBrowsingHit(@NonNull WebView view, @NonNull WebResourceRequest request,
+ @SafeBrowsingThreat int threatType,
+ /* SafeBrowsingResponse */ @NonNull InvocationHandler handler) {
+ onSafeBrowsingHit(view, request, threatType,
+ SafeBrowsingResponseCompat.fromInvocationHandler(handler));
+ }
+
+ /**
+ * Invoked by chromium (in legacy WebView APKs) for the {@code onSafeBrowsingHit} event on
+ * {@link Build.VERSION_CODES.O_MR1} and above. Applications are not meant to override this, and
+ * should instead override the non-final {@link onSafeBrowsingHit(WebView, WebResourceRequest,
+ * int, SafeBrowsingResponseCompat)} method.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @Override
+ @RequiresApi(27)
+ public final void onSafeBrowsingHit(@NonNull WebView view, @NonNull WebResourceRequest request,
+ @SafeBrowsingThreat int threatType, @NonNull SafeBrowsingResponse response) {
+ onSafeBrowsingHit(view, request, threatType,
+ SafeBrowsingResponseCompat.fromSafeBrowsingResponse(response));
+ }
+
+ /**
+ * Notify the host application that a loading URL has been flagged by Safe Browsing.
+ *
+ * The application must invoke the callback to indicate the preferred response. The default
+ * behavior is to show an interstitial to the user, with the reporting checkbox visible.
+ *
+ * If the application needs to show its own custom interstitial UI, the callback can be invoked
+ * asynchronously with {@link SafeBrowsingResponseCompat#backToSafety} or {@link
+ * SafeBrowsingResponseCompat#proceed}, depending on user response.
+ *
+ * @param view The WebView that hit the malicious resource.
+ * @param request Object containing the details of the request.
+ * @param threatType The reason the resource was caught by Safe Browsing, corresponding to a
+ * {@code SAFE_BROWSING_THREAT_*} value.
+ * @param callback Applications must invoke one of the callback methods.
+ */
+ public void onSafeBrowsingHit(@NonNull WebView view, @NonNull WebResourceRequest request,
+ @SafeBrowsingThreat int threatType, @NonNull SafeBrowsingResponseCompat callback) {
+ if (WebViewFeature.isFeatureSupported(
+ WebViewFeature.SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL)) {
+ callback.showInterstitial(true);
+ } else {
+ // This should not happen, but in case the WebView APK eventually drops support for
+ // showInterstitial(), raise a runtime exception and require the WebView APK to handle
+ // this.
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ /**
+ * Give the host application a chance to take over the control when a new
+ * url is about to be loaded in the current WebView. If WebViewClient is not
+ * provided, by default WebView will ask Activity Manager to choose the
+ * proper handler for the url. If WebViewClient is provided, return {@code true}
+ * means the host application handles the url, while return {@code false} means the
+ * current WebView handles the url.
+ *
+ * <p>Notes:
+ * <ul>
+ * <li>This method is not called for requests using the POST "method".</li>
+ * <li>This method is also called for subframes with non-http schemes, thus it is
+ * strongly disadvised to unconditionally call {@link WebView#loadUrl(String)}
+ * with the request's url from inside the method and then return {@code true},
+ * as this will make WebView to attempt loading a non-http url, and thus fail.</li>
+ * </ul>
+ *
+ * <p>This method is called only if {@link WebViewFeature#SHOULD_OVERRIDE_WITH_REDIRECTS} is
+ * supported. You can check whether that flag is supported using {@link
+ * WebViewFeature#isFeatureSupported(String)}.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param request Object containing the details of the request.
+ * @return {@code true} if the host application wants to leave the current WebView
+ * and handle the url itself, otherwise return {@code false}.
+ */
+ @Override
+ @SuppressWarnings("deprecation") // for invoking the old shouldOverrideUrlLoading.
+ @RequiresApi(21)
+ public boolean shouldOverrideUrlLoading(@NonNull WebView view,
+ @NonNull WebResourceRequest request) {
+ if (Build.VERSION.SDK_INT < 21) return false;
+ return shouldOverrideUrlLoading(view, request.getUrl().toString());
+ }
+}
diff --git a/webkit/src/main/java/androidx/webkit/WebViewCompat.java b/webkit/src/main/java/androidx/webkit/WebViewCompat.java
index 6a48e35..1172997 100644
--- a/webkit/src/main/java/androidx/webkit/WebViewCompat.java
+++ b/webkit/src/main/java/androidx/webkit/WebViewCompat.java
@@ -16,7 +16,10 @@
package androidx.webkit;
+import android.annotation.SuppressLint;
import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Looper;
@@ -25,10 +28,13 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RequiresFeature;
import androidx.core.os.BuildCompat;
+import androidx.webkit.internal.WebMessagePortImpl;
+import androidx.webkit.internal.WebViewFeatureInternal;
import androidx.webkit.internal.WebViewGlueCommunicator;
import androidx.webkit.internal.WebViewProviderAdapter;
-import androidx.webkit.internal.WebViewProviderFactoryAdapter;
+import androidx.webkit.internal.WebViewProviderFactory;
import org.chromium.support_lib_boundary.WebViewProviderBoundaryInterface;
@@ -40,6 +46,9 @@
* Compatibility version of {@link android.webkit.WebView}
*/
public class WebViewCompat {
+ private static final Uri WILDCARD_URI = Uri.parse("*");
+ private static final Uri EMPTY_URI = Uri.parse("");
+
private WebViewCompat() {} // Don't allow instances of this class to be constructed.
/**
@@ -104,13 +113,22 @@
* {@link android.webkit.WebSettings#setOffscreenPreRaster} for more details and do consider its
* caveats.
*
+ * This method should only be called if
+ * {@link WebViewFeature#isFeatureSupported(String)}
+ * returns true for {@link WebViewFeature#VISUAL_STATE_CALLBACK}.
+ *
* @param requestId An id that will be returned in the callback to allow callers to match
* requests with callbacks.
* @param callback The callback to be invoked.
*/
+ @SuppressWarnings("NewApi")
+ @RequiresFeature(name = WebViewFeature.VISUAL_STATE_CALLBACK,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
public static void postVisualStateCallback(@NonNull WebView webview, long requestId,
@NonNull final VisualStateCallback callback) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ WebViewFeatureInternal webViewFeature =
+ WebViewFeatureInternal.getFeature(WebViewFeature.VISUAL_STATE_CALLBACK);
+ if (webViewFeature.isSupportedByFramework()) {
webview.postVisualStateCallback(requestId,
new android.webkit.WebView.VisualStateCallback() {
@Override
@@ -118,10 +136,11 @@
callback.onComplete(l);
}
});
- } else {
- // TODO(gsennton): guard with if WebViewApk.hasFeature(POSTVISUALSTATECALLBACK)
+ } else if (webViewFeature.isSupportedByWebView()) {
checkThread(webview);
getProvider(webview).insertVisualStateCallback(requestId, callback);
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
}
}
@@ -143,12 +162,19 @@
* @param callback will be called on the UI thread with {@code true} if initialization is
* successful, {@code false} otherwise.
*/
+ @SuppressLint("NewApi")
+ @RequiresFeature(name = WebViewFeature.START_SAFE_BROWSING,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
public static void startSafeBrowsing(@NonNull Context context,
@Nullable ValueCallback<Boolean> callback) {
- if (Build.VERSION.SDK_INT >= 27) {
+ WebViewFeatureInternal webviewFeature =
+ WebViewFeatureInternal.getFeature(WebViewFeature.START_SAFE_BROWSING);
+ if (webviewFeature.isSupportedByFramework()) {
WebView.startSafeBrowsing(context, callback);
- } else { // TODO(gsennton): guard with WebViewApk.hasFeature(SafeBrowsing)
+ } else if (webviewFeature.isSupportedByWebView()) {
getFactory().getStatics().initSafeBrowsing(context, callback);
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
}
}
@@ -175,12 +201,19 @@
* whitelist. It will be called with {@code false} if any hosts are malformed. The callback
* will be run on the UI thread
*/
+ @SuppressLint("NewApi")
+ @RequiresFeature(name = WebViewFeature.SAFE_BROWSING_WHITELIST,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
public static void setSafeBrowsingWhitelist(@NonNull List<String> hosts,
@Nullable ValueCallback<Boolean> callback) {
- if (Build.VERSION.SDK_INT >= 27) {
+ WebViewFeatureInternal webviewFeature =
+ WebViewFeatureInternal.getFeature(WebViewFeature.SAFE_BROWSING_WHITELIST);
+ if (webviewFeature.isSupportedByFramework()) {
WebView.setSafeBrowsingWhitelist(hosts, callback);
- } else { // TODO(gsennton): guard with WebViewApk.hasFeature(SafeBrowsing)
+ } else if (webviewFeature.isSupportedByWebView()) {
getFactory().getStatics().setSafeBrowsingWhitelist(hosts, callback);
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
}
}
@@ -189,12 +222,110 @@
*
* @return the url pointing to a privacy policy document which can be displayed to users.
*/
+ @SuppressLint("NewApi")
@NonNull
+ @RequiresFeature(name = WebViewFeature.SAFE_BROWSING_PRIVACY_POLICY_URL,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
public static Uri getSafeBrowsingPrivacyPolicyUrl() {
- if (Build.VERSION.SDK_INT >= 27) {
+ WebViewFeatureInternal webviewFeature =
+ WebViewFeatureInternal.getFeature(WebViewFeature.SAFE_BROWSING_PRIVACY_POLICY_URL);
+ if (webviewFeature.isSupportedByFramework()) {
return WebView.getSafeBrowsingPrivacyPolicyUrl();
- } else { // TODO(gsennton): guard with WebViewApk.hasFeature(SafeBrowsing)
+ } else if (webviewFeature.isSupportedByWebView()) {
return getFactory().getStatics().getSafeBrowsingPrivacyPolicyUrl();
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ /**
+ * If WebView has already been loaded into the current process this method will return the
+ * package that was used to load it. Otherwise, the package that would be used if the WebView
+ * was loaded right now will be returned; this does not cause WebView to be loaded, so this
+ * information may become outdated at any time.
+ * The WebView package changes either when the current WebView package is updated, disabled, or
+ * uninstalled. It can also be changed through a Developer Setting.
+ * If the WebView package changes, any app process that has loaded WebView will be killed. The
+ * next time the app starts and loads WebView it will use the new WebView package instead.
+ * @return the current WebView package, or {@code null} if there is none.
+ */
+ // Note that this API is not protected by a {@link androidx.webkit.WebViewFeature} since
+ // this feature is not dependent on the WebView APK.
+ @Nullable
+ public static PackageInfo getCurrentWebViewPackage(@NonNull Context context) {
+ // There was no WebView Package before Lollipop, the WebView code was part of the framework
+ // back then.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ return null;
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ return WebView.getCurrentWebViewPackage();
+ } else { // L-N
+ try {
+ PackageInfo loadedWebViewPackageInfo = getLoadedWebViewPackageInfo();
+ if (loadedWebViewPackageInfo != null) return loadedWebViewPackageInfo;
+ } catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException
+ | NoSuchMethodException e) {
+ return null;
+ }
+
+ // If WebViewFactory.getLoadedPackageInfo() returns null then WebView hasn't been loaded
+ // yet, in that case we need to fetch the name of the WebView package, and fetch the
+ // corresponding PackageInfo through the PackageManager
+ return getNotYetLoadedWebViewPackageInfo(context);
+ }
+ }
+
+ /**
+ * Return the PackageInfo of the currently loaded WebView APK. This method uses reflection and
+ * propagates any exceptions thrown, to the caller.
+ */
+ private static PackageInfo getLoadedWebViewPackageInfo()
+ throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException,
+ IllegalAccessException {
+ Class<?> webViewFactoryClass = Class.forName("android.webkit.WebViewFactory");
+ PackageInfo webviewPackageInfo =
+ (PackageInfo) webViewFactoryClass.getMethod(
+ "getLoadedPackageInfo").invoke(null);
+ return webviewPackageInfo;
+ }
+
+ /**
+ * Return the PackageInfo of the WebView APK that would have been used as WebView implementation
+ * if WebView was to be loaded right now.
+ */
+ private static PackageInfo getNotYetLoadedWebViewPackageInfo(Context context) {
+ String webviewPackageName = null;
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
+ && Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
+ Class<?> webViewFactoryClass = null;
+ webViewFactoryClass = Class.forName("android.webkit.WebViewFactory");
+
+ webviewPackageName = (String) webViewFactoryClass.getMethod(
+ "getWebViewPackageName").invoke(null);
+ } else {
+ Class<?> webviewUpdateServiceClass =
+ Class.forName("android.webkit.WebViewUpdateService");
+ webviewPackageName = (String) webviewUpdateServiceClass.getMethod(
+ "getCurrentWebViewPackageName").invoke(null);
+ }
+ } catch (ClassNotFoundException e) {
+ return null;
+ } catch (IllegalAccessException e) {
+ return null;
+ } catch (InvocationTargetException e) {
+ return null;
+ } catch (NoSuchMethodException e) {
+ return null;
+ }
+ if (webviewPackageName == null) return null;
+ PackageManager pm = context.getPackageManager();
+ try {
+ return pm.getPackageInfo(webviewPackageName, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
}
}
@@ -202,7 +333,58 @@
return new WebViewProviderAdapter(createProvider(webview));
}
- private static WebViewProviderFactoryAdapter getFactory() {
+ /**
+ * Creates a message channel to communicate with JS and returns the message
+ * ports that represent the endpoints of this message channel. The HTML5 message
+ * channel functionality is described
+ * <a href="https://html.spec.whatwg.org/multipage/comms.html#messagechannel">here
+ * </a>
+ *
+ * <p>The returned message channels are entangled and already in started state.
+ *
+ * @return an array of size two, containing the two message ports that form the message channel.
+ */
+ public static @NonNull WebMessagePortCompat[] createWebMessageChannel(
+ @NonNull WebView webview) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ return WebMessagePortImpl.portsToCompat(webview.createWebMessageChannel());
+ } else { // TODO(gsennton) add reflection-based implementation
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ /**
+ * Post a message to main frame. The embedded application can restrict the
+ * messages to a certain target origin. See
+ * <a href="https://html.spec.whatwg.org/multipage/comms.html#posting-messages">
+ * HTML5 spec</a> for how target origin can be used.
+ * <p>
+ * A target origin can be set as a wildcard ("*"). However this is not recommended.
+ * See the page above for security issues.
+ *
+ * @param message the WebMessage
+ * @param targetOrigin the target origin.
+ */
+ public static void postWebMessage(@NonNull WebView webview, @NonNull WebMessageCompat message,
+ @NonNull Uri targetOrigin) {
+ // The wildcard ("*") Uri was first supported in WebView 60, see
+ // crrev/5ec5b67cbab33cea51b0ee11a286c885c2de4d5d, so on some Android versions using "*"
+ // won't work. WebView has always supported using an empty Uri "" as a wildcard - so convert
+ // "*" into "" here.
+ if (WILDCARD_URI.equals(targetOrigin)) {
+ targetOrigin = EMPTY_URI;
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ webview.postWebMessage(
+ WebMessagePortImpl.compatToFrameworkMessage(message),
+ targetOrigin);
+ } else { // TODO(gsennton) add reflection-based implementation
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ private static WebViewProviderFactory getFactory() {
return WebViewGlueCommunicator.getFactory();
}
@@ -213,11 +395,11 @@
@SuppressWarnings("NewApi")
private static void checkThread(WebView webview) {
if (BuildCompat.isAtLeastP()) {
- if (webview.getLooper() != Looper.myLooper()) {
+ if (webview.getWebViewLooper() != Looper.myLooper()) {
throw new RuntimeException("A WebView method was called on thread '"
+ Thread.currentThread().getName() + "'. "
+ "All WebView methods must be called on the same thread. "
- + "(Expected Looper " + webview.getLooper() + " called on "
+ + "(Expected Looper " + webview.getWebViewLooper() + " called on "
+ Looper.myLooper() + ", FYI main Looper is " + Looper.getMainLooper()
+ ")");
}
diff --git a/webkit/src/main/java/androidx/webkit/WebViewFeature.java b/webkit/src/main/java/androidx/webkit/WebViewFeature.java
new file mode 100644
index 0000000..e750295
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/WebViewFeature.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import android.content.Context;
+import android.webkit.ValueCallback;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebSettings;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.StringDef;
+import androidx.webkit.internal.WebViewFeatureInternal;
+
+import org.chromium.support_lib_boundary.util.Features;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.List;
+
+/**
+ * Utility class for checking which WebView Support Library features are supported on the device.
+ */
+public class WebViewFeature {
+
+ private WebViewFeature() {}
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @StringDef(value = {
+ VISUAL_STATE_CALLBACK,
+ OFF_SCREEN_PRERASTER,
+ SAFE_BROWSING_ENABLE,
+ DISABLED_ACTION_MODE_MENU_ITEMS,
+ START_SAFE_BROWSING,
+ SAFE_BROWSING_WHITELIST,
+ SAFE_BROWSING_PRIVACY_POLICY_URL,
+ SERVICE_WORKER_BASIC_USAGE,
+ SERVICE_WORKER_CACHE_MODE,
+ SERVICE_WORKER_CONTENT_ACCESS,
+ SERVICE_WORKER_FILE_ACCESS,
+ SERVICE_WORKER_BLOCK_NETWORK_LOADS,
+ SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST,
+ RECEIVE_WEB_RESOURCE_ERROR,
+ RECEIVE_HTTP_ERROR,
+ SHOULD_OVERRIDE_WITH_REDIRECTS,
+ SAFE_BROWSING_HIT,
+ WEB_RESOURCE_REQUEST_IS_REDIRECT,
+ WEB_RESOURCE_ERROR_GET_DESCRIPTION,
+ WEB_RESOURCE_ERROR_GET_CODE,
+ SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY,
+ SAFE_BROWSING_RESPONSE_PROCEED,
+ SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @Target({ElementType.PARAMETER, ElementType.METHOD})
+ public @interface WebViewSupportFeature {}
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link androidx.webkit.WebViewCompat#postVisualStateCallback(android.webkit.WebView, long,
+ * WebViewCompat.VisualStateCallback)}, and {@link
+ * WebViewClientCompat#onPageCommitVisible(
+ * android.webkit.WebView, String)}.
+ */
+ public static final String VISUAL_STATE_CALLBACK = Features.VISUAL_STATE_CALLBACK;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link androidx.webkit.WebSettingsCompat#getOffscreenPreRaster(WebSettings)}, and
+ * {@link androidx.webkit.WebSettingsCompat#setOffscreenPreRaster(WebSettings, boolean)}.
+ */
+ public static final String OFF_SCREEN_PRERASTER = Features.OFF_SCREEN_PRERASTER;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link androidx.webkit.WebSettingsCompat#getSafeBrowsingEnabled(WebSettings)}, and
+ * {@link androidx.webkit.WebSettingsCompat#setSafeBrowsingEnabled(WebSettings, boolean)}.
+ */
+ public static final String SAFE_BROWSING_ENABLE = Features.SAFE_BROWSING_ENABLE;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link androidx.webkit.WebSettingsCompat#getDisabledActionModeMenuItems(WebSettings)}, and
+ * {@link androidx.webkit.WebSettingsCompat#setDisabledActionModeMenuItems(WebSettings, int)}.
+ */
+ public static final String DISABLED_ACTION_MODE_MENU_ITEMS =
+ Features.DISABLED_ACTION_MODE_MENU_ITEMS;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link androidx.webkit.WebViewCompat#startSafeBrowsing(Context, ValueCallback)}.
+ */
+ public static final String START_SAFE_BROWSING = Features.START_SAFE_BROWSING;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link androidx.webkit.WebViewCompat#setSafeBrowsingWhitelist(List, ValueCallback)}.
+ */
+ public static final String SAFE_BROWSING_WHITELIST = Features.SAFE_BROWSING_WHITELIST;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link WebViewCompat#getSafeBrowsingPrivacyPolicyUrl()}.
+ */
+ public static final String SAFE_BROWSING_PRIVACY_POLICY_URL =
+ Features.SAFE_BROWSING_PRIVACY_POLICY_URL;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link ServiceWorkerControllerCompat#getInstance()}.
+ */
+ public static final String SERVICE_WORKER_BASIC_USAGE = Features.SERVICE_WORKER_BASIC_USAGE;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link ServiceWorkerWebSettingsCompat#getCacheMode()}, and
+ * {@link ServiceWorkerWebSettingsCompat#setCacheMode(int)}.
+ */
+ public static final String SERVICE_WORKER_CACHE_MODE = Features.SERVICE_WORKER_CACHE_MODE;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link ServiceWorkerWebSettingsCompat#getAllowContentAccess()}, and
+ * {@link ServiceWorkerWebSettingsCompat#setAllowContentAccess(boolean)}.
+ */
+ public static final String SERVICE_WORKER_CONTENT_ACCESS =
+ Features.SERVICE_WORKER_CONTENT_ACCESS;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link ServiceWorkerWebSettingsCompat#getAllowFileAccess()}, and
+ * {@link ServiceWorkerWebSettingsCompat#setAllowFileAccess(boolean)}.
+ */
+ public static final String SERVICE_WORKER_FILE_ACCESS = Features.SERVICE_WORKER_FILE_ACCESS;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link ServiceWorkerWebSettingsCompat#getBlockNetworkLoads()}, and
+ * {@link ServiceWorkerWebSettingsCompat#setBlockNetworkLoads(boolean)}.
+ */
+ public static final String SERVICE_WORKER_BLOCK_NETWORK_LOADS =
+ Features.SERVICE_WORKER_BLOCK_NETWORK_LOADS;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link ServiceWorkerClientCompat#shouldInterceptRequest(WebResourceRequest)}.
+ */
+ public static final String SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST =
+ Features.SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link WebViewClientCompat#onReceivedError(android.webkit.WebView, WebResourceRequest,
+ * WebResourceErrorCompat)}.
+ */
+ public static final String RECEIVE_WEB_RESOURCE_ERROR = Features.RECEIVE_WEB_RESOURCE_ERROR;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link WebViewClientCompat#onReceivedHttpError(android.webkit.WebView, WebResourceRequest,
+ * WebResourceResponse)}.
+ */
+ public static final String RECEIVE_HTTP_ERROR = Features.RECEIVE_HTTP_ERROR;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link WebViewClientCompat#shouldOverrideUrlLoading(android.webkit.WebView,
+ * WebResourceRequest)}.
+ */
+ public static final String SHOULD_OVERRIDE_WITH_REDIRECTS =
+ Features.SHOULD_OVERRIDE_WITH_REDIRECTS;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link WebViewClientCompat#onSafeBrowsingHit(android.webkit.WebView,
+ * WebResourceRequest, int, SafeBrowsingResponseCompat)}.
+ */
+ public static final String SAFE_BROWSING_HIT = Features.SAFE_BROWSING_HIT;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link WebResourceRequestCompat#isRedirect(WebResourceRequest)}.
+ */
+ public static final String WEB_RESOURCE_REQUEST_IS_REDIRECT =
+ Features.WEB_RESOURCE_REQUEST_IS_REDIRECT;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link WebResourceErrorCompat#getDescription()}.
+ */
+ public static final String WEB_RESOURCE_ERROR_GET_DESCRIPTION =
+ Features.WEB_RESOURCE_ERROR_GET_DESCRIPTION;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link WebResourceErrorCompat#getErrorCode()}.
+ */
+ public static final String WEB_RESOURCE_ERROR_GET_CODE =
+ Features.WEB_RESOURCE_ERROR_GET_CODE;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link SafeBrowsingResponseCompat#backToSafety(boolean)}.
+ */
+ public static final String SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY =
+ Features.SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link SafeBrowsingResponseCompat#proceed(boolean)}.
+ */
+ public static final String SAFE_BROWSING_RESPONSE_PROCEED =
+ Features.SAFE_BROWSING_RESPONSE_PROCEED;
+
+ /**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link SafeBrowsingResponseCompat#showInterstitial(boolean)}.
+ */
+ public static final String SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL =
+ Features.SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL;
+
+ /**
+ * Return whether a feature is supported at run-time. This depends on the Android version of the
+ * device and the WebView APK on the device.
+ */
+ public static boolean isFeatureSupported(@NonNull @WebViewSupportFeature String feature) {
+ WebViewFeatureInternal webviewFeature = WebViewFeatureInternal.getFeature(feature);
+ return webviewFeature.isSupportedByFramework() || webviewFeature.isSupportedByWebView();
+ }
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/FrameworkServiceWorkerClient.java b/webkit/src/main/java/androidx/webkit/internal/FrameworkServiceWorkerClient.java
new file mode 100644
index 0000000..c28346e
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/FrameworkServiceWorkerClient.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit.internal;
+
+import android.os.Build;
+import android.webkit.ServiceWorkerClient;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+
+import androidx.annotation.RequiresApi;
+import androidx.webkit.ServiceWorkerClientCompat;
+
+/**
+ * A shim class that implements {@link ServiceWorkerClient} by delegating to a
+ * {@link ServiceWorkerClientCompat}.
+ * This class is used on up-to-date devices to avoid using reflection to call into WebView APK code.
+ */
+@RequiresApi(Build.VERSION_CODES.N)
+public class FrameworkServiceWorkerClient extends ServiceWorkerClient {
+ private final ServiceWorkerClientCompat mImpl;
+
+ public FrameworkServiceWorkerClient(ServiceWorkerClientCompat impl) {
+ mImpl = impl;
+ }
+
+ @Override
+ public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) {
+ return mImpl.shouldInterceptRequest(request);
+ }
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/IncompatibleApkWebViewProviderFactory.java b/webkit/src/main/java/androidx/webkit/internal/IncompatibleApkWebViewProviderFactory.java
new file mode 100644
index 0000000..71d5768
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/IncompatibleApkWebViewProviderFactory.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit.internal;
+
+import android.webkit.WebView;
+
+import org.chromium.support_lib_boundary.ServiceWorkerControllerBoundaryInterface;
+import org.chromium.support_lib_boundary.StaticsBoundaryInterface;
+import org.chromium.support_lib_boundary.WebViewProviderBoundaryInterface;
+import org.chromium.support_lib_boundary.WebkitToCompatConverterBoundaryInterface;
+
+/**
+ * This is a stub class used when the WebView Support Library is invoked on a device incompatible
+ * with the library (either a pre-L device or a device without a compatible WebView APK).
+ * The only method in this class that should be called is {@link #getWebViewFeatures()}.
+ */
+public class IncompatibleApkWebViewProviderFactory implements WebViewProviderFactory {
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+ private static final String UNSUPPORTED_EXCEPTION_EXPLANATION =
+ "This should never happen, if this method was called it means we're trying to reach "
+ + "into WebView APK code on an incompatible device. This most likely means the current "
+ + "method is being called too early, or is being called on start-up rather than lazily";
+
+ @Override
+ public WebViewProviderBoundaryInterface createWebView(WebView webview) {
+ throw new UnsupportedOperationException(UNSUPPORTED_EXCEPTION_EXPLANATION);
+ }
+
+ @Override
+ public WebkitToCompatConverterBoundaryInterface getWebkitToCompatConverter() {
+ throw new UnsupportedOperationException(UNSUPPORTED_EXCEPTION_EXPLANATION);
+ }
+
+ @Override
+ public StaticsBoundaryInterface getStatics() {
+ throw new UnsupportedOperationException(UNSUPPORTED_EXCEPTION_EXPLANATION);
+ }
+
+ @Override
+ public String[] getWebViewFeatures() {
+ return EMPTY_STRING_ARRAY;
+ }
+
+ @Override
+ public ServiceWorkerControllerBoundaryInterface getServiceWorkerController() {
+ throw new UnsupportedOperationException(UNSUPPORTED_EXCEPTION_EXPLANATION);
+ }
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerClientAdapter.java b/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerClientAdapter.java
new file mode 100644
index 0000000..f96bbd1
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerClientAdapter.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit.internal;
+
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+
+import androidx.webkit.ServiceWorkerClientCompat;
+
+import org.chromium.support_lib_boundary.ServiceWorkerClientBoundaryInterface;
+import org.chromium.support_lib_boundary.util.Features;
+
+/**
+ * Adapter between {@link ServiceWorkerClientCompat} and
+ * {@link ServiceWorkerClientBoundaryInterface} (the corresponding interface shared with the support
+ * library glue in the WebView APK).
+ */
+public class ServiceWorkerClientAdapter implements ServiceWorkerClientBoundaryInterface {
+ private final ServiceWorkerClientCompat mClient;
+
+ public ServiceWorkerClientAdapter(ServiceWorkerClientCompat client) {
+ mClient = client;
+ }
+
+ @Override
+ public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) {
+ return mClient.shouldInterceptRequest(request);
+ }
+
+ @Override
+ public String[] getSupportedFeatures() {
+ return new String[] { Features.SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST };
+ }
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerControllerImpl.java b/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerControllerImpl.java
new file mode 100644
index 0000000..3787a92
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerControllerImpl.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit.internal;
+
+import android.annotation.SuppressLint;
+import android.webkit.ServiceWorkerController;
+
+import androidx.annotation.RequiresApi;
+import androidx.webkit.ServiceWorkerClientCompat;
+import androidx.webkit.ServiceWorkerControllerCompat;
+import androidx.webkit.ServiceWorkerWebSettingsCompat;
+
+import org.chromium.support_lib_boundary.ServiceWorkerControllerBoundaryInterface;
+import org.chromium.support_lib_boundary.util.BoundaryInterfaceReflectionUtil;
+
+/**
+ * Implementation of {@link ServiceWorkerControllerCompat}.
+ * This class uses either the framework, the WebView APK, or both, to implement
+ * {@link ServiceWorkerControllerCompat} functionality.
+ */
+public class ServiceWorkerControllerImpl extends ServiceWorkerControllerCompat {
+ private ServiceWorkerController mFrameworksImpl;
+ private ServiceWorkerControllerBoundaryInterface mBoundaryInterface;
+ private final ServiceWorkerWebSettingsCompat mWebSettings;
+
+ @SuppressLint("NewApi")
+ public ServiceWorkerControllerImpl() {
+ final WebViewFeatureInternal feature = WebViewFeatureInternal.SERVICE_WORKER_BASIC_USAGE;
+ if (feature.isSupportedByFramework()) {
+ mFrameworksImpl = ServiceWorkerController.getInstance();
+ // The current WebView APK might not be compatible with the support library, so set the
+ // boundary interface to null for now.
+ mBoundaryInterface = null;
+ mWebSettings = new ServiceWorkerWebSettingsImpl(
+ mFrameworksImpl.getServiceWorkerWebSettings());
+ } else if (feature.isSupportedByWebView()) {
+ mFrameworksImpl = null;
+ mBoundaryInterface = WebViewGlueCommunicator.getFactory().getServiceWorkerController();
+ mWebSettings = new ServiceWorkerWebSettingsImpl(
+ mBoundaryInterface.getServiceWorkerWebSettings());
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ @RequiresApi(24)
+ private ServiceWorkerController getFrameworksImpl() {
+ if (mFrameworksImpl == null) {
+ mFrameworksImpl = ServiceWorkerController.getInstance();
+ }
+ return mFrameworksImpl;
+ }
+
+ private ServiceWorkerControllerBoundaryInterface getBoundaryInterface() {
+ if (mBoundaryInterface == null) {
+ mBoundaryInterface = WebViewGlueCommunicator.getFactory().getServiceWorkerController();
+ }
+ return mBoundaryInterface;
+ }
+
+ @Override
+ public ServiceWorkerWebSettingsCompat getServiceWorkerWebSettings() {
+ return mWebSettings;
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void setServiceWorkerClient(ServiceWorkerClientCompat client) {
+ final WebViewFeatureInternal feature = WebViewFeatureInternal.SERVICE_WORKER_BASIC_USAGE;
+ if (feature.isSupportedByFramework()) {
+ getFrameworksImpl().setServiceWorkerClient(new FrameworkServiceWorkerClient(client));
+ } else if (feature.isSupportedByWebView()) {
+ getBoundaryInterface().setServiceWorkerClient(
+ BoundaryInterfaceReflectionUtil.createInvocationHandlerFor(
+ new ServiceWorkerClientAdapter(client)));
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerWebSettingsImpl.java b/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerWebSettingsImpl.java
new file mode 100644
index 0000000..cad3ccb
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerWebSettingsImpl.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit.internal;
+
+import android.annotation.SuppressLint;
+import android.webkit.ServiceWorkerWebSettings;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.webkit.ServiceWorkerWebSettingsCompat;
+
+import org.chromium.support_lib_boundary.ServiceWorkerWebSettingsBoundaryInterface;
+import org.chromium.support_lib_boundary.util.BoundaryInterfaceReflectionUtil;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Proxy;
+
+/**
+ * Implementation of {@link ServiceWorkerWebSettingsCompat}.
+ * This class uses either the framework, the WebView APK, or both, to implement
+ * {@link ServiceWorkerWebSettingsCompat} functionality.
+ */
+public class ServiceWorkerWebSettingsImpl extends ServiceWorkerWebSettingsCompat {
+ private ServiceWorkerWebSettings mFrameworksImpl;
+ private ServiceWorkerWebSettingsBoundaryInterface mBoundaryInterface;
+
+ /**
+ * This class handles three different scenarios:
+ * 1. The Android version on the device is high enough to support all APIs used.
+ * 2. The Android version on the device is too low to support any ServiceWorkerWebSettings APIs
+ * so we use the support library glue instead through
+ * {@link ServiceWorkerWebSettingsBoundaryInterface}.
+ * 3. The Android version on the device is high enough to support some ServiceWorkerWebSettings
+ * APIs, so we call into them using {@link android.webkit.ServiceWorkerWebSettings}, but the
+ * rest of the APIs are only supported by the support library glue, so whenever we call such an
+ * API we fetch a {@link ServiceWorkerWebSettingsBoundaryInterface} corresponding to our
+ * {@link android.webkit.ServiceWorkerWebSettings}.
+ */
+ public ServiceWorkerWebSettingsImpl(@NonNull ServiceWorkerWebSettings settings) {
+ mFrameworksImpl = settings;
+ }
+
+ public ServiceWorkerWebSettingsImpl(@NonNull InvocationHandler invocationHandler) {
+ mBoundaryInterface = BoundaryInterfaceReflectionUtil.castToSuppLibClass(
+ ServiceWorkerWebSettingsBoundaryInterface.class, invocationHandler);
+ }
+
+ @RequiresApi(24)
+ private ServiceWorkerWebSettings getFrameworksImpl() {
+ if (mFrameworksImpl == null) {
+ mFrameworksImpl =
+ WebViewGlueCommunicator.getCompatConverter().convertServiceWorkerSettings(
+ Proxy.getInvocationHandler(mBoundaryInterface));
+ }
+ return mFrameworksImpl;
+ }
+
+ private ServiceWorkerWebSettingsBoundaryInterface getBoundaryInterface() {
+ if (mBoundaryInterface == null) {
+ // If the boundary interface is null we must have a working frameworks implementation to
+ // convert into a boundary interface.
+ // The case of the boundary interface being null here only occurs if we created the
+ // ServiceWorkerWebSettingsImpl using a frameworks API, but now want to call an API on
+ // the ServiceWorkerWebSettingsImpl that is only supported by the support library glue.
+ // This could happen for example if we introduce a new ServiceWorkerWebSettings API in
+ // level 30 and we run the support library on an N device (whose framework supports
+ // ServiceWorkerWebSettings).
+ mBoundaryInterface = BoundaryInterfaceReflectionUtil.castToSuppLibClass(
+ ServiceWorkerWebSettingsBoundaryInterface.class,
+ WebViewGlueCommunicator.getCompatConverter().convertServiceWorkerSettings(
+ mFrameworksImpl));
+ }
+ return mBoundaryInterface;
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void setCacheMode(int mode) {
+ final WebViewFeatureInternal feature = WebViewFeatureInternal.SERVICE_WORKER_CACHE_MODE;
+ if (feature.isSupportedByFramework()) {
+ getFrameworksImpl().setCacheMode(mode);
+ } else if (feature.isSupportedByWebView()) {
+ getBoundaryInterface().setCacheMode(mode);
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public int getCacheMode() {
+ final WebViewFeatureInternal feature = WebViewFeatureInternal.SERVICE_WORKER_CACHE_MODE;
+ if (feature.isSupportedByFramework()) {
+ return getFrameworksImpl().getCacheMode();
+ } else if (feature.isSupportedByWebView()) {
+ return getBoundaryInterface().getCacheMode();
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void setAllowContentAccess(boolean allow) {
+ final WebViewFeatureInternal feature = WebViewFeatureInternal.SERVICE_WORKER_CONTENT_ACCESS;
+ if (feature.isSupportedByFramework()) {
+ getFrameworksImpl().setAllowContentAccess(allow);
+ } else if (feature.isSupportedByWebView()) {
+ getBoundaryInterface().setAllowContentAccess(allow);
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public boolean getAllowContentAccess() {
+ final WebViewFeatureInternal feature = WebViewFeatureInternal.SERVICE_WORKER_CONTENT_ACCESS;
+ if (feature.isSupportedByFramework()) {
+ return getFrameworksImpl().getAllowContentAccess();
+ } else if (feature.isSupportedByWebView()) {
+ return getBoundaryInterface().getAllowContentAccess();
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void setAllowFileAccess(boolean allow) {
+ final WebViewFeatureInternal feature = WebViewFeatureInternal.SERVICE_WORKER_FILE_ACCESS;
+ if (feature.isSupportedByFramework()) {
+ getFrameworksImpl().setAllowFileAccess(allow);
+ } else if (feature.isSupportedByWebView()) {
+ getBoundaryInterface().setAllowFileAccess(allow);
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public boolean getAllowFileAccess() {
+ final WebViewFeatureInternal feature = WebViewFeatureInternal.SERVICE_WORKER_FILE_ACCESS;
+ if (feature.isSupportedByFramework()) {
+ return getFrameworksImpl().getAllowFileAccess();
+ } else if (feature.isSupportedByWebView()) {
+ return getBoundaryInterface().getAllowFileAccess();
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void setBlockNetworkLoads(boolean flag) {
+ final WebViewFeatureInternal feature =
+ WebViewFeatureInternal.SERVICE_WORKER_BLOCK_NETWORK_LOADS;
+ if (feature.isSupportedByFramework()) {
+ getFrameworksImpl().setBlockNetworkLoads(flag);
+ } else if (feature.isSupportedByWebView()) {
+ getBoundaryInterface().setBlockNetworkLoads(flag);
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public boolean getBlockNetworkLoads() {
+ final WebViewFeatureInternal feature =
+ WebViewFeatureInternal.SERVICE_WORKER_BLOCK_NETWORK_LOADS;
+ if (feature.isSupportedByFramework()) {
+ return getFrameworksImpl().getBlockNetworkLoads();
+ } else if (feature.isSupportedByWebView()) {
+ return getBoundaryInterface().getBlockNetworkLoads();
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebMessagePortImpl.java b/webkit/src/main/java/androidx/webkit/internal/WebMessagePortImpl.java
new file mode 100644
index 0000000..2de5105
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/WebMessagePortImpl.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit.internal;
+
+import android.annotation.SuppressLint;
+import android.os.Build;
+import android.os.Handler;
+import android.webkit.WebMessage;
+import android.webkit.WebMessagePort;
+
+import androidx.annotation.RequiresApi;
+import androidx.webkit.WebMessageCompat;
+import androidx.webkit.WebMessagePortCompat;
+
+/**
+ * Implementation of {@link WebMessagePortCompat}.
+ * This class uses either the framework, the WebView APK, or both, to implement
+ * {@link WebMessagePortCompat} functionality.
+ */
+public class WebMessagePortImpl extends WebMessagePortCompat {
+ private final WebMessagePort mFrameworksImpl;
+ // TODO(gsennton) add WebMessagePortBoundaryInterface variable
+
+ public WebMessagePortImpl(WebMessagePort frameworksImpl) {
+ mFrameworksImpl = frameworksImpl;
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void postMessage(WebMessageCompat message) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ mFrameworksImpl.postMessage(compatToFrameworkMessage(message));
+ } else { // TODO(gsennton) add reflection-based implementation
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void close() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ mFrameworksImpl.close();
+ } else { // TODO(gsennton) add reflection-based implementation
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ @Override
+ public void setWebMessageCallback(final WebMessageCallbackCompat callback) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ mFrameworksImpl.setWebMessageCallback(new WebMessagePort.WebMessageCallback() {
+ @Override
+ @SuppressWarnings("NewApi")
+ public void onMessage(WebMessagePort port, WebMessage message) {
+ callback.onMessage(new WebMessagePortImpl(port),
+ frameworkMessageToCompat(message));
+ }
+ });
+ } else { // TODO(gsennton) add reflection-based implementation
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ @Override
+ public void setWebMessageCallback(Handler handler, final WebMessageCallbackCompat callback) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ mFrameworksImpl.setWebMessageCallback(new WebMessagePort.WebMessageCallback() {
+ @Override
+ @SuppressWarnings("NewApi")
+ public void onMessage(WebMessagePort port, WebMessage message) {
+ callback.onMessage(new WebMessagePortImpl(port),
+ frameworkMessageToCompat(message));
+ }
+ }, handler);
+ } else { // TODO(gsennton) add reflection-based implementation
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ @Override
+ public WebMessagePort getFrameworkPort() {
+ return mFrameworksImpl;
+ }
+
+ /**
+ * Convert an array of {@link WebMessagePort} objects into an array containing objects of the
+ * corresponding support library class {@link WebMessagePortCompat}.
+ */
+ public static WebMessagePortCompat[] portsToCompat(WebMessagePort[] ports) {
+ if (ports == null) return null;
+ WebMessagePortCompat[] compatPorts = new WebMessagePortCompat[ports.length];
+ for (int n = 0; n < ports.length; n++) {
+ compatPorts[n] = new WebMessagePortImpl(ports[n]);
+ }
+ return compatPorts;
+ }
+
+ /**
+ * Convert an array of {@link WebMessagePortCompat} objects into an array containing objects of
+ * the corresponding framework class {@link WebMessagePort}.
+ */
+ public static WebMessagePort[] compatToPorts(WebMessagePortCompat[] compatPorts) {
+ if (compatPorts == null) return null;
+ WebMessagePort[] ports = new WebMessagePort[compatPorts.length];
+ for (int n = 0; n < ports.length; n++) {
+ ports[n] = compatPorts[n].getFrameworkPort();
+ }
+ return ports;
+ }
+
+ /**
+ * Convert a {@link WebMessageCompat} into the corresponding framework class {@link WebMessage}.
+ */
+ @RequiresApi(23)
+ public static WebMessage compatToFrameworkMessage(WebMessageCompat message) {
+ return new WebMessage(
+ message.getData(),
+ compatToPorts(message.getPorts()));
+ }
+
+ /**
+ * Convert a {@link WebMessage} into the corresponding support library class
+ * {@link WebMessageCompat}.
+ */
+ @RequiresApi(23)
+ public static WebMessageCompat frameworkMessageToCompat(WebMessage message) {
+ return new WebMessageCompat(
+ message.getData(),
+ portsToCompat(message.getPorts()));
+ }
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebResourceErrorImpl.java b/webkit/src/main/java/androidx/webkit/internal/WebResourceErrorImpl.java
new file mode 100644
index 0000000..a27f61c
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/WebResourceErrorImpl.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit.internal;
+
+import android.annotation.SuppressLint;
+import android.webkit.WebResourceError;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.webkit.WebResourceErrorCompat;
+import androidx.webkit.WebViewFeature;
+
+import org.chromium.support_lib_boundary.WebResourceErrorBoundaryInterface;
+import org.chromium.support_lib_boundary.util.BoundaryInterfaceReflectionUtil;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Proxy;
+
+/**
+ * Implementation of {@link WebResourceErrorCompat}.
+ * This class uses either the framework, the WebView APK, or both, to implement
+ * {@link WebResourceErrorCompat} functionality.
+ *
+ */
+public class WebResourceErrorImpl extends WebResourceErrorCompat {
+ /**
+ * Frameworks implementation - do not use this directly, instead use
+ * {@link #getFrameworksImpl()} to ensure this variable has been instantiated correctly.
+ */
+ private WebResourceError mFrameworksImpl;
+
+ /**
+ * Support library glue implementation - do not use this directly, instead use
+ * {@link #getBoundaryInterface()} to ensure this variable has been instantiated correctly.
+ */
+ private WebResourceErrorBoundaryInterface mBoundaryInterface;
+
+ public WebResourceErrorImpl(@NonNull InvocationHandler invocationHandler) {
+ mBoundaryInterface = BoundaryInterfaceReflectionUtil.castToSuppLibClass(
+ WebResourceErrorBoundaryInterface.class, invocationHandler);
+ }
+
+ public WebResourceErrorImpl(@NonNull WebResourceError error) {
+ mFrameworksImpl = error;
+ }
+
+ @RequiresApi(23)
+ private WebResourceError getFrameworksImpl() {
+ if (mFrameworksImpl == null) {
+ mFrameworksImpl = WebViewGlueCommunicator.getCompatConverter().convertWebResourceError(
+ Proxy.getInvocationHandler(mBoundaryInterface));
+ }
+ return mFrameworksImpl;
+ }
+
+ private WebResourceErrorBoundaryInterface getBoundaryInterface() {
+ if (mBoundaryInterface == null) {
+ mBoundaryInterface = BoundaryInterfaceReflectionUtil.castToSuppLibClass(
+ WebResourceErrorBoundaryInterface.class,
+ WebViewGlueCommunicator.getCompatConverter().convertWebResourceError(
+ mFrameworksImpl));
+ }
+ return mBoundaryInterface;
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public int getErrorCode() {
+ final WebViewFeatureInternal feature =
+ WebViewFeatureInternal.getFeature(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE);
+ if (feature.isSupportedByFramework()) {
+ return getFrameworksImpl().getErrorCode();
+ } else if (feature.isSupportedByWebView()) {
+ return getBoundaryInterface().getErrorCode();
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public CharSequence getDescription() {
+ final WebViewFeatureInternal feature = WebViewFeatureInternal.getFeature(
+ WebViewFeature.WEB_RESOURCE_ERROR_GET_DESCRIPTION);
+ if (feature.isSupportedByFramework()) {
+ return getFrameworksImpl().getDescription();
+ } else if (feature.isSupportedByWebView()) {
+ return getBoundaryInterface().getDescription();
+ }
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebResourceRequestAdapter.java b/webkit/src/main/java/androidx/webkit/internal/WebResourceRequestAdapter.java
new file mode 100644
index 0000000..0d6a05d
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/WebResourceRequestAdapter.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit.internal;
+
+import android.webkit.WebResourceRequest;
+
+import org.chromium.support_lib_boundary.WebResourceRequestBoundaryInterface;
+
+/**
+ * Adapter between {@link androidx.webkit.WebResourceRequestCompat} and
+ * {@link org.chromium.support_lib_boundary.WebResourceRequestBoundaryInterface}.
+ */
+public class WebResourceRequestAdapter {
+ private final WebResourceRequestBoundaryInterface mBoundaryInterface;
+
+ public WebResourceRequestAdapter(WebResourceRequestBoundaryInterface boundaryInterface) {
+ mBoundaryInterface = boundaryInterface;
+ }
+
+ /**
+ * Adapter method for
+ * {@link androidx.webkit.WebResourceRequestCompat#isRedirect(WebResourceRequest)}.
+ */
+ public boolean isRedirect() {
+ return mBoundaryInterface.isRedirect();
+ }
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java b/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
new file mode 100644
index 0000000..916da9d
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit.internal;
+
+import android.content.Context;
+import android.os.Build;
+import android.webkit.ValueCallback;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebSettings;
+
+import androidx.webkit.ServiceWorkerClientCompat;
+import androidx.webkit.WebResourceRequestCompat;
+import androidx.webkit.WebViewClientCompat;
+import androidx.webkit.WebViewCompat;
+import androidx.webkit.WebViewFeature;
+import androidx.webkit.WebViewFeature.WebViewSupportFeature;
+
+import java.util.List;
+
+/**
+ * Enum representing a WebView feature, this provides functionality for determining whether a
+ * feature is supported by the current framework and/or WebView APK.
+ */
+public enum WebViewFeatureInternal {
+ /**
+ * This feature covers
+ * {@link androidx.webkit.WebViewCompat#postVisualStateCallback(android.webkit.WebView, long,
+ * androidx.webkit.WebViewCompat.VisualStateCallback)}, and
+ * {@link WebViewClientCompat#onPageCommitVisible(android.webkit.WebView, String)}.
+ */
+ VISUAL_STATE_CALLBACK_FEATURE(WebViewFeature.VISUAL_STATE_CALLBACK, Build.VERSION_CODES.M),
+
+ /**
+ * This feature covers
+ * {@link androidx.webkit.WebSettingsCompat#getOffscreenPreRaster(WebSettings)}, and
+ * {@link androidx.webkit.WebSettingsCompat#setOffscreenPreRaster(WebSettings, boolean)}.
+ */
+ OFF_SCREEN_PRERASTER(WebViewFeature.OFF_SCREEN_PRERASTER, Build.VERSION_CODES.M),
+
+ /**
+ * This feature covers
+ * {@link androidx.webkit.WebSettingsCompat#getSafeBrowsingEnabled(WebSettings)}, and
+ * {@link androidx.webkit.WebSettingsCompat#setSafeBrowsingEnabled(WebSettings, boolean)}.
+ */
+ SAFE_BROWSING_ENABLE(WebViewFeature.SAFE_BROWSING_ENABLE, Build.VERSION_CODES.O),
+
+ /**
+ * This feature covers
+ * {@link androidx.webkit.WebSettingsCompat#getDisabledActionModeMenuItems(WebSettings)}, and
+ * {@link androidx.webkit.WebSettingsCompat#setDisabledActionModeMenuItems(WebSettings, int)}.
+ */
+ DISABLED_ACTION_MODE_MENU_ITEMS(WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS,
+ Build.VERSION_CODES.N),
+
+ /**
+ * This feature covers
+ * {@link androidx.webkit.WebViewCompat#startSafeBrowsing(Context, ValueCallback)}.
+ */
+ START_SAFE_BROWSING(WebViewFeature.START_SAFE_BROWSING, Build.VERSION_CODES.O_MR1),
+
+ /**
+ * This feature covers
+ * {@link androidx.webkit.WebViewCompat#setSafeBrowsingWhitelist(List, ValueCallback)}.
+ */
+ SAFE_BROWSING_WHITELIST(WebViewFeature.SAFE_BROWSING_WHITELIST, Build.VERSION_CODES.O_MR1),
+
+ /**
+ * This feature covers
+ * {@link WebViewCompat#getSafeBrowsingPrivacyPolicyUrl()}.
+ */
+ SAFE_BROWSING_PRIVACY_POLICY_URL(WebViewFeature.SAFE_BROWSING_PRIVACY_POLICY_URL,
+ Build.VERSION_CODES.O_MR1),
+
+ /**
+ * This feature covers
+ * {@link androidx.webkit.ServiceWorkerControllerCompat#getInstance()}.
+ */
+ SERVICE_WORKER_BASIC_USAGE(WebViewFeature.SERVICE_WORKER_BASIC_USAGE, Build.VERSION_CODES.N),
+
+ /**
+ * This feature covers
+ * {@link androidx.webkit.ServiceWorkerWebSettingsCompat#getCacheMode()}, and
+ * {@link androidx.webkit.ServiceWorkerWebSettingsCompat#setCacheMode(int)}.
+ */
+ SERVICE_WORKER_CACHE_MODE(WebViewFeature.SERVICE_WORKER_CACHE_MODE, Build.VERSION_CODES.N),
+
+ /**
+ * This feature covers
+ * {@link androidx.webkit.ServiceWorkerWebSettingsCompat#getAllowContentAccess()}, and
+ * {@link androidx.webkit.ServiceWorkerWebSettingsCompat#setAllowContentAccess(boolean)}.
+ */
+ SERVICE_WORKER_CONTENT_ACCESS(WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS,
+ Build.VERSION_CODES.N),
+
+ /**
+ * This feature covers
+ * {@link androidx.webkit.ServiceWorkerWebSettingsCompat#getAllowFileAccess()}, and
+ * {@link androidx.webkit.ServiceWorkerWebSettingsCompat#setAllowFileAccess(boolean)}.
+ */
+ SERVICE_WORKER_FILE_ACCESS(WebViewFeature.SERVICE_WORKER_FILE_ACCESS, Build.VERSION_CODES.N),
+
+ /**
+ * This feature covers
+ * {@link androidx.webkit.ServiceWorkerWebSettingsCompat#getBlockNetworkLoads()}, and
+ * {@link androidx.webkit.ServiceWorkerWebSettingsCompat#setBlockNetworkLoads(boolean)}.
+ */
+ SERVICE_WORKER_BLOCK_NETWORK_LOADS(WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS,
+ Build.VERSION_CODES.N),
+
+ /**
+ * This feature covers
+ * {@link ServiceWorkerClientCompat#shouldInterceptRequest(WebResourceRequest)}.
+ */
+ SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST(WebViewFeature.SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST,
+ Build.VERSION_CODES.N),
+
+ /**
+ * This feature covers
+ * {@link WebViewClientCompat#onReceivedError(android.webkit.WebView, WebResourceRequest,
+ * WebResourceErrorCompat)}.
+ */
+ RECEIVE_WEB_RESOURCE_ERROR(WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR, Build.VERSION_CODES.M),
+
+ /**
+ * This feature covers
+ * {@link WebViewClientCompat#onReceivedHttpError(android.webkit.WebView, WebResourceRequest,
+ * WebResourceResponse)}.
+ */
+ RECEIVE_HTTP_ERROR(WebViewFeature.RECEIVE_HTTP_ERROR, Build.VERSION_CODES.M),
+
+ /**
+ * This feature covers
+ * {@link WebViewClientCompat#shouldOverrideUrlLoading(android.webkit.WebView,
+ * WebResourceRequest)}.
+ */
+ SHOULD_OVERRIDE_WITH_REDIRECTS(WebViewFeature.SHOULD_OVERRIDE_WITH_REDIRECTS,
+ Build.VERSION_CODES.N),
+
+ /**
+ * This feature covers
+ * {@link WebViewClientCompat#onSafeBrowsingHit(android.webkit.WebView,
+ * WebResourceRequest, int, SafeBrowsingResponseCompat)}.
+ */
+ SAFE_BROWSING_HIT(WebViewFeature.SAFE_BROWSING_HIT, Build.VERSION_CODES.O_MR1),
+
+ /**
+ * This feature covers
+ * {@link WebResourceRequestCompat#isRedirect(WebResourceRequest)}.
+ */
+ WEB_RESOURCE_REQUEST_IS_REDIRECT(WebViewFeature.WEB_RESOURCE_REQUEST_IS_REDIRECT,
+ Build.VERSION_CODES.N),
+
+ /**
+ * This feature covers
+ * {@link WebResourceErrorCompat#getDescription()}.
+ */
+ WEB_RESOURCE_ERROR_GET_DESCRIPTION(WebViewFeature.WEB_RESOURCE_ERROR_GET_DESCRIPTION,
+ Build.VERSION_CODES.M),
+
+ /**
+ * This feature covers
+ * {@link WebResourceErrorCompat#getErrorCode()}.
+ */
+ WEB_RESOURCE_ERROR_GET_CODE(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE,
+ Build.VERSION_CODES.M),
+
+ /**
+ * This feature covers
+ * {@link SafeBrowsingResponseCompat#backToSafety(boolean)}.
+ */
+ SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY(WebViewFeature.SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY,
+ Build.VERSION_CODES.O_MR1),
+
+ /**
+ * This feature covers
+ * {@link SafeBrowsingResponseCompat#proceed(boolean)}.
+ */
+ SAFE_BROWSING_RESPONSE_PROCEED(WebViewFeature.SAFE_BROWSING_RESPONSE_PROCEED,
+ Build.VERSION_CODES.O_MR1),
+
+ /**
+ * This feature covers
+ * {@link SafeBrowsingResponseCompat#showInterstitial(boolean)}.
+ */
+ SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL(
+ WebViewFeature.SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL,
+ Build.VERSION_CODES.O_MR1);
+
+ private final String mFeatureValue;
+ private final int mOsVersion;
+
+ WebViewFeatureInternal(@WebViewSupportFeature String featureValue, int osVersion) {
+ mFeatureValue = featureValue;
+ mOsVersion = osVersion;
+ }
+
+ /**
+ * Return the {@link WebViewFeatureInternal} corresponding to {@param feature}.
+ */
+ public static WebViewFeatureInternal getFeature(@WebViewSupportFeature String feature) {
+ for (WebViewFeatureInternal internalFeature : WebViewFeatureInternal.values()) {
+ if (internalFeature.mFeatureValue.equals(feature)) return internalFeature;
+ }
+ throw new RuntimeException("Unknown feature " + feature);
+ }
+
+ /**
+ * Return whether this {@link WebViewFeature} is supported by the framework of the current
+ * device.
+ */
+ public boolean isSupportedByFramework() {
+ return Build.VERSION.SDK_INT >= mOsVersion;
+ }
+
+ /**
+ * Return whether this {@link WebViewFeature} is supported by the current WebView APK.
+ */
+ public boolean isSupportedByWebView() {
+ String[] webviewFeatures = LAZY_HOLDER.WEBVIEW_APK_FEATURES;
+ for (String webviewFeature : webviewFeatures) {
+ if (webviewFeature.equals(mFeatureValue)) return true;
+ }
+ return false;
+ }
+
+ private static class LAZY_HOLDER {
+ static final String[] WEBVIEW_APK_FEATURES =
+ WebViewGlueCommunicator.getFactory().getWebViewFeatures();
+ }
+
+
+ public static String[] getWebViewApkFeaturesForTesting() {
+ return LAZY_HOLDER.WEBVIEW_APK_FEATURES;
+ }
+
+ /**
+ * Utility method for throwing an exception explaining that the feature the app trying to use
+ * isn't supported.
+ */
+ public static UnsupportedOperationException getUnsupportedOperationException() {
+ return new UnsupportedOperationException("This method is not supported by the current "
+ + "version of the framework and the current WebView APK");
+ }
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebViewGlueCommunicator.java b/webkit/src/main/java/androidx/webkit/internal/WebViewGlueCommunicator.java
index 1325ed0..ea4460f 100644
--- a/webkit/src/main/java/androidx/webkit/internal/WebViewGlueCommunicator.java
+++ b/webkit/src/main/java/androidx/webkit/internal/WebViewGlueCommunicator.java
@@ -16,6 +16,7 @@
package androidx.webkit.internal;
+import android.os.Build;
import android.webkit.WebView;
import androidx.core.os.BuildCompat;
@@ -39,40 +40,57 @@
/**
* Fetch the one global support library WebViewProviderFactory from the WebView glue layer.
*/
- public static WebViewProviderFactoryAdapter getFactory() {
+ public static WebViewProviderFactory getFactory() {
return LAZY_FACTORY_HOLDER.INSTANCE;
}
public static WebkitToCompatConverter getCompatConverter() {
- return LAZY_FACTORY_HOLDER.COMPAT_CONVERTER;
+ return LAZY_COMPAT_CONVERTER_HOLDER.INSTANCE;
}
private static class LAZY_FACTORY_HOLDER {
- static final WebViewProviderFactoryAdapter INSTANCE =
- new WebViewProviderFactoryAdapter(
- WebViewGlueCommunicator.createGlueProviderFactory());
- static final WebkitToCompatConverter COMPAT_CONVERTER =
- new WebkitToCompatConverter(
- INSTANCE.getWebkitToCompatConverter());
+ private static final WebViewProviderFactory INSTANCE =
+ WebViewGlueCommunicator.createGlueProviderFactory();
}
- private static InvocationHandler fetchGlueProviderFactoryImpl() {
+ private static class LAZY_COMPAT_CONVERTER_HOLDER {
+ static final WebkitToCompatConverter INSTANCE = new WebkitToCompatConverter(
+ WebViewGlueCommunicator.getFactory().getWebkitToCompatConverter());
+ }
+
+ private static InvocationHandler fetchGlueProviderFactoryImpl() throws IllegalAccessException,
+ InvocationTargetException, ClassNotFoundException, NoSuchMethodException {
+ Class<?> glueFactoryProviderFetcherClass = Class.forName(
+ GLUE_FACTORY_PROVIDER_FETCHER_CLASS, false, getWebViewClassLoader());
+ Method createProviderFactoryMethod = glueFactoryProviderFetcherClass.getDeclaredMethod(
+ GLUE_FACTORY_PROVIDER_FETCHER_METHOD);
+ return (InvocationHandler) createProviderFactoryMethod.invoke(null);
+ }
+
+ private static WebViewProviderFactory createGlueProviderFactory() {
+ // We do not support pre-L devices since their WebView APKs cannot be updated.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ return new IncompatibleApkWebViewProviderFactory();
+ }
+ InvocationHandler invocationHandler;
try {
- Class<?> glueFactoryProviderFetcherClass = Class.forName(
- GLUE_FACTORY_PROVIDER_FETCHER_CLASS, false, getWebViewClassLoader());
- Method createProviderFactoryMethod = glueFactoryProviderFetcherClass.getDeclaredMethod(
- GLUE_FACTORY_PROVIDER_FETCHER_METHOD);
- return (InvocationHandler) createProviderFactoryMethod.invoke(null);
- } catch (IllegalAccessException | InvocationTargetException | ClassNotFoundException
- | NoSuchMethodException e) {
+ invocationHandler = fetchGlueProviderFactoryImpl();
+ // The only way we should fail to fetch the provider-factory is if the class we are
+ // calling into doesn't exist - any other kind of failure is unexpected and should cause
+ // a run-time exception.
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e);
+ } catch (ClassNotFoundException e) {
+ // If WebView APK support library glue entry point doesn't exist then return a Provider
+ // factory that declares that there are no features available.
+ return new IncompatibleApkWebViewProviderFactory();
+ } catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
- }
-
- private static WebViewProviderFactoryBoundaryInterface createGlueProviderFactory() {
- InvocationHandler invocationHandler = fetchGlueProviderFactoryImpl();
- return BoundaryInterfaceReflectionUtil.castToSuppLibClass(
- WebViewProviderFactoryBoundaryInterface.class, invocationHandler);
+ return new WebViewProviderFactoryAdapter(BoundaryInterfaceReflectionUtil.castToSuppLibClass(
+ WebViewProviderFactoryBoundaryInterface.class, invocationHandler));
}
/**
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactory.java b/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactory.java
new file mode 100644
index 0000000..5e4669a
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactory.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit.internal;
+
+import android.webkit.WebView;
+
+import org.chromium.support_lib_boundary.ServiceWorkerControllerBoundaryInterface;
+import org.chromium.support_lib_boundary.StaticsBoundaryInterface;
+import org.chromium.support_lib_boundary.WebViewProviderBoundaryInterface;
+import org.chromium.support_lib_boundary.WebkitToCompatConverterBoundaryInterface;
+
+/**
+ * Interface representing {@link android.webkit.WebViewProviderFactory}.
+ * On device with a compatible WebView APK this interface is implemented by a class defined in the
+ * WebView APK itself.
+ * On devices without a compatible WebView APK this interface is implemented by a stub class
+ * {@link androidx.webkit.internal.IncompatibleWebViewProviderFactory}.
+ */
+public interface WebViewProviderFactory {
+ /**
+ * Create a support library version of {@link android.webkit.WebViewProvider}.
+ */
+ WebViewProviderBoundaryInterface createWebView(WebView webview);
+
+ /**
+ * Create the boundary interface for {@link androidx.webkit.internal.WebkitToCompatConverter}
+ * which converts android.webkit classes into their corresponding support library classes.
+ */
+ WebkitToCompatConverterBoundaryInterface getWebkitToCompatConverter();
+
+ /**
+ * Fetch the boundary interface representing
+ * {@link android.webkit.WebViewFactoryProvider#Statics}.
+ */
+ StaticsBoundaryInterface getStatics();
+
+ /**
+ * Fetch the features supported by the current WebView APK.
+ */
+ String[] getWebViewFeatures();
+
+ /**
+ * Fetch the boundary interface representing {@link android.webkit.ServiceWorkerController}.
+ */
+ ServiceWorkerControllerBoundaryInterface getServiceWorkerController();
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactoryAdapter.java b/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactoryAdapter.java
index 62ce41e..43e5eae 100644
--- a/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactoryAdapter.java
+++ b/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactoryAdapter.java
@@ -18,6 +18,7 @@
import android.webkit.WebView;
+import org.chromium.support_lib_boundary.ServiceWorkerControllerBoundaryInterface;
import org.chromium.support_lib_boundary.StaticsBoundaryInterface;
import org.chromium.support_lib_boundary.WebViewProviderBoundaryInterface;
import org.chromium.support_lib_boundary.WebViewProviderFactoryBoundaryInterface;
@@ -28,7 +29,7 @@
* Adapter for WebViewProviderFactoryBoundaryInterface providing static WebView functionality
* similar to that provided by {@link android.webkit.WebViewFactoryProvider}.
*/
-public class WebViewProviderFactoryAdapter {
+public class WebViewProviderFactoryAdapter implements WebViewProviderFactory {
WebViewProviderFactoryBoundaryInterface mImpl;
public WebViewProviderFactoryAdapter(WebViewProviderFactoryBoundaryInterface impl) {
@@ -40,6 +41,7 @@
* {@link android.webkit.WebViewProvider} - the class used to implement
* {@link androidx.webkit.WebViewCompat}.
*/
+ @Override
public WebViewProviderBoundaryInterface createWebView(WebView webview) {
return BoundaryInterfaceReflectionUtil.castToSuppLibClass(
WebViewProviderBoundaryInterface.class, mImpl.createWebView(webview));
@@ -50,6 +52,7 @@
* {@link androidx.webkit.internal.WebkitToCompatConverter}, which converts android.webkit
* classes into their corresponding support library classes.
*/
+ @Override
public WebkitToCompatConverterBoundaryInterface getWebkitToCompatConverter() {
return BoundaryInterfaceReflectionUtil.castToSuppLibClass(
WebkitToCompatConverterBoundaryInterface.class, mImpl.getWebkitToCompatConverter());
@@ -59,8 +62,27 @@
* Adapter method for fetching the support library class representing
* {@link android.webkit.WebViewFactoryProvider#Statics}.
*/
+ @Override
public StaticsBoundaryInterface getStatics() {
return BoundaryInterfaceReflectionUtil.castToSuppLibClass(
StaticsBoundaryInterface.class, mImpl.getStatics());
}
+
+ /**
+ * Adapter method for fetching the features supported by the current WebView APK.
+ */
+ @Override
+ public String[] getWebViewFeatures() {
+ return mImpl.getSupportedFeatures();
+ }
+
+ /**
+ * Adapter method for fetching the support library class representing
+ * {@link android.webkit.ServiceWorkerController}.
+ */
+ @Override
+ public ServiceWorkerControllerBoundaryInterface getServiceWorkerController() {
+ return BoundaryInterfaceReflectionUtil.castToSuppLibClass(
+ ServiceWorkerControllerBoundaryInterface.class, mImpl.getServiceWorkerController());
+ }
}
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebkitToCompatConverter.java b/webkit/src/main/java/androidx/webkit/internal/WebkitToCompatConverter.java
index a07cf07..2e18bb9 100644
--- a/webkit/src/main/java/androidx/webkit/internal/WebkitToCompatConverter.java
+++ b/webkit/src/main/java/androidx/webkit/internal/WebkitToCompatConverter.java
@@ -16,12 +16,22 @@
package androidx.webkit.internal;
+import android.webkit.ServiceWorkerWebSettings;
+import android.webkit.WebResourceError;
+import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
+import androidx.annotation.RequiresApi;
+import androidx.webkit.WebResourceErrorCompat;
+
+import org.chromium.support_lib_boundary.ServiceWorkerWebSettingsBoundaryInterface;
+import org.chromium.support_lib_boundary.WebResourceRequestBoundaryInterface;
import org.chromium.support_lib_boundary.WebSettingsBoundaryInterface;
import org.chromium.support_lib_boundary.WebkitToCompatConverterBoundaryInterface;
import org.chromium.support_lib_boundary.util.BoundaryInterfaceReflectionUtil;
+import java.lang.reflect.InvocationHandler;
+
/**
* A class providing functionality for converting android.webkit classes into support library
* classes.
@@ -42,4 +52,55 @@
return new WebSettingsAdapter(BoundaryInterfaceReflectionUtil.castToSuppLibClass(
WebSettingsBoundaryInterface.class, mImpl.convertSettings(webSettings)));
}
+
+ /**
+ * Return a {@link WebResourceRequestAdapter} linked to the given {@link WebResourceRequest} so
+ * that calls on either of those objects affect the other object.
+ */
+ public WebResourceRequestAdapter convertWebResourceRequest(WebResourceRequest request) {
+ return new WebResourceRequestAdapter(BoundaryInterfaceReflectionUtil.castToSuppLibClass(
+ WebResourceRequestBoundaryInterface.class,
+ mImpl.convertWebResourceRequest(request)));
+ }
+
+ /**
+ * Return a {@link ServiceWorkerWebSettingsBoundaryInterface} linked to the given
+ * {@link ServiceWorkerWebSettings} such that calls on either of those objects affect the other
+ * object.
+ */
+ public InvocationHandler convertServiceWorkerSettings(
+ ServiceWorkerWebSettings settings) {
+ return mImpl.convertServiceWorkerSettings(settings);
+ }
+
+ /**
+ * Convert from an {@link InvocationHandler} representing an
+ * {@link androidx.webkit.ServiceWorkerWebSettingsCompat} into a
+ * {@link ServiceWorkerWebSettings}.
+ */
+ @RequiresApi(24)
+ public ServiceWorkerWebSettings convertServiceWorkerSettings(
+ /* SupportLibServiceWorkerSettings */ InvocationHandler serviceWorkerSettings) {
+ return (ServiceWorkerWebSettings) mImpl.convertServiceWorkerSettings(serviceWorkerSettings);
+ }
+
+ /**
+ * Return a {@link InvocationHandler} linked to the given
+ * {@link WebResourceError}such that calls on either of those objects affect the other
+ * object.
+ */
+ InvocationHandler convertWebResourceError(WebResourceError webResourceError) {
+ return mImpl.convertWebResourceError(webResourceError);
+ }
+
+
+ /**
+ * Convert from an {@link InvocationHandler} representing a {@link WebResourceErrorCompat} into
+ * a {@link WebResourceError}.
+ */
+ @RequiresApi(23)
+ WebResourceError convertWebResourceError(
+ /* SupportLibWebResourceError */ InvocationHandler webResourceError) {
+ return (WebResourceError) mImpl.convertWebResourceError(webResourceError);
+ }
}