aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2019-09-05 16:53:21 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2019-09-05 16:53:21 +0000
commit2b151947b067c5e959fcad1bbdfe47fd61902069 (patch)
treec24e107fad8d9af38ddf10d376f337969fdc01f6
parent48d3e658a387e158e6111d35f274696054a7f403 (diff)
parent5b2bb6743e06ffc5141237073518387b149da039 (diff)
downloadandroid-2b151947b067c5e959fcad1bbdfe47fd61902069.tar.gz
Merge "DO NOT MERGE - Merge Android 10 into master"ndk-sysroot-r21
-rw-r--r--content/SharingShortcuts/Application/.gitignore16
-rw-r--r--content/SharingShortcuts/Application/proguard-project.txt20
-rw-r--r--content/SharingShortcuts/Application/src/androidTest/java/com/example/android/sharingshortcuts/test/SampleTest.java45
-rw-r--r--content/SharingShortcuts/Application/src/main/AndroidManifest.xml83
-rw-r--r--content/SharingShortcuts/Application/src/main/java/com/example/android/sharingshortcuts/Contact.java95
-rw-r--r--content/SharingShortcuts/Application/src/main/java/com/example/android/sharingshortcuts/ContactViewBinder.java37
-rw-r--r--content/SharingShortcuts/Application/src/main/java/com/example/android/sharingshortcuts/MainActivity.java145
-rw-r--r--content/SharingShortcuts/Application/src/main/java/com/example/android/sharingshortcuts/SelectContactActivity.java98
-rw-r--r--content/SharingShortcuts/Application/src/main/java/com/example/android/sharingshortcuts/SendMessageActivity.java162
-rw-r--r--content/SharingShortcuts/Application/src/main/java/com/example/android/sharingshortcuts/SharingShortcutsManager.java110
-rw-r--r--content/SharingShortcuts/Application/src/main/res/layout/activity_main.xml51
-rw-r--r--content/SharingShortcuts/Application/src/main/res/layout/activity_select_contact.xml24
-rw-r--r--content/SharingShortcuts/Application/src/main/res/layout/activity_send_message.xml84
-rw-r--r--content/SharingShortcuts/Application/src/main/res/layout/item_contact.xml28
-rw-r--r--content/SharingShortcuts/Application/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 3328 bytes
-rw-r--r--content/SharingShortcuts/Application/src/main/res/mipmap-hdpi/logo_avatar.pngbin0 -> 1642 bytes
-rw-r--r--content/SharingShortcuts/Application/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 2032 bytes
-rw-r--r--content/SharingShortcuts/Application/src/main/res/mipmap-mdpi/logo_avatar.pngbin0 -> 1115 bytes
-rw-r--r--content/SharingShortcuts/Application/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 4314 bytes
-rw-r--r--content/SharingShortcuts/Application/src/main/res/mipmap-xhdpi/logo_avatar.pngbin0 -> 2195 bytes
-rw-r--r--content/SharingShortcuts/Application/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 7141 bytes
-rw-r--r--content/SharingShortcuts/Application/src/main/res/mipmap-xxhdpi/logo_avatar.pngbin0 -> 3327 bytes
-rw-r--r--content/SharingShortcuts/Application/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 10315 bytes
-rw-r--r--content/SharingShortcuts/Application/src/main/res/mipmap-xxxhdpi/logo_avatar.pngbin0 -> 4563 bytes
-rw-r--r--content/SharingShortcuts/Application/src/main/res/values/colors.xml25
-rw-r--r--content/SharingShortcuts/Application/src/main/res/values/dimens.xml19
-rw-r--r--content/SharingShortcuts/Application/src/main/res/values/strings.xml38
-rw-r--r--content/SharingShortcuts/Application/src/main/res/values/styles.xml31
-rw-r--r--content/SharingShortcuts/Application/src/main/res/xml/file_paths.xml20
-rw-r--r--content/SharingShortcuts/Application/src/main/res/xml/shortcuts.xml21
-rw-r--r--content/SharingShortcuts/build.gradle21
-rw-r--r--content/SharingShortcuts/buildSrc/build.gradle17
-rw-r--r--content/SharingShortcuts/gradle.properties22
-rw-r--r--content/SharingShortcuts/gradle/wrapper/gradle-wrapper.jarbin0 -> 49896 bytes
-rw-r--r--content/SharingShortcuts/gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xcontent/SharingShortcuts/gradlew164
-rw-r--r--content/SharingShortcuts/gradlew.bat90
-rw-r--r--content/SharingShortcuts/screenshots/1-main.pngbin0 -> 68662 bytes
-rw-r--r--content/SharingShortcuts/screenshots/2-intent.pngbin0 -> 128191 bytes
-rw-r--r--content/SharingShortcuts/screenshots/3-message.pngbin0 -> 85666 bytes
-rw-r--r--content/SharingShortcuts/screenshots/4-static_shortcuts.pngbin0 -> 367043 bytes
-rw-r--r--content/SharingShortcuts/screenshots/icon-web.pngbin0 -> 39530 bytes
-rw-r--r--content/SharingShortcuts/settings.gradle2
-rw-r--r--content/SharingShortcuts/template-params.xml114
-rw-r--r--media/MediaBrowserService/template-params.xml12
-rw-r--r--notification/Bubbles/.gitignore14
-rw-r--r--notification/Bubbles/app/.gitignore1
-rw-r--r--notification/Bubbles/app/build.gradle50
-rw-r--r--notification/Bubbles/app/proguard-rules.pro21
-rw-r--r--notification/Bubbles/app/src/androidTest/java/com/example/android/bubbles/BubbleActivityTest.kt44
-rw-r--r--notification/Bubbles/app/src/androidTest/java/com/example/android/bubbles/MainActivityTest.kt41
-rw-r--r--notification/Bubbles/app/src/main/AndroidManifest.xml91
-rw-r--r--notification/Bubbles/app/src/main/java/com/example/android/bubbles/BubbleActivity.kt58
-rw-r--r--notification/Bubbles/app/src/main/java/com/example/android/bubbles/MainActivity.kt115
-rw-r--r--notification/Bubbles/app/src/main/java/com/example/android/bubbles/NavigationController.kt46
-rw-r--r--notification/Bubbles/app/src/main/java/com/example/android/bubbles/VoiceCallActivity.kt49
-rw-r--r--notification/Bubbles/app/src/main/java/com/example/android/bubbles/data/Chat.kt44
-rw-r--r--notification/Bubbles/app/src/main/java/com/example/android/bubbles/data/ChatRepository.kt141
-rw-r--r--notification/Bubbles/app/src/main/java/com/example/android/bubbles/data/Contact.kt74
-rw-r--r--notification/Bubbles/app/src/main/java/com/example/android/bubbles/data/Message.kt40
-rw-r--r--notification/Bubbles/app/src/main/java/com/example/android/bubbles/data/NotificationHelper.kt195
-rw-r--r--notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/chat/ChatFragment.kt213
-rw-r--r--notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/chat/ChatViewModel.kt104
-rw-r--r--notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/chat/MessageAdapter.kt132
-rw-r--r--notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/main/ContactAdapter.kt72
-rw-r--r--notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/main/MainFragment.kt63
-rw-r--r--notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/main/MainViewModel.kt32
-rw-r--r--notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/photo/PhotoFragment.kt47
-rw-r--r--notification/Bubbles/app/src/main/res/drawable-nodpi/cat.jpgbin0 -> 41237 bytes
-rw-r--r--notification/Bubbles/app/src/main/res/drawable-nodpi/dog.jpgbin0 -> 30140 bytes
-rw-r--r--notification/Bubbles/app/src/main/res/drawable-nodpi/parrot.jpgbin0 -> 44701 bytes
-rw-r--r--notification/Bubbles/app/src/main/res/drawable-nodpi/sheep.jpgbin0 -> 32500 bytes
-rw-r--r--notification/Bubbles/app/src/main/res/drawable-nodpi/sheep_full.jpgbin0 -> 89073 bytes
-rw-r--r--notification/Bubbles/app/src/main/res/drawable/ic_launcher_foreground.xml18
-rw-r--r--notification/Bubbles/app/src/main/res/drawable/ic_message.xml9
-rw-r--r--notification/Bubbles/app/src/main/res/drawable/ic_open_in_new.xml10
-rw-r--r--notification/Bubbles/app/src/main/res/drawable/ic_send.xml9
-rw-r--r--notification/Bubbles/app/src/main/res/drawable/ic_voice_call.xml9
-rw-r--r--notification/Bubbles/app/src/main/res/drawable/message_incoming.xml36
-rw-r--r--notification/Bubbles/app/src/main/res/drawable/message_outgoing.xml36
-rw-r--r--notification/Bubbles/app/src/main/res/layout/bubble_activity.xml21
-rw-r--r--notification/Bubbles/app/src/main/res/layout/chat_fragment.xml74
-rw-r--r--notification/Bubbles/app/src/main/res/layout/chat_item.xml50
-rw-r--r--notification/Bubbles/app/src/main/res/layout/main_activity.xml77
-rw-r--r--notification/Bubbles/app/src/main/res/layout/main_fragment.xml24
-rw-r--r--notification/Bubbles/app/src/main/res/layout/message_item.xml28
-rw-r--r--notification/Bubbles/app/src/main/res/layout/photo_fragment.xml26
-rw-r--r--notification/Bubbles/app/src/main/res/layout/voice_call_activity.xml63
-rw-r--r--notification/Bubbles/app/src/main/res/menu/chat.xml11
-rw-r--r--notification/Bubbles/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml5
-rw-r--r--notification/Bubbles/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml5
-rw-r--r--notification/Bubbles/app/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 1006 bytes
-rw-r--r--notification/Bubbles/app/src/main/res/mipmap-hdpi/ic_launcher_round.pngbin0 -> 2832 bytes
-rw-r--r--notification/Bubbles/app/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 762 bytes
-rw-r--r--notification/Bubbles/app/src/main/res/mipmap-mdpi/ic_launcher_round.pngbin0 -> 1861 bytes
-rw-r--r--notification/Bubbles/app/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 1370 bytes
-rw-r--r--notification/Bubbles/app/src/main/res/mipmap-xhdpi/ic_launcher_round.pngbin0 -> 3886 bytes
-rw-r--r--notification/Bubbles/app/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 2056 bytes
-rw-r--r--notification/Bubbles/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.pngbin0 -> 6104 bytes
-rw-r--r--notification/Bubbles/app/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 2869 bytes
-rw-r--r--notification/Bubbles/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.pngbin0 -> 8607 bytes
-rw-r--r--notification/Bubbles/app/src/main/res/transition/app_bar.xml26
-rw-r--r--notification/Bubbles/app/src/main/res/transition/slide_bottom.xml21
-rw-r--r--notification/Bubbles/app/src/main/res/transition/slide_top.xml21
-rw-r--r--notification/Bubbles/app/src/main/res/values/colors.xml24
-rw-r--r--notification/Bubbles/app/src/main/res/values/dimens.xml26
-rw-r--r--notification/Bubbles/app/src/main/res/values/ic_launcher_background.xml4
-rw-r--r--notification/Bubbles/app/src/main/res/values/ids.xml19
-rw-r--r--notification/Bubbles/app/src/main/res/values/strings.xml29
-rw-r--r--notification/Bubbles/app/src/main/res/values/styles.xml31
-rw-r--r--notification/Bubbles/app/src/test/java/com/example/android/bubbles/LiveDataTestUtils.kt44
-rw-r--r--notification/Bubbles/app/src/test/java/com/example/android/bubbles/data/TestChatRepository.kt88
-rw-r--r--notification/Bubbles/app/src/test/java/com/example/android/bubbles/ui/chat/ChatViewModelTest.kt82
-rw-r--r--notification/Bubbles/app/src/test/java/com/example/android/bubbles/ui/main/MainViewModelTest.kt61
-rw-r--r--notification/Bubbles/build.gradle36
-rw-r--r--notification/Bubbles/buildSrc/build.gradle18
-rw-r--r--notification/Bubbles/gradle.properties21
-rw-r--r--notification/Bubbles/gradle/wrapper/gradle-wrapper.jarbin0 -> 55190 bytes
-rw-r--r--notification/Bubbles/gradle/wrapper/gradle-wrapper.properties5
-rwxr-xr-xnotification/Bubbles/gradlew172
-rw-r--r--notification/Bubbles/gradlew.bat84
-rw-r--r--notification/Bubbles/screenshots/bubble.pngbin0 -> 274153 bytes
-rw-r--r--notification/Bubbles/screenshots/chat.pngbin0 -> 110575 bytes
-rw-r--r--notification/Bubbles/screenshots/icon-web.pngbin0 -> 9128 bytes
-rw-r--r--notification/Bubbles/screenshots/main.pngbin0 -> 312178 bytes
-rw-r--r--notification/Bubbles/settings.gradle1
-rw-r--r--notification/Bubbles/template-params.xml114
-rw-r--r--projects.txt1
-rw-r--r--security/FingerprintDialog/Application/src/main/Android.mk11
-rw-r--r--security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/MainActivity.java6
-rw-r--r--security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/SettingsActivity.java2
-rw-r--r--security/FingerprintDialog/Application/src/main/res/layout/activity_main.xml2
-rw-r--r--ui/text/README.md47
-rw-r--r--ui/text/RoundedBackground-Kotlin/README.md93
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/build.gradle43
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/AndroidManifest.xml35
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/java/com/android/example/text/styling/roundedbg/app/MainActivity.kt30
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/res/drawable-v24/ic_launcher_foreground.xml50
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/res/drawable/ic_launcher_background.xml185
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/res/drawable/rounded.xml21
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/res/drawable/rounded_left.xml22
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/res/drawable/rounded_mid.xml20
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/res/drawable/rounded_right.xml22
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/res/layout/activity_main.xml131
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml20
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml20
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 3056 bytes
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-hdpi/ic_launcher_round.pngbin0 -> 5024 bytes
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 2096 bytes
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-mdpi/ic_launcher_round.pngbin0 -> 2858 bytes
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 4569 bytes
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-xhdpi/ic_launcher_round.pngbin0 -> 7098 bytes
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 6464 bytes
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.pngbin0 -> 10676 bytes
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 9250 bytes
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.pngbin0 -> 15523 bytes
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/res/values/colors.xml26
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/res/values/dimens.xml23
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/res/values/strings.xml32
-rw-r--r--ui/text/RoundedBackground-Kotlin/app/src/main/res/values/styles.xml42
-rw-r--r--ui/text/RoundedBackground-Kotlin/build.gradle49
-rw-r--r--ui/text/RoundedBackground-Kotlin/gradle.properties13
-rw-r--r--ui/text/RoundedBackground-Kotlin/gradle/wrapper/gradle-wrapper.jarbin0 -> 54708 bytes
-rw-r--r--ui/text/RoundedBackground-Kotlin/gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xui/text/RoundedBackground-Kotlin/gradlew172
-rw-r--r--ui/text/RoundedBackground-Kotlin/gradlew.bat84
-rw-r--r--ui/text/RoundedBackground-Kotlin/lib/.gitignore1
-rw-r--r--ui/text/RoundedBackground-Kotlin/lib/build.gradle43
-rw-r--r--ui/text/RoundedBackground-Kotlin/lib/src/main/AndroidManifest.xml18
-rw-r--r--ui/text/RoundedBackground-Kotlin/lib/src/main/java/com/android/example/text/styling/roundedbg/LayoutExtensions.kt91
-rw-r--r--ui/text/RoundedBackground-Kotlin/lib/src/main/java/com/android/example/text/styling/roundedbg/RoundedBgTextView.kt61
-rw-r--r--ui/text/RoundedBackground-Kotlin/lib/src/main/java/com/android/example/text/styling/roundedbg/TextRoundedBgAttributeReader.kt72
-rw-r--r--ui/text/RoundedBackground-Kotlin/lib/src/main/java/com/android/example/text/styling/roundedbg/TextRoundedBgHelper.kt102
-rw-r--r--ui/text/RoundedBackground-Kotlin/lib/src/main/java/com/android/example/text/styling/roundedbg/TextRoundedBgRenderer.kt209
-rw-r--r--ui/text/RoundedBackground-Kotlin/lib/src/main/res/drawable/rounded_text_bg.xml21
-rw-r--r--ui/text/RoundedBackground-Kotlin/lib/src/main/res/drawable/rounded_text_bg_left.xml22
-rw-r--r--ui/text/RoundedBackground-Kotlin/lib/src/main/res/drawable/rounded_text_bg_mid.xml20
-rw-r--r--ui/text/RoundedBackground-Kotlin/lib/src/main/res/drawable/rounded_text_bg_right.xml22
-rw-r--r--ui/text/RoundedBackground-Kotlin/lib/src/main/res/values/attrs.xml26
-rw-r--r--ui/text/RoundedBackground-Kotlin/lib/src/main/res/values/colors.xml20
-rw-r--r--ui/text/RoundedBackground-Kotlin/lib/src/main/res/values/dimens.xml20
-rw-r--r--ui/text/RoundedBackground-Kotlin/lib/src/main/res/values/styles.xml28
-rw-r--r--ui/text/RoundedBackground-Kotlin/settings.gradle1
-rw-r--r--ui/text/screenshots/lines.pngbin0 -> 42099 bytes
-rw-r--r--ui/text/screenshots/multi.pngbin0 -> 103725 bytes
-rw-r--r--ui/text/screenshots/rounded_bg.pngbin0 -> 130326 bytes
-rw-r--r--ui/text/screenshots/rtl.pngbin0 -> 64885 bytes
-rw-r--r--ui/text/screenshots/single.pngbin0 -> 24439 bytes
188 files changed, 6656 insertions, 54 deletions
diff --git a/content/SharingShortcuts/Application/.gitignore b/content/SharingShortcuts/Application/.gitignore
new file mode 100644
index 00000000..6eb878d4
--- /dev/null
+++ b/content/SharingShortcuts/Application/.gitignore
@@ -0,0 +1,16 @@
+# Copyright 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+src/template/
+src/common/
+build.gradle
diff --git a/content/SharingShortcuts/Application/proguard-project.txt b/content/SharingShortcuts/Application/proguard-project.txt
new file mode 100644
index 00000000..f2fe1559
--- /dev/null
+++ b/content/SharingShortcuts/Application/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/content/SharingShortcuts/Application/src/androidTest/java/com/example/android/sharingshortcuts/test/SampleTest.java b/content/SharingShortcuts/Application/src/androidTest/java/com/example/android/sharingshortcuts/test/SampleTest.java
new file mode 100644
index 00000000..0a5bd704
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/androidTest/java/com/example/android/sharingshortcuts/test/SampleTest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.sharingshortcuts;
+
+import android.test.ActivityInstrumentationTestCase2;
+
+/**
+ * Tests for DirectShare sample.
+ */
+public class SampleTest extends ActivityInstrumentationTestCase2<MainActivity> {
+
+ private MainActivity mTestActivity;
+
+ public SampleTest() {
+ super(MainActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mTestActivity = getActivity();
+ }
+
+ /**
+ * Test if the test fixture has been set up correctly.
+ */
+ public void testPreconditions() {
+ assertNotNull("mTestActivity is null", mTestActivity);
+ }
+
+}
diff --git a/content/SharingShortcuts/Application/src/main/AndroidManifest.xml b/content/SharingShortcuts/Application/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..d03e7c9d
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/AndroidManifest.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+ Copyright 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.example.android.sharingshortcuts"
+ android:versionCode="1"
+ android:versionName="1.0">
+
+ <application
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@style/SharingShortcutsTheme"
+ tools:ignore="GoogleAppIndexingWarning">
+
+ <activity
+ android:name=".MainActivity"
+ android:label="@string/app_name">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ <!-- Reference resource file where the app's shortcuts are defined -->
+ <meta-data
+ android:name="android.app.shortcuts"
+ android:resource="@xml/shortcuts" />
+ </activity>
+
+ <activity
+ android:name=".SendMessageActivity"
+ android:label="@string/app_name"
+ android:theme="@style/SharingShortcutsDialogTheme">
+ <!-- This activity can respond to Intents of type SEND and with text/plain data -->
+ <intent-filter>
+ <action android:name="android.intent.action.SEND" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:mimeType="text/plain" />
+ </intent-filter>
+ <!-- Only needed if you import the sharetarget AndroidX library that provides
+ backwards compatibility with the old DirectShare API.
+ The activity that receives the Sharing Shortcut intent needs to be taken into
+ account with this chooser target provider. -->
+ <meta-data
+ android:name="android.service.chooser.chooser_target_service"
+ android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
+ </activity>
+
+ <activity
+ android:name=".SelectContactActivity"
+ android:label="@string/app_name"
+ android:theme="@style/SharingShortcutsDialogTheme" />
+
+ <!-- Only needed if you want to add a thumbnail to the direct share.
+ FileProvider is a subclass of ContentProvider that facilitates secure sharing.
+ Here we specify a FileProvider for our app. -->
+ <provider
+ android:name="androidx.core.content.FileProvider"
+ android:authorities="com.example.android.sharingshortcuts.fileprovider"
+ android:exported="false"
+ android:grantUriPermissions="true">
+ <!-- Specify the directories the FileProvider can generate content URIs for. -->
+ <meta-data
+ android:name="android.support.FILE_PROVIDER_PATHS"
+ android:resource="@xml/file_paths" />
+ </provider>
+
+ </application>
+
+</manifest>
diff --git a/content/SharingShortcuts/Application/src/main/java/com/example/android/sharingshortcuts/Contact.java b/content/SharingShortcuts/Application/src/main/java/com/example/android/sharingshortcuts/Contact.java
new file mode 100644
index 00000000..155a8412
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/java/com/example/android/sharingshortcuts/Contact.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.sharingshortcuts;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Provides the list of dummy contacts. This sample implements this as constants, but real-life apps
+ * should use a database and such.
+ */
+public class Contact {
+
+ /**
+ * The list of dummy contacts.
+ */
+ public static final Contact[] CONTACTS = {
+ new Contact("Tereasa"),
+ new Contact("Chang"),
+ new Contact("Kory"),
+ new Contact("Clare"),
+ new Contact("Landon"),
+ new Contact("Kyle"),
+ new Contact("Deana"),
+ new Contact("Daria"),
+ new Contact("Melisa"),
+ new Contact("Sammie"),
+ };
+
+ /**
+ * The contact ID.
+ */
+ public static final String ID = "contact_id";
+
+ /**
+ * Representative invalid contact ID.
+ */
+ public static final int INVALID_ID = -1;
+
+ /**
+ * The name of this contact.
+ */
+ private final String mName;
+
+ /**
+ * Instantiates a new {@link Contact}.
+ *
+ * @param name The name of the contact.
+ */
+ public Contact(@NonNull String name) {
+ mName = name;
+ }
+
+ /**
+ * Finds a {@link Contact} specified by a contact ID.
+ *
+ * @param id The contact ID. This needs to be a valid ID.
+ * @return A {@link Contact}
+ */
+ public static Contact byId(int id) {
+ return CONTACTS[id];
+ }
+
+ /**
+ * Gets the name of this contact.
+ *
+ * @return The name of this contact.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Gets the icon of this contact.
+ *
+ * @return The icon.
+ */
+ public int getIcon() {
+ return R.mipmap.logo_avatar;
+ }
+
+}
diff --git a/content/SharingShortcuts/Application/src/main/java/com/example/android/sharingshortcuts/ContactViewBinder.java b/content/SharingShortcuts/Application/src/main/java/com/example/android/sharingshortcuts/ContactViewBinder.java
new file mode 100644
index 00000000..86925922
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/java/com/example/android/sharingshortcuts/ContactViewBinder.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.sharingshortcuts;
+
+import android.widget.TextView;
+
+/**
+ * A simple utility to bind a {@link TextView} with a {@link Contact}.
+ */
+public class ContactViewBinder {
+
+ /**
+ * Binds the {@code textView} with the specified {@code contact}.
+ *
+ * @param contact The contact.
+ * @param textView The TextView.
+ */
+ public static void bind(Contact contact, TextView textView) {
+ textView.setText(contact.getName());
+ textView.setCompoundDrawablesRelativeWithIntrinsicBounds(contact.getIcon(), 0, 0, 0);
+ }
+
+}
diff --git a/content/SharingShortcuts/Application/src/main/java/com/example/android/sharingshortcuts/MainActivity.java b/content/SharingShortcuts/Application/src/main/java/com/example/android/sharingshortcuts/MainActivity.java
new file mode 100644
index 00000000..17dfdc9f
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/java/com/example/android/sharingshortcuts/MainActivity.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.sharingshortcuts;
+
+import android.app.Activity;
+import android.content.ClipData;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.EditText;
+
+import androidx.core.content.FileProvider;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * Provides the landing screen of this sample. There is nothing particularly interesting here. All
+ * the codes related to the Direct Share feature are in {@link SharingShortcutsManager}.
+ */
+public class MainActivity extends Activity {
+
+ // Domain authority for our app FileProvider
+ private static final String FILE_PROVIDER_AUTHORITY =
+ "com.example.android.sharingshortcuts.fileprovider";
+
+ // Cache directory to store images
+ // This is the same path specified in the @xml/file_paths and accessed from the AndroidManifest
+ private static final String IMAGE_CACHE_DIR = "images";
+
+ // Name of the file to use for the thumbnail image
+ private static final String IMAGE_FILE = "image.png";
+
+ private EditText mEditBody;
+ private SharingShortcutsManager mSharingShortcutsManager;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ mEditBody = findViewById(R.id.body);
+ findViewById(R.id.share).setOnClickListener(mOnClickListener);
+
+ mSharingShortcutsManager = new SharingShortcutsManager();
+ mSharingShortcutsManager.pushDirectShareTargets(this);
+ }
+
+ private View.OnClickListener mOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.share:
+ share();
+ break;
+ }
+ }
+ };
+
+ /**
+ * Emits a sample share {@link Intent}.
+ */
+ private void share() {
+ Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND);
+ sharingIntent.setType("text/plain");
+ sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, mEditBody.getText().toString());
+ // (Optional) If you want a preview title, set it with Intent.EXTRA_TITLE
+ sharingIntent.putExtra(Intent.EXTRA_TITLE, getString(R.string.send_intent_title));
+
+ // (Optional) if you want a preview thumbnail, create a content URI and add it
+ // The system only supports content URIs
+ ClipData thumbnail = getClipDataThumbnail();
+ if (thumbnail != null) {
+ sharingIntent.setClipData(thumbnail);
+ sharingIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ }
+
+ startActivity(Intent.createChooser(sharingIntent, null));
+ }
+
+ /**
+ * Get ClipData thumbnail object that needs to be passed in the Intent.
+ * It stores the launcher icon in the cache and retrieves in a content URI.
+ * The ClipData object is created with the URI we get from the FileProvider.
+ * <p>
+ * For this to work, you need to configure a FileProvider in the project. We added it to the
+ * AndroidManifest.xml file where we can configure it. We added the images path where we
+ * save the image to the @xml/file_paths file which tells the FileProvider where we intend to
+ * request content URIs.
+ *
+ * @return thumbnail ClipData object to set in the sharing Intent.
+ */
+ private ClipData getClipDataThumbnail() {
+ try {
+ Uri contentUri = saveImageThumbnail();
+ return ClipData.newUri(getContentResolver(), null, contentUri);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ return null;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ /**
+ * Save our Launcher image to the cache and return it as a content URI.
+ *
+ * IMPORTANT: This could trigger StrictMode. Do not do this in your app.
+ * For the simplicity of the code sample, this is running on the Main thread
+ * but these tasks should be done in a background thread.
+ *
+ * @throws IOException if image couldn't be saved to the cache.
+ * @return image content Uri
+ */
+ private Uri saveImageThumbnail() throws IOException {
+ Bitmap bm = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);
+ File cachePath = new File(getCacheDir(), IMAGE_CACHE_DIR);
+ cachePath.mkdirs();
+ FileOutputStream stream = new FileOutputStream(cachePath + "/" + IMAGE_FILE);
+ bm.compress(Bitmap.CompressFormat.PNG, 100, stream);
+ stream.close();
+ File imagePath = new File(getCacheDir(), IMAGE_CACHE_DIR);
+ File newFile = new File(imagePath, IMAGE_FILE);
+ return FileProvider.getUriForFile(this, FILE_PROVIDER_AUTHORITY, newFile);
+ }
+}
diff --git a/content/SharingShortcuts/Application/src/main/java/com/example/android/sharingshortcuts/SelectContactActivity.java b/content/SharingShortcuts/Application/src/main/java/com/example/android/sharingshortcuts/SelectContactActivity.java
new file mode 100644
index 00000000..4e4d5437
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/java/com/example/android/sharingshortcuts/SelectContactActivity.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.sharingshortcuts;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * The dialog for selecting a contact to share the text with. This dialog is shown when the user
+ * taps on this sample's icon rather than any of the Direct Share contacts.
+ */
+public class SelectContactActivity extends Activity {
+
+ /**
+ * The action string for Intents.
+ */
+ public static final String ACTION_SELECT_CONTACT =
+ "com.example.android.sharingshortcuts.intent.action.SELECT_CONTACT";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_select_contact);
+ Intent intent = getIntent();
+ if (!ACTION_SELECT_CONTACT.equals(intent.getAction())) {
+ finish();
+ return;
+ }
+ // Set up the list of contacts
+ RecyclerView recyclerView = findViewById(R.id.recycler_view);
+ recyclerView.setAdapter(mContactAdapter);
+ recyclerView.setLayoutManager(new LinearLayoutManager(this));
+ }
+
+ private final RecyclerView.Adapter mContactAdapter =
+ new RecyclerView.Adapter<ContactViewHolder>() {
+
+ @NonNull
+ @Override
+ public ContactViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
+ int viewType) {
+ TextView textView = (TextView) LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.item_contact, parent, false);
+ return new ContactViewHolder(textView);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ContactViewHolder holder,
+ final int position) {
+ Contact contact = Contact.CONTACTS[position];
+ ContactViewBinder.bind(contact, (TextView) holder.itemView);
+ holder.itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent data = new Intent();
+ data.putExtra(Contact.ID, position);
+ setResult(RESULT_OK, data);
+ finish();
+ }
+ });
+ }
+
+ @Override
+ public int getItemCount() {
+ return Contact.CONTACTS.length;
+ }
+ };
+
+ private static class ContactViewHolder extends RecyclerView.ViewHolder {
+
+ ContactViewHolder(@NonNull TextView textView) {
+ super(textView);
+ }
+ }
+}
diff --git a/content/SharingShortcuts/Application/src/main/java/com/example/android/sharingshortcuts/SendMessageActivity.java b/content/SharingShortcuts/Application/src/main/java/com/example/android/sharingshortcuts/SendMessageActivity.java
new file mode 100644
index 00000000..7519a7b8
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/java/com/example/android/sharingshortcuts/SendMessageActivity.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.sharingshortcuts;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+/**
+ * Provides the UI for sharing a text with a {@link Contact}.
+ */
+public class SendMessageActivity extends Activity {
+
+ /**
+ * The request code for {@link SelectContactActivity}. This is used when the user doesn't
+ * select any of Direct Share icons.
+ */
+ private static final int REQUEST_SELECT_CONTACT = 1;
+
+ /**
+ * The text to share.
+ */
+ private String mBody;
+
+ /**
+ * The ID of the contact to share the text with.
+ */
+ private int mContactId;
+
+ // View references.
+ private TextView mTextContactName;
+ private TextView mTextMessageBody;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_send_message);
+ setTitle(R.string.sending_message);
+ // View references.
+ mTextContactName = findViewById(R.id.contact_name);
+ mTextMessageBody = findViewById(R.id.message_body);
+ // Handle the share Intent.
+ boolean handled = handleIntent(getIntent());
+ if (!handled) {
+ finish();
+ return;
+ }
+ // Bind event handlers.
+ findViewById(R.id.send).setOnClickListener(mOnClickListener);
+ // Set up the UI.
+ prepareUi();
+ // The contact ID will not be passed on when the user clicks on the app icon rather than any
+ // of the Direct Share icons. In this case, we show another dialog for selecting a contact.
+ if (mContactId == Contact.INVALID_ID) {
+ selectContact();
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case REQUEST_SELECT_CONTACT:
+ if (resultCode == RESULT_OK) {
+ mContactId = data.getIntExtra(Contact.ID, Contact.INVALID_ID);
+ }
+ // Give up sharing the send_message if the user didn't choose a contact.
+ if (mContactId == Contact.INVALID_ID) {
+ finish();
+ return;
+ }
+ prepareUi();
+ break;
+ default:
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+ /**
+ * Handles the passed {@link Intent}. This method can only handle intents for sharing a plain
+ * text. {@link #mBody} and {@link #mContactId} are modified accordingly.
+ *
+ * @param intent The {@link Intent}.
+ * @return true if the {@code intent} is handled properly.
+ */
+ private boolean handleIntent(Intent intent) {
+ if (Intent.ACTION_SEND.equals(intent.getAction())
+ && "text/plain".equals(intent.getType())) {
+ mBody = intent.getStringExtra(Intent.EXTRA_TEXT);
+ // The intent comes from Direct share
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
+ && intent.hasExtra(Intent.EXTRA_SHORTCUT_ID)) {
+ String shortcutId = intent.getStringExtra(Intent.EXTRA_SHORTCUT_ID);
+ mContactId = Integer.valueOf(shortcutId);
+ } else {
+ // The text was shared and the user chose our app
+ mContactId = Contact.INVALID_ID;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Sets up the UI.
+ */
+ private void prepareUi() {
+ if (mContactId != Contact.INVALID_ID) {
+ Contact contact = Contact.byId(mContactId);
+ ContactViewBinder.bind(contact, mTextContactName);
+ }
+ mTextMessageBody.setText(mBody);
+ }
+
+ /**
+ * Delegates selection of a {@Contact} to {@link SelectContactActivity}.
+ */
+ private void selectContact() {
+ Intent intent = new Intent(this, SelectContactActivity.class);
+ intent.setAction(SelectContactActivity.ACTION_SELECT_CONTACT);
+ startActivityForResult(intent, REQUEST_SELECT_CONTACT);
+ }
+
+ private View.OnClickListener mOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ switch (view.getId()) {
+ case R.id.send:
+ send();
+ break;
+ }
+ }
+ };
+
+ /**
+ * Pretends to send the text to the contact. This only shows a dummy message.
+ */
+ private void send() {
+ Toast.makeText(this,
+ getString(R.string.message_sent, mBody, Contact.byId(mContactId).getName()),
+ Toast.LENGTH_SHORT).show();
+ finish();
+ }
+
+}
diff --git a/content/SharingShortcuts/Application/src/main/java/com/example/android/sharingshortcuts/SharingShortcutsManager.java b/content/SharingShortcuts/Application/src/main/java/com/example/android/sharingshortcuts/SharingShortcutsManager.java
new file mode 100644
index 00000000..14cc9290
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/java/com/example/android/sharingshortcuts/SharingShortcutsManager.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.sharingshortcuts;
+
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.annotation.NonNull;
+import androidx.core.app.Person;
+import androidx.core.content.pm.ShortcutInfoCompat;
+import androidx.core.content.pm.ShortcutManagerCompat;
+import androidx.core.graphics.drawable.IconCompat;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Provides the Sharing Shortcuts items to the system.
+ * <p>
+ * Use the ShortcutManagerCompat to make it work on older Android versions
+ * without any extra work needed.
+ * <p>
+ * Interactions with the ShortcutManager API can happen on any thread.
+ */
+public class SharingShortcutsManager {
+
+ /**
+ * Define maximum number of shortcuts.
+ * Don't add more than {@link ShortcutManagerCompat#getMaxShortcutCountPerActivity(Context)}.
+ */
+ private static final int MAX_SHORTCUTS = 4;
+
+ /**
+ * Category name defined in res/xml/shortcuts.xml that accepts data of type text/plain
+ * and will trigger {@link SendMessageActivity}
+ */
+ private static final String CATEGORY_TEXT_SHARE_TARGET =
+ "com.example.android.sharingshortcuts.category.TEXT_SHARE_TARGET";
+
+ /**
+ * Publish the list of dynamics shortcuts that will be used in Direct Share.
+ * <p>
+ * For each shortcut, we specify the categories that it will be associated to,
+ * the intent that will trigger when opened as a static launcher shortcut,
+ * and the Shortcut ID between other things.
+ * <p>
+ * The Shortcut ID that we specify in the {@link ShortcutInfoCompat.Builder} constructor will
+ * be received in the intent as {@link Intent#EXTRA_SHORTCUT_ID}.
+ * <p>
+ * In this code sample, this method is completely static. We are always setting the same sharing
+ * shortcuts. In a real-world example, we would replace existing shortcuts depending on
+ * how the user interacts with the app as often as we want to.
+ */
+ public void pushDirectShareTargets(@NonNull Context context) {
+ ArrayList<ShortcutInfoCompat> shortcuts = new ArrayList<>();
+
+ // Category that our sharing shortcuts will be assigned to
+ Set<String> contactCategories = new HashSet<>();
+ contactCategories.add(CATEGORY_TEXT_SHARE_TARGET);
+
+ // Adding maximum number of shortcuts to the list
+ for (int id = 0; id < MAX_SHORTCUTS; ++id) {
+ Contact contact = Contact.byId(id);
+
+ // Item that will be sent if the shortcut is opened as a static launcher shortcut
+ Intent staticLauncherShortcutIntent = new Intent(Intent.ACTION_DEFAULT);
+
+ // Creates a new Sharing Shortcut and adds it to the list
+ // The id passed in the constructor will become EXTRA_SHORTCUT_ID in the received Intent
+ shortcuts.add(new ShortcutInfoCompat.Builder(context, Integer.toString(id))
+ .setShortLabel(contact.getName())
+ // Icon that will be displayed in the share target
+ .setIcon(IconCompat.createWithResource(context, contact.getIcon()))
+ .setIntent(staticLauncherShortcutIntent)
+ // Make this sharing shortcut cached by the system
+ // Even if it is unpublished, it can still appear on the sharesheet
+ .setLongLived()
+ .setCategories(contactCategories)
+ // Person objects are used to give better suggestions
+ .setPerson(new Person.Builder()
+ .setName(contact.getName())
+ .build())
+ .build());
+ }
+
+ ShortcutManagerCompat.addDynamicShortcuts(context, shortcuts);
+ }
+
+ /**
+ * Remove all dynamic shortcuts
+ */
+ public void removeAllDirectShareTargets(@NonNull Context context) {
+ ShortcutManagerCompat.removeAllDynamicShortcuts(context);
+ }
+}
diff --git a/content/SharingShortcuts/Application/src/main/res/layout/activity_main.xml b/content/SharingShortcuts/Application/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..524303d8
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/res/layout/activity_main.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/margin_medium"
+ android:text="@string/explanation" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@android:color/darker_gray" />
+
+ <EditText
+ android:id="@+id/body"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/margin_medium"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:layout_marginEnd="@dimen/margin_medium"
+ android:hint="@string/text_to_share"
+ android:text="@string/hello" />
+
+ <Button
+ android:id="@+id/share"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end"
+ android:layout_marginStart="@dimen/margin_medium"
+ android:layout_marginEnd="@dimen/margin_medium"
+ android:text="@string/share" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/content/SharingShortcuts/Application/src/main/res/layout/activity_select_contact.xml b/content/SharingShortcuts/Application/src/main/res/layout/activity_select_contact.xml
new file mode 100644
index 00000000..b9cd8032
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/res/layout/activity_select_contact.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:clipToPadding="false"
+ android:divider="@null"
+ android:paddingTop="@dimen/margin_tiny"
+ android:paddingBottom="@dimen/margin_tiny" />
diff --git a/content/SharingShortcuts/Application/src/main/res/layout/activity_send_message.xml b/content/SharingShortcuts/Application/src/main/res/layout/activity_send_message.xml
new file mode 100644
index 00000000..b8ad5957
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/res/layout/activity_send_message.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/dialog_padding"
+ android:layout_marginTop="@dimen/dialog_padding"
+ android:layout_marginEnd="@dimen/dialog_padding"
+ android:orientation="horizontal">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:text="@string/to"
+ android:textAppearance="@android:style/TextAppearance.Material.Caption" />
+
+ <TextView
+ android:id="@+id/contact_name"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/margin_medium"
+ android:layout_weight="1"
+ android:drawablePadding="@dimen/margin_medium"
+ android:gravity="center_vertical"
+ android:textAppearance="@android:style/TextAppearance.Material.Body1"
+ tools:drawableStart="@mipmap/logo_avatar"
+ tools:text="Taro" />
+
+ </LinearLayout>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/dialog_padding"
+ android:layout_marginTop="@dimen/margin_medium"
+ android:layout_marginEnd="@dimen/dialog_padding"
+ android:gravity="center_vertical"
+ android:text="@string/body"
+ android:textAppearance="@android:style/TextAppearance.Material.Caption" />
+
+ <TextView
+ android:id="@+id/message_body"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/dialog_padding"
+ android:layout_marginEnd="@dimen/dialog_padding"
+ android:gravity="top"
+ android:hint="@string/hint_body"
+ android:paddingTop="@dimen/margin_small"
+ android:paddingBottom="@dimen/margin_small"
+ android:textAppearance="@android:style/TextAppearance.Material.Body1"
+ tools:text="Hello, world!" />
+
+ <Button
+ android:id="@+id/send"
+ style="@android:style/Widget.Material.Button.Borderless.Colored"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end"
+ android:layout_marginEnd="@dimen/dialog_button_padding"
+ android:layout_marginBottom="@dimen/dialog_button_padding"
+ android:text="@string/send" />
+
+</LinearLayout>
diff --git a/content/SharingShortcuts/Application/src/main/res/layout/item_contact.xml b/content/SharingShortcuts/Application/src/main/res/layout/item_contact.xml
new file mode 100644
index 00000000..a7c17354
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/res/layout/item_contact.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/contact_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawablePadding="@dimen/margin_medium"
+ android:gravity="center_vertical"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingTop="@dimen/margin_tiny"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingBottom="@dimen/margin_tiny"
+ tools:drawableStart="@mipmap/logo_avatar"
+ tools:text="Taro" />
diff --git a/content/SharingShortcuts/Application/src/main/res/mipmap-hdpi/ic_launcher.png b/content/SharingShortcuts/Application/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..988f2ec8
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/content/SharingShortcuts/Application/src/main/res/mipmap-hdpi/logo_avatar.png b/content/SharingShortcuts/Application/src/main/res/mipmap-hdpi/logo_avatar.png
new file mode 100644
index 00000000..8892c08d
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/res/mipmap-hdpi/logo_avatar.png
Binary files differ
diff --git a/content/SharingShortcuts/Application/src/main/res/mipmap-mdpi/ic_launcher.png b/content/SharingShortcuts/Application/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..0baa1cc7
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/content/SharingShortcuts/Application/src/main/res/mipmap-mdpi/logo_avatar.png b/content/SharingShortcuts/Application/src/main/res/mipmap-mdpi/logo_avatar.png
new file mode 100644
index 00000000..c2de7747
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/res/mipmap-mdpi/logo_avatar.png
Binary files differ
diff --git a/content/SharingShortcuts/Application/src/main/res/mipmap-xhdpi/ic_launcher.png b/content/SharingShortcuts/Application/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..da0aa2f2
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/content/SharingShortcuts/Application/src/main/res/mipmap-xhdpi/logo_avatar.png b/content/SharingShortcuts/Application/src/main/res/mipmap-xhdpi/logo_avatar.png
new file mode 100644
index 00000000..10c2dc9a
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/res/mipmap-xhdpi/logo_avatar.png
Binary files differ
diff --git a/content/SharingShortcuts/Application/src/main/res/mipmap-xxhdpi/ic_launcher.png b/content/SharingShortcuts/Application/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..e1cc1ff1
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/content/SharingShortcuts/Application/src/main/res/mipmap-xxhdpi/logo_avatar.png b/content/SharingShortcuts/Application/src/main/res/mipmap-xxhdpi/logo_avatar.png
new file mode 100644
index 00000000..df02f04f
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/res/mipmap-xxhdpi/logo_avatar.png
Binary files differ
diff --git a/content/SharingShortcuts/Application/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/content/SharingShortcuts/Application/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..8b0f60c4
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/content/SharingShortcuts/Application/src/main/res/mipmap-xxxhdpi/logo_avatar.png b/content/SharingShortcuts/Application/src/main/res/mipmap-xxxhdpi/logo_avatar.png
new file mode 100644
index 00000000..dc8d3769
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/res/mipmap-xxxhdpi/logo_avatar.png
Binary files differ
diff --git a/content/SharingShortcuts/Application/src/main/res/values/colors.xml b/content/SharingShortcuts/Application/src/main/res/values/colors.xml
new file mode 100644
index 00000000..4ffc20b8
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/res/values/colors.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<resources>
+ <color name="primary">#3F51B5</color>
+ <color name="primary_dark">#303F9F</color>
+ <color name="primary_light">#C5CAE9</color>
+ <color name="accent">#00BCD4</color>
+ <color name="primary_text">#212121</color>
+ <color name="secondary_text">#727272</color>
+ <color name="icons">#FFFFFF</color>
+ <color name="divider">#B6B6B6</color>
+</resources>
diff --git a/content/SharingShortcuts/Application/src/main/res/values/dimens.xml b/content/SharingShortcuts/Application/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..dce82532
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/res/values/dimens.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<resources>
+ <dimen name="dialog_padding">24dp</dimen>
+ <dimen name="dialog_button_padding">8dp</dimen>
+</resources> \ No newline at end of file
diff --git a/content/SharingShortcuts/Application/src/main/res/values/strings.xml b/content/SharingShortcuts/Application/src/main/res/values/strings.xml
new file mode 100644
index 00000000..2fd9ffa1
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/res/values/strings.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<resources>
+
+ <!-- MainActivity -->
+
+ <string name="send_intent_title">Send message</string>
+ <string name="explanation">
+ This app demonstrates how to implement Direct Share. Use some other app and share a text.
+ For your convenience, you can also use the input below to share the text.
+ </string>
+ <string name="text_to_share">Text to share</string>
+ <string name="hello">Hello!</string>
+ <string name="share">Share</string>
+
+ <!-- SendMessageActivity -->
+
+ <string name="sending_message">Sending a message</string>
+ <string name="to">To:</string>
+ <string name="send">Send</string>
+ <string name="body">Body:</string>
+ <string name="hint_body">Edit your message</string>
+ <string name="message_sent">Sent a message \"%1$s\" to %2$s</string>
+
+</resources> \ No newline at end of file
diff --git a/content/SharingShortcuts/Application/src/main/res/values/styles.xml b/content/SharingShortcuts/Application/src/main/res/values/styles.xml
new file mode 100644
index 00000000..ca1a0e7b
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/res/values/styles.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<resources>
+
+ <style name="SharingShortcutsTheme" parent="android:Theme.Material.Light.DarkActionBar">
+ <item name="android:colorPrimary">@color/primary</item>
+ <item name="android:colorPrimaryDark">@color/primary_dark</item>
+ <item name="android:colorAccent">@color/accent</item>
+ </style>
+
+ <style name="SharingShortcutsDialogTheme" parent="android:Theme.Material.Light.Dialog">
+ <item name="android:colorPrimary">@color/primary</item>
+ <item name="android:colorPrimaryDark">@color/primary_dark</item>
+ <item name="android:colorAccent">@color/accent</item>
+ </style>
+
+
+</resources> \ No newline at end of file
diff --git a/content/SharingShortcuts/Application/src/main/res/xml/file_paths.xml b/content/SharingShortcuts/Application/src/main/res/xml/file_paths.xml
new file mode 100644
index 00000000..5d344b31
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/res/xml/file_paths.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<paths>
+ <cache-path
+ name="shared_images"
+ path="images/" />
+</paths>
diff --git a/content/SharingShortcuts/Application/src/main/res/xml/shortcuts.xml b/content/SharingShortcuts/Application/src/main/res/xml/shortcuts.xml
new file mode 100644
index 00000000..a715cd35
--- /dev/null
+++ b/content/SharingShortcuts/Application/src/main/res/xml/shortcuts.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
+ <share-target android:targetClass="com.example.android.sharingshortcuts.SendMessageActivity">
+ <data android:mimeType="text/plain" />
+ <category android:name="com.example.android.sharingshortcuts.category.TEXT_SHARE_TARGET" />
+ </share-target>
+</shortcuts> \ No newline at end of file
diff --git a/content/SharingShortcuts/build.gradle b/content/SharingShortcuts/build.gradle
new file mode 100644
index 00000000..9fa56856
--- /dev/null
+++ b/content/SharingShortcuts/build.gradle
@@ -0,0 +1,21 @@
+
+
+// BEGIN_EXCLUDE
+import com.example.android.samples.build.SampleGenPlugin
+apply plugin: SampleGenPlugin
+
+samplegen {
+ pathToBuild "../../../../build"
+ pathToSamplesCommon "../../common"
+}
+apply from: "../../../../build/build.gradle"
+buildscript {
+ ext.kotlin_version = '1.3.21'
+ repositories {
+ mavenCentral()
+ }
+ dependencies {
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+// END_EXCLUDE
diff --git a/content/SharingShortcuts/buildSrc/build.gradle b/content/SharingShortcuts/buildSrc/build.gradle
new file mode 100644
index 00000000..75b00eeb
--- /dev/null
+++ b/content/SharingShortcuts/buildSrc/build.gradle
@@ -0,0 +1,17 @@
+
+repositories {
+ google()
+ jcenter()
+}
+dependencies {
+ implementation 'org.freemarker:freemarker:2.3.20'
+}
+
+sourceSets {
+ main {
+ groovy {
+ srcDir new File(rootDir, "../../../../../build/buildSrc/src/main/groovy")
+ }
+ }
+}
+
diff --git a/content/SharingShortcuts/gradle.properties b/content/SharingShortcuts/gradle.properties
new file mode 100644
index 00000000..94f84728
--- /dev/null
+++ b/content/SharingShortcuts/gradle.properties
@@ -0,0 +1,22 @@
+
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Settings specified in this file will override any Gradle settings
+# configured through the IDE.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx10248m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/content/SharingShortcuts/gradle/wrapper/gradle-wrapper.jar b/content/SharingShortcuts/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..8c0fb64a
--- /dev/null
+++ b/content/SharingShortcuts/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/content/SharingShortcuts/gradle/wrapper/gradle-wrapper.properties b/content/SharingShortcuts/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..77cff654
--- /dev/null
+++ b/content/SharingShortcuts/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Mon Feb 18 15:19:26 CET 2019
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
diff --git a/content/SharingShortcuts/gradlew b/content/SharingShortcuts/gradlew
new file mode 100755
index 00000000..91a7e269
--- /dev/null
+++ b/content/SharingShortcuts/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/content/SharingShortcuts/gradlew.bat b/content/SharingShortcuts/gradlew.bat
new file mode 100644
index 00000000..aec99730
--- /dev/null
+++ b/content/SharingShortcuts/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/content/SharingShortcuts/screenshots/1-main.png b/content/SharingShortcuts/screenshots/1-main.png
new file mode 100644
index 00000000..693b2be3
--- /dev/null
+++ b/content/SharingShortcuts/screenshots/1-main.png
Binary files differ
diff --git a/content/SharingShortcuts/screenshots/2-intent.png b/content/SharingShortcuts/screenshots/2-intent.png
new file mode 100644
index 00000000..f01e3951
--- /dev/null
+++ b/content/SharingShortcuts/screenshots/2-intent.png
Binary files differ
diff --git a/content/SharingShortcuts/screenshots/3-message.png b/content/SharingShortcuts/screenshots/3-message.png
new file mode 100644
index 00000000..cde24969
--- /dev/null
+++ b/content/SharingShortcuts/screenshots/3-message.png
Binary files differ
diff --git a/content/SharingShortcuts/screenshots/4-static_shortcuts.png b/content/SharingShortcuts/screenshots/4-static_shortcuts.png
new file mode 100644
index 00000000..0404027b
--- /dev/null
+++ b/content/SharingShortcuts/screenshots/4-static_shortcuts.png
Binary files differ
diff --git a/content/SharingShortcuts/screenshots/icon-web.png b/content/SharingShortcuts/screenshots/icon-web.png
new file mode 100644
index 00000000..ee7c5579
--- /dev/null
+++ b/content/SharingShortcuts/screenshots/icon-web.png
Binary files differ
diff --git a/content/SharingShortcuts/settings.gradle b/content/SharingShortcuts/settings.gradle
new file mode 100644
index 00000000..0a5c310b
--- /dev/null
+++ b/content/SharingShortcuts/settings.gradle
@@ -0,0 +1,2 @@
+
+include 'Application'
diff --git a/content/SharingShortcuts/template-params.xml b/content/SharingShortcuts/template-params.xml
new file mode 100644
index 00000000..5bf56268
--- /dev/null
+++ b/content/SharingShortcuts/template-params.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+ Copyright 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<sample>
+ <name>SharingShortcuts</name>
+ <group>Content</group>
+ <package>com.example.android.sharingshortcuts</package>
+
+ <minSdk>21</minSdk>
+
+ <!-- Include additional dependencies here.-->
+ <dependency>androidx.appcompat:appcompat:1.1.0-alpha03</dependency>
+ <dependency>androidx.sharetarget:sharetarget:1.0.0-alpha01</dependency>
+ <dependency>androidx.recyclerview:recyclerview:1.0.0</dependency>
+
+ <strings>
+ <intro>
+ <![CDATA[
+This sample demonstrates how to provide the Sharing Shortcuts - Direct Share feature. The app shows
+some options directly in the list of share intent candidates and launcher shortcuts.
+ ]]>
+ </intro>
+ </strings>
+
+ <template src="base" />
+ <androidX>true</androidX>
+
+ <metadata>
+ <!-- Values: {DRAFT | PUBLISHED | INTERNAL | DEPRECATED | SUPERCEDED} -->
+ <status>DRAFT</status>
+ <categories>Content</categories>
+ <technologies>Android</technologies>
+ <languages>Java</languages>
+ <solutions>Mobile</solutions>
+ <level>INTERMEDIATE</level>
+ <icon>screenshots/icon-web.png</icon>
+ <screenshots>
+ <img>screenshots/1-main.png</img>
+ <img>screenshots/2-intent.png</img>
+ <img>screenshots/3-message.png</img>
+ <img>screenshots/4-static_shortcuts.png</img>
+ </screenshots>
+
+ <api_refs>
+ <androidx>androidx.core.content.pm.ShortcutManagerCompat</androidx>
+ <androidx>androidx.core.content.pm.ShortcutInfoCompat</androidx>
+ </api_refs>
+ <description>
+ <![CDATA[
+Sample demonstrating how to show some options directly in the list of share intent candidates.
+ ]]>
+ </description>
+
+ <intro>
+ <![CDATA[
+Direct Share is a feature that allows apps to show their internal options directly in the
+system Intent chooser dialog. This sample is a dummy messaging app, and just like any other
+messaging apps, it receives intents for sharing a plain text. When a user shares some text from some
+other app, this sample app will be listed as an option. Using the Direct Share feature, this app
+also shows some of contacts directly in the chooser dialog.
+
+Direct Share was [first introduced][1] in Android M where you had to implement a service to provide
+direct share targets on demand. The way to do that changed in Android Q, you need to provide your
+direct share targets in advance. First, you have to declare share-target elements in the same
+application's resource file than [static shortcuts][2]. Then, you need to publish dynamic
+shortcuts with the same category you declared in the share-target with the [ShortcutManager API][3].
+You will need to manually update the list of shortcuts every time you consider it appropriate.
+The API offers methods to update, remove or add shortcuts. You can use the
+[ShortcutInfoCompat.Builder][4] to customize your shortcut. If you don't want to block the UI thread
+doing these operations, interactions with the ShortcutManager can also happen on a background thread.
+
+There are three ways the app is sending/receiving intents:
+- Dynamic Share: The user selected the app in the sharesheet to receive the text. After this,
+the user will have to select the contact to share the text with. In our case, the app receives an
+intent of type Intent.ACTION_SEND.
+- Direct Share: The user selected a person of your app in the sharesheet to share the text with.
+The received intent of type Intent.ACTION_SEND will contain a String EXTRA_SHORTCUT_ID that will
+have the id of the shortcut that was selected. After this, the app is ready to send the text.
+- Launcher shortcut: When the user taps on a launcher shortcut, the intent that was
+added to the shortcut will get fired. In our case, it triggers an intent of type Intent.ACTION_DEFAULT.
+
+To make Direct Share backwards compatible with older Android versions, you need to add the AndroidX
+sharetarget library and in your AndroidManifest.xml, add a meta-data tag in your Activity that
+receives the Intent. Specify android:name as android.service.chooser.chooser_target_service and
+android:value as androidx.sharetarget.ChooserTargetServiceCompat.
+
+The way to share text has also changed. Before, you could specify a title in the
+[Intent.createChooser()][5] method itself. That is deprecated and unused in Android Q. You can achieve
+the same behavior by adding an Intent.EXTRA_TITLE extra to the intent. Similarly, if you want a
+preview thumbnail to appear, you can create a content URI and set a ClipData object in the intent.
+You can see how to do that in our example, open the MainActivity.java file for more details.
+
+[1]: https://developer.android.com/about/versions/marshmallow/android-6.0#direct-share
+[2]: https://developer.android.com/guide/topics/ui/shortcuts/creating-shortcuts
+[3]: https://developer.android.com/reference/androidx/core/content/pm/ShortcutManagerCompat.html
+[4]: https://developer.android.com/reference/androidx/core/content/pm/ShortcutInfoCompat.Builder.html
+[5]: https://developer.android.com/reference/android/content/Intent.html#createChooser(android.content.Intent,%20java.lang.CharSequence)
+ ]]>
+ </intro>
+ </metadata>
+</sample>
diff --git a/media/MediaBrowserService/template-params.xml b/media/MediaBrowserService/template-params.xml
index 9abe2120..b7f6f722 100644
--- a/media/MediaBrowserService/template-params.xml
+++ b/media/MediaBrowserService/template-params.xml
@@ -29,11 +29,7 @@
<strings>
<intro>
- <![CDATA[
-This sample shows how to implement an audio media app that provides
-media library metadata and playback controls through a standard
-service. It exposes a simple music library through the new
-MediaBrowserService and provides MediaSession callbacks.]]>
+ <![CDATA[ This sample is DEPRECATED. ]]>
</intro>
</strings>
@@ -42,7 +38,7 @@ MediaBrowserService and provides MediaSession callbacks.]]>
<template src="base-application" />
<metadata>
- <status>PUBLISHED</status>
+ <status>DEPRECATED</status>
<categories>Media</categories>
<technologies>Android</technologies>
<languages>Java</languages>
@@ -62,6 +58,10 @@ MediaBrowserService and provides MediaSession callbacks.]]>
</api_refs>
<description>
<![CDATA[
+This sample is deprecated. See the
+[Universal Android Music Player](https://github.com/googlesamples/android-UniversalMusicPlayer)
+sample for a more complete solution for using a MediaSession and MediaBrowserService.
+
This sample shows how to implement a media app that allows
background playback of audio, and provide a media library
that is exposed to other apps.
diff --git a/notification/Bubbles/.gitignore b/notification/Bubbles/.gitignore
new file mode 100644
index 00000000..603b1407
--- /dev/null
+++ b/notification/Bubbles/.gitignore
@@ -0,0 +1,14 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
diff --git a/notification/Bubbles/app/.gitignore b/notification/Bubbles/app/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/notification/Bubbles/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/notification/Bubbles/app/build.gradle b/notification/Bubbles/app/build.gradle
new file mode 100644
index 00000000..1600ee47
--- /dev/null
+++ b/notification/Bubbles/app/build.gradle
@@ -0,0 +1,50 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+android {
+ compileSdkVersion 'android-Q'
+ defaultConfig {
+ applicationId "com.example.android.bubbles"
+ minSdkVersion 'Q'
+ targetSdkVersion 'Q'
+ versionCode 1
+ versionName '1.0'
+ testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+
+ implementation 'androidx.appcompat:appcompat:1.0.2'
+ implementation 'androidx.fragment:fragment-ktx:1.0.0'
+ implementation 'androidx.core:core-ktx:1.0.1'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation 'androidx.recyclerview:recyclerview:1.0.0'
+
+ def lifecycle_version = '2.0.0'
+ implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
+ testImplementation "androidx.arch.core:core-testing:$lifecycle_version"
+
+ implementation 'com.google.android.material:material:1.0.0'
+
+ implementation 'com.github.bumptech.glide:glide:4.9.0'
+
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.0'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
+
+ testImplementation 'org.robolectric:robolectric:4.2'
+ testImplementation "androidx.arch.core:core-testing:$lifecycle_version"
+ testImplementation 'androidx.test.ext:junit:1.1.0'
+ testImplementation 'androidx.test.espresso:espresso-core:3.1.1'
+ testImplementation 'androidx.test.ext:truth:1.1.0'
+ testImplementation 'com.google.truth:truth:0.42'
+}
diff --git a/notification/Bubbles/app/proguard-rules.pro b/notification/Bubbles/app/proguard-rules.pro
new file mode 100644
index 00000000..f1b42451
--- /dev/null
+++ b/notification/Bubbles/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/notification/Bubbles/app/src/androidTest/java/com/example/android/bubbles/BubbleActivityTest.kt b/notification/Bubbles/app/src/androidTest/java/com/example/android/bubbles/BubbleActivityTest.kt
new file mode 100644
index 00000000..81a8fde2
--- /dev/null
+++ b/notification/Bubbles/app/src/androidTest/java/com/example/android/bubbles/BubbleActivityTest.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.bubbles
+
+import android.app.Application
+import android.content.Intent
+import android.net.Uri
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withHint
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class BubbleActivityTest {
+
+ @Test
+ fun showsChatFragment() {
+ ActivityScenario.launch<BubbleActivity>(
+ Intent(ApplicationProvider.getApplicationContext<Application>(), BubbleActivity::class.java)
+ .setAction(Intent.ACTION_VIEW)
+ .setData(Uri.parse("https://android.example.com/chat/1"))
+ ).use {
+ onView(withHint("Type a message…")).check(matches(isDisplayed()))
+ }
+ }
+}
diff --git a/notification/Bubbles/app/src/androidTest/java/com/example/android/bubbles/MainActivityTest.kt b/notification/Bubbles/app/src/androidTest/java/com/example/android/bubbles/MainActivityTest.kt
new file mode 100644
index 00000000..c817477a
--- /dev/null
+++ b/notification/Bubbles/app/src/androidTest/java/com/example/android/bubbles/MainActivityTest.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.bubbles
+
+import androidx.test.core.app.ActivityScenario
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withHint
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class MainActivityTest {
+
+ @Test
+ fun navigateToChatFragment() {
+ ActivityScenario.launch(MainActivity::class.java).use {
+ onView(withText("Cat"))
+ .check(matches(isDisplayed()))
+ .perform(click())
+ onView(withHint("Type a message…")).check(matches(isDisplayed()))
+ }
+ }
+}
diff --git a/notification/Bubbles/app/src/main/AndroidManifest.xml b/notification/Bubbles/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..45fac5e7
--- /dev/null
+++ b/notification/Bubbles/app/src/main/AndroidManifest.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.example.android.bubbles">
+
+ <application
+ android:allowBackup="false"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.Bubbles"
+ tools:ignore="GoogleAppIndexingWarning">
+
+ <!--
+ Our main entry point.
+ -->
+ <activity
+ android:name=".MainActivity"
+ android:launchMode="singleTop">
+ <!--
+ This activity is the one that's shown on the launcher.
+ -->
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ <!--
+ This is used as the content URI of notifications. It navigates directly to the specified chat screen.
+ -->
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data
+ android:host="android.example.com"
+ android:pathPattern="/chat/*"
+ android:scheme="https" />
+ </intent-filter>
+ </activity>
+
+ <!--
+ The dummy voice-call screen.
+ This Activity can be launched from inside an expanded Bubble. Since this Activity is launched as a new task,
+ it is opened as a full Activity, rather than stacked inside the expanded Bubble.
+ -->
+ <activity
+ android:name=".VoiceCallActivity"
+ android:launchMode="singleInstance"
+ android:theme="@style/Theme.Bubbles.Voice" />
+
+ <!--
+ This Activity is the expanded Bubble. For that, this Activity has to have several attributes.
+ - allowEmbedded="true": The expanded Bubble is embedded in the System UI.
+ - resizeableActivity="true": The expanded Bubble is resized by the System UI.
+ - documentLaunchMode="always": We show multiple bubbles in this sample. There will be multiple instances of
+ this Activity.
+ -->
+ <activity
+ android:name=".BubbleActivity"
+ android:allowEmbedded="true"
+ android:documentLaunchMode="always"
+ android:resizeableActivity="true">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data
+ android:host="android.example.com"
+ android:pathPattern="/chat/*"
+ android:scheme="https" />
+ </intent-filter>
+ </activity>
+
+ </application>
+
+</manifest>
diff --git a/notification/Bubbles/app/src/main/java/com/example/android/bubbles/BubbleActivity.kt b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/BubbleActivity.kt
new file mode 100644
index 00000000..bae103a8
--- /dev/null
+++ b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/BubbleActivity.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.bubbles
+
+import android.os.Bundle
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.transaction
+import com.example.android.bubbles.ui.chat.ChatFragment
+import com.example.android.bubbles.ui.photo.PhotoFragment
+
+/**
+ * Entry point of the app when it is launched as an expanded Bubble.
+ */
+class BubbleActivity : AppCompatActivity(), NavigationController {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.bubble_activity)
+ val id = intent.data.lastPathSegment.toLongOrNull() ?: return
+ if (savedInstanceState == null) {
+ supportFragmentManager.transaction(now = true) {
+ replace(R.id.container, ChatFragment.newInstance(id, false))
+ }
+ }
+ }
+
+ override fun openChat(id: Long) {
+ throw UnsupportedOperationException("BubbleActivity always shows a single chat thread.")
+ }
+
+ override fun openPhoto(photo: Int) {
+ // In an expanded Bubble, you can navigate between Fragments just like you would normally do in a normal
+ // Activity. Just make sure you don't block onBackPressed().
+ supportFragmentManager.transaction {
+ addToBackStack(null)
+ replace(R.id.container, PhotoFragment.newInstance(photo))
+ }
+ }
+
+ override fun updateAppBar(showContact: Boolean, hidden: Boolean, body: (name: TextView, icon: ImageView) -> Unit) {
+ // The expanded bubble does not have an app bar. Ignore.
+ }
+}
diff --git a/notification/Bubbles/app/src/main/java/com/example/android/bubbles/MainActivity.kt b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/MainActivity.kt
new file mode 100644
index 00000000..c01f4f7b
--- /dev/null
+++ b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/MainActivity.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.bubbles
+
+import android.content.Intent
+import android.os.Bundle
+import android.transition.Transition
+import android.transition.TransitionInflater
+import android.transition.TransitionManager
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.transaction
+import com.example.android.bubbles.ui.chat.ChatFragment
+import com.example.android.bubbles.ui.main.MainFragment
+import com.example.android.bubbles.ui.photo.PhotoFragment
+
+/**
+ * Entry point of the app when it is launched as a full app.
+ */
+class MainActivity : AppCompatActivity(), NavigationController {
+
+ companion object {
+ private const val FRAGMENT_CHAT = "chat"
+ }
+
+ private lateinit var appBar: ViewGroup
+ private lateinit var name: TextView
+ private lateinit var icon: ImageView
+
+ private lateinit var transition: Transition
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.main_activity)
+ setSupportActionBar(findViewById(R.id.toolbar))
+ transition = TransitionInflater.from(this).inflateTransition(R.transition.app_bar)
+ appBar = findViewById(R.id.app_bar)
+ name = findViewById(R.id.name)
+ icon = findViewById(R.id.icon)
+ if (savedInstanceState == null) {
+ supportFragmentManager.transaction(now = true) {
+ replace(R.id.container, MainFragment())
+ }
+ intent?.let(::handleIntent)
+ }
+ }
+
+ override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+ if (intent != null) {
+ handleIntent(intent)
+ }
+ }
+
+ private fun handleIntent(intent: Intent) {
+ if (intent.action == Intent.ACTION_VIEW) {
+ val id = intent.data.lastPathSegment.toLongOrNull()
+ if (id != null) {
+ openChat(id)
+ }
+ }
+ }
+
+ override fun updateAppBar(showContact: Boolean, hidden: Boolean, body: (name: TextView, icon: ImageView) -> Unit) {
+ if (hidden) {
+ appBar.visibility = View.GONE
+ } else {
+ appBar.visibility = View.VISIBLE
+ TransitionManager.beginDelayedTransition(appBar, transition)
+ if (showContact) {
+ supportActionBar?.setDisplayShowTitleEnabled(false)
+ name.visibility = View.VISIBLE
+ icon.visibility = View.VISIBLE
+ } else {
+ supportActionBar?.setDisplayShowTitleEnabled(true)
+ name.visibility = View.GONE
+ icon.visibility = View.GONE
+ }
+ }
+ body(name, icon)
+ }
+
+ override fun openChat(id: Long) {
+ supportFragmentManager.popBackStack(FRAGMENT_CHAT, FragmentManager.POP_BACK_STACK_INCLUSIVE)
+ supportFragmentManager.transaction {
+ addToBackStack(FRAGMENT_CHAT)
+ replace(R.id.container, ChatFragment.newInstance(id, true))
+ }
+ }
+
+ override fun openPhoto(photo: Int) {
+ supportFragmentManager.transaction {
+ addToBackStack(null)
+ replace(R.id.container, PhotoFragment.newInstance(photo))
+ }
+ }
+}
+
diff --git a/notification/Bubbles/app/src/main/java/com/example/android/bubbles/NavigationController.kt b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/NavigationController.kt
new file mode 100644
index 00000000..c15c4ddb
--- /dev/null
+++ b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/NavigationController.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.bubbles
+
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.DrawableRes
+import androidx.fragment.app.Fragment
+
+/**
+ * Common interface between [MainActivity] and [BubbleActivity].
+ */
+interface NavigationController {
+
+ fun openChat(id: Long)
+
+ fun openPhoto(@DrawableRes photo: Int)
+
+ /**
+ * Updates the appearance and functionality of the app bar.
+ *
+ * @param showContact Whether to show contact information instead the screen title.
+ * @param hidden Whether to hide the app bar.
+ * @param body Provide this function to update the content of the app bar.
+ */
+ fun updateAppBar(
+ showContact: Boolean = true,
+ hidden: Boolean = false,
+ body: (name: TextView, icon: ImageView) -> Unit = { _, _ -> }
+ )
+}
+
+fun Fragment.getNavigationController() = requireActivity() as NavigationController
diff --git a/notification/Bubbles/app/src/main/java/com/example/android/bubbles/VoiceCallActivity.kt b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/VoiceCallActivity.kt
new file mode 100644
index 00000000..46917695
--- /dev/null
+++ b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/VoiceCallActivity.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.bubbles
+
+import android.os.Bundle
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import com.bumptech.glide.Glide
+import com.bumptech.glide.request.RequestOptions
+
+/**
+ * A dummy voice call screen. It only shows the icon and the name.
+ */
+class VoiceCallActivity : AppCompatActivity() {
+
+ companion object {
+ const val EXTRA_NAME = "name"
+ const val EXTRA_ICON = "icon"
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.voice_call_activity)
+ val name = intent.getStringExtra(EXTRA_NAME)
+ val icon = intent.getIntExtra(EXTRA_ICON, 0)
+ if (name == null || icon == 0) {
+ finish()
+ return
+ }
+ val textName: TextView = findViewById(R.id.name)
+ textName.text = name
+ val imageIcon: ImageView = findViewById(R.id.icon)
+ Glide.with(imageIcon).load(icon).apply(RequestOptions.circleCropTransform()).into(imageIcon)
+ }
+}
diff --git a/notification/Bubbles/app/src/main/java/com/example/android/bubbles/data/Chat.kt b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/data/Chat.kt
new file mode 100644
index 00000000..0d0fc5f4
--- /dev/null
+++ b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/data/Chat.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.bubbles.data
+
+typealias ChatThreadListener = (List<Message>) -> Unit
+
+class Chat(val contact: Contact) {
+
+ private val listeners = mutableListOf<ChatThreadListener>()
+
+ private val _messages = mutableListOf(
+ Message(1L, contact.id, "Send me a message", null, System.currentTimeMillis()),
+ Message(2L, contact.id, "I will reply in 5 seconds", null, System.currentTimeMillis())
+ )
+ val messages: List<Message>
+ get() = _messages
+
+ fun addListener(listener: ChatThreadListener) {
+ listeners.add(listener)
+ }
+
+ fun removeListener(listener: ChatThreadListener) {
+ listeners.remove(listener)
+ }
+
+ fun addMessage(builder: Message.Builder) {
+ builder.id = _messages.last().id + 1
+ _messages.add(builder.build())
+ listeners.forEach { listener -> listener(_messages) }
+ }
+}
diff --git a/notification/Bubbles/app/src/main/java/com/example/android/bubbles/data/ChatRepository.kt b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/data/ChatRepository.kt
new file mode 100644
index 00000000..cf3af753
--- /dev/null
+++ b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/data/ChatRepository.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.bubbles.data
+
+import android.content.Context
+import androidx.annotation.MainThread
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import java.util.concurrent.Executor
+import java.util.concurrent.Executors
+
+interface ChatRepository {
+ fun getContacts(): LiveData<List<Contact>>
+ fun findContact(id: Long): LiveData<Contact?>
+ fun findMessages(id: Long): LiveData<List<Message>>
+ fun sendMessage(id: Long, text: String)
+ fun activateChat(id: Long)
+ fun deactivateChat(id: Long)
+ fun showAsBubble(id: Long)
+ fun canBubble(): Boolean
+}
+
+class DefaultChatRepository internal constructor(
+ private val notificationHelper: NotificationHelper,
+ private val executor: Executor
+) : ChatRepository {
+
+ companion object {
+ private var instance: DefaultChatRepository? = null
+
+ fun getInstance(context: Context): DefaultChatRepository {
+ return instance ?: synchronized(this) {
+ instance ?: DefaultChatRepository(
+ NotificationHelper(context),
+ Executors.newFixedThreadPool(4)
+ ).also {
+ instance = it
+ }
+ }
+ }
+ }
+
+ private var currentChat: Long = 0L
+
+ private val chats = Contact.CONTACTS.map { contact ->
+ contact.id to Chat(contact)
+ }.toMap()
+
+ init {
+ notificationHelper.setUpNotificationChannels()
+ }
+
+ @MainThread
+ override fun getContacts(): LiveData<List<Contact>> {
+ return MutableLiveData<List<Contact>>().apply {
+ postValue(Contact.CONTACTS)
+ }
+ }
+
+ @MainThread
+ override fun findContact(id: Long): LiveData<Contact?> {
+ return MutableLiveData<Contact>().apply {
+ postValue(Contact.CONTACTS.find { it.id == id })
+ }
+ }
+
+ @MainThread
+ override fun findMessages(id: Long): LiveData<List<Message>> {
+ val chat = chats.getValue(id)
+ return object : LiveData<List<Message>>() {
+
+ private val listener = { messages: List<Message> ->
+ postValue(messages)
+ }
+
+ override fun onActive() {
+ value = chat.messages
+ chat.addListener(listener)
+ }
+
+ override fun onInactive() {
+ chat.removeListener(listener)
+ }
+ }
+ }
+
+ @MainThread
+ override fun sendMessage(id: Long, text: String) {
+ val chat = chats.getValue(id)
+ chat.addMessage(Message.Builder().apply {
+ sender = 0L // User
+ this.text = text
+ timestamp = System.currentTimeMillis()
+ })
+ executor.execute {
+ // The animal is typing...
+ Thread.sleep(5000L)
+ // Receive a reply.
+ chat.addMessage(chat.contact.reply(text))
+ // Show notification if the chat is not on the foreground.
+ if (chat.contact.id != currentChat) {
+ notificationHelper.showNotification(chat, false)
+ }
+ }
+ }
+
+ override fun activateChat(id: Long) {
+ currentChat = id
+ notificationHelper.dismissNotification(id)
+ }
+
+ override fun deactivateChat(id: Long) {
+ if (currentChat == id) {
+ currentChat = 0
+ }
+ }
+
+ override fun showAsBubble(id: Long) {
+ val chat = chats.getValue(id)
+ executor.execute {
+ notificationHelper.showNotification(chat, true)
+ }
+ }
+
+ override fun canBubble(): Boolean {
+ return notificationHelper.canBubble()
+ }
+}
diff --git a/notification/Bubbles/app/src/main/java/com/example/android/bubbles/data/Contact.kt b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/data/Contact.kt
new file mode 100644
index 00000000..28798f32
--- /dev/null
+++ b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/data/Contact.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.bubbles.data
+
+import androidx.annotation.DrawableRes
+import com.example.android.bubbles.R
+
+abstract class Contact(
+ val id: Long,
+ val name: String,
+ @DrawableRes
+ val icon: Int
+) {
+
+ companion object {
+ val CONTACTS = listOf(
+ object : Contact(1L, "Cat", R.drawable.cat) {
+ override fun reply(text: String) = buildReply().apply { this.text = "Meow" }
+ },
+ object : Contact(2L, "Dog", R.drawable.dog) {
+ override fun reply(text: String) = buildReply().apply { this.text = "Woof woof!!" }
+ },
+ object : Contact(3L, "Parrot", R.drawable.parrot) {
+ override fun reply(text: String) = buildReply().apply { this.text = text }
+ },
+ object : Contact(4L, "Sheep", R.drawable.sheep) {
+ override fun reply(text: String) = buildReply().apply {
+ this.text = "Look at me!"
+ photo = R.drawable.sheep_full
+ }
+ }
+ )
+ }
+
+ fun buildReply() = Message.Builder().apply {
+ sender = this@Contact.id
+ timestamp = System.currentTimeMillis()
+ }
+
+ abstract fun reply(text: String): Message.Builder
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as Contact
+
+ if (id != other.id) return false
+ if (name != other.name) return false
+ if (icon != other.icon) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = id.hashCode()
+ result = 31 * result + name.hashCode()
+ result = 31 * result + icon
+ return result
+ }
+}
diff --git a/notification/Bubbles/app/src/main/java/com/example/android/bubbles/data/Message.kt b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/data/Message.kt
new file mode 100644
index 00000000..bc48a0f6
--- /dev/null
+++ b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/data/Message.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.bubbles.data
+
+import androidx.annotation.DrawableRes
+
+data class Message(
+ val id: Long,
+ val sender: Long,
+ val text: String,
+ @DrawableRes
+ val photo: Int?,
+ val timestamp: Long
+) {
+
+ val isIncoming: Boolean
+ get() = sender != 0L
+
+ class Builder {
+ var id: Long? = null
+ var sender: Long? = null
+ var text: String? = null
+ var photo: Int? = null
+ var timestamp: Long? = null
+ fun build() = Message(id!!, sender!!, text!!, photo, timestamp!!)
+ }
+}
diff --git a/notification/Bubbles/app/src/main/java/com/example/android/bubbles/data/NotificationHelper.kt b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/data/NotificationHelper.kt
new file mode 100644
index 00000000..825489c3
--- /dev/null
+++ b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/data/NotificationHelper.kt
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.bubbles.data
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Person
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.BlendMode
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Rect
+import android.graphics.drawable.Icon
+import android.net.Uri
+import androidx.annotation.DrawableRes
+import androidx.annotation.WorkerThread
+import androidx.core.graphics.applyCanvas
+import com.example.android.bubbles.BubbleActivity
+import com.example.android.bubbles.MainActivity
+import com.example.android.bubbles.R
+
+/**
+ * Handles all operations related to [Notification].
+ */
+class NotificationHelper(private val context: Context) {
+
+ companion object {
+ /**
+ * The notification channel for messages. This is used for showing Bubbles.
+ */
+ private const val CHANNEL_NEW_MESSAGES = "new_messages"
+
+ private const val REQUEST_CONTENT = 1
+ private const val REQUEST_BUBBLE = 2
+ }
+
+ private val notificationManager = context.getSystemService(NotificationManager::class.java)
+
+ fun setUpNotificationChannels() {
+ if (notificationManager.getNotificationChannel(CHANNEL_NEW_MESSAGES) == null) {
+ notificationManager.createNotificationChannel(
+ NotificationChannel(
+ CHANNEL_NEW_MESSAGES,
+ context.getString(R.string.channel_new_messages),
+ // The importance must be IMPORTANCE_HIGH to show Bubbles.
+ NotificationManager.IMPORTANCE_HIGH
+ ).apply {
+ description = context.getString(R.string.channel_new_messages_description)
+ }
+ )
+ }
+ }
+
+ @WorkerThread
+ fun showNotification(chat: Chat, fromUser: Boolean) {
+ val icon = Icon.createWithBitmap(roundIcon(context, chat.contact.icon))
+ val person = Person.Builder()
+ .setName(chat.contact.name)
+ .setIcon(icon)
+ .build()
+ val contentUri = Uri.parse("https://android.example.com/chat/${chat.contact.id}")
+ val builder = Notification.Builder(context, CHANNEL_NEW_MESSAGES)
+ // A notification can be shown as a bubble by calling setBubbleMetadata()
+ .setBubbleMetadata(
+ Notification.BubbleMetadata.Builder()
+ // The height of the expanded bubble.
+ .setDesiredHeight(context.resources.getDimensionPixelSize(R.dimen.bubble_height))
+ // The icon of the bubble.
+ // TODO: The icon is not displayed in Android Q Beta 2.
+ .setIcon(icon)
+ .apply {
+ // When the bubble is explicitly opened by the user, we can show the bubble automatically
+ // in the expanded state. This works only when the app is in the foreground.
+ // TODO: This does not yet work in Android Q Beta 2.
+ if (fromUser) {
+ setAutoExpandBubble(true)
+ setSuppressInitialNotification(true)
+ }
+ }
+ // The Intent to be used for the expanded bubble.
+ .setIntent(
+ PendingIntent.getActivity(
+ context,
+ REQUEST_BUBBLE,
+ // Launch BubbleActivity as the expanded bubble.
+ Intent(context, BubbleActivity::class.java)
+ .setAction(Intent.ACTION_VIEW)
+ .setData(Uri.parse("https://android.example.com/chat/${chat.contact.id}")),
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ )
+ .build()
+ )
+ // The user can turn off the bubble in system settings. In that case, this notification is shown as a
+ // normal notification instead of a bubble. Make sure that this notification works as a normal notification
+ // as well.
+ .setContentTitle(chat.contact.name)
+ .setSmallIcon(R.drawable.ic_message)
+ .setCategory(Notification.CATEGORY_MESSAGE)
+ .addPerson(person)
+ .setShowWhen(true)
+ // The content Intent is used when the user clicks on the "Open Content" icon button on the expanded bubble,
+ // as well as when the fall-back notification is clicked.
+ .setContentIntent(
+ PendingIntent.getActivity(
+ context,
+ REQUEST_CONTENT,
+ Intent(context, MainActivity::class.java)
+ .setAction(Intent.ACTION_VIEW)
+ .setData(contentUri),
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ )
+
+ if (fromUser) {
+ // This is a Bubble explicitly opened by the user.
+ builder.setContentText(context.getString(R.string.chat_with_contact, chat.contact.name))
+ } else {
+ // Let's add some more content to the notification in case it falls back to a normal notification.
+ val lastOutgoingId = chat.messages.last { !it.isIncoming }.id
+ val newMessages = chat.messages.filter { message ->
+ message.id > lastOutgoingId
+ }
+ val lastMessage = newMessages.last()
+ builder
+ .setStyle(
+ if (lastMessage.photo != null) {
+ Notification.BigPictureStyle()
+ .bigPicture(BitmapFactory.decodeResource(context.resources, lastMessage.photo))
+ .bigLargeIcon(icon)
+ .setSummaryText(lastMessage.text)
+ } else {
+ Notification.MessagingStyle(person)
+ .apply {
+ for (message in newMessages) {
+ addMessage(message.text, message.timestamp, person)
+ }
+ }
+ .setGroupConversation(false)
+ }
+ )
+ .setContentText(newMessages.joinToString("\n") { it.text })
+ .setWhen(newMessages.last().timestamp)
+ }
+
+ notificationManager.notify(chat.contact.id.toInt(), builder.build())
+ }
+
+ fun dismissNotification(id: Long) {
+ notificationManager.cancel(id.toInt())
+ }
+
+ fun canBubble(): Boolean {
+ val channel = notificationManager.getNotificationChannel(CHANNEL_NEW_MESSAGES)
+ return notificationManager.areBubblesAllowed() && channel.canBubble()
+ }
+}
+
+@WorkerThread
+private fun roundIcon(context: Context, @DrawableRes id: Int): Bitmap {
+ val original = BitmapFactory.decodeResource(context.resources, id)
+ val width = original.width
+ val height = original.height
+ val rect = Rect(0, 0, width, height)
+ val icon = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+ val paint = Paint().apply {
+ isAntiAlias = true
+ color = Color.BLACK
+ }
+ icon.applyCanvas {
+ drawARGB(0, 0, 0, 0)
+ drawOval(0f, 0f, width.toFloat(), height.toFloat(), paint)
+ paint.blendMode = BlendMode.SRC_IN
+ drawBitmap(original, rect, rect, paint)
+ }
+ return icon
+}
diff --git a/notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/chat/ChatFragment.kt b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/chat/ChatFragment.kt
new file mode 100644
index 00000000..4bc25484
--- /dev/null
+++ b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/chat/ChatFragment.kt
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.bubbles.ui.chat
+
+import android.content.Intent
+import android.graphics.drawable.Drawable
+import android.os.Bundle
+import android.transition.TransitionInflater
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.view.inputmethod.EditorInfo
+import android.widget.EditText
+import android.widget.ImageButton
+import android.widget.Toast
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProviders
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.DataSource
+import com.bumptech.glide.load.engine.GlideException
+import com.bumptech.glide.request.RequestListener
+import com.bumptech.glide.request.RequestOptions
+import com.bumptech.glide.request.target.Target
+import com.example.android.bubbles.R
+import com.example.android.bubbles.VoiceCallActivity
+import com.example.android.bubbles.getNavigationController
+
+/**
+ * The chat screen. This is used in the full app (MainActivity) as well as in the expanded Bubble (BubbleActivity).
+ */
+class ChatFragment : Fragment() {
+
+ companion object {
+ private const val ARG_ID = "id"
+ private const val ARG_FOREGROUND = "foreground"
+
+ fun newInstance(id: Long, foreground: Boolean) = ChatFragment().apply {
+ arguments = Bundle().apply {
+ putLong(ARG_ID, id)
+ putBoolean(ARG_FOREGROUND, foreground)
+ }
+ }
+ }
+
+ private lateinit var viewModel: ChatViewModel
+ private lateinit var input: EditText
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setHasOptionsMenu(true)
+ enterTransition = TransitionInflater.from(context).inflateTransition(R.transition.slide_bottom)
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ return inflater.inflate(R.layout.chat_fragment, container, false)
+ }
+
+ private val startPostponedTransitionOnEnd = object : RequestListener<Drawable> {
+ override fun onLoadFailed(
+ e: GlideException?,
+ model: Any?,
+ target: Target<Drawable>?,
+ isFirstResource: Boolean
+ ): Boolean {
+ startPostponedEnterTransition()
+ return false
+ }
+
+ override fun onResourceReady(
+ resource: Drawable?,
+ model: Any?,
+ target: Target<Drawable>?,
+ dataSource: DataSource?,
+ isFirstResource: Boolean
+ ): Boolean {
+ startPostponedEnterTransition()
+ return false
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ val id = arguments?.getLong(ARG_ID)
+ if (id == null) {
+ fragmentManager?.popBackStack()
+ return
+ }
+ val navigationController = getNavigationController()
+
+ viewModel = ViewModelProviders.of(this).get(ChatViewModel::class.java)
+ viewModel.setChatId(id)
+
+ val messages: RecyclerView = view.findViewById(R.id.messages)
+ val voiceCall: ImageButton = view.findViewById(R.id.voice_call)
+ input = view.findViewById(R.id.input)
+ val send: ImageButton = view.findViewById(R.id.send)
+
+ val messageAdapter = MessageAdapter(view.context) { photo ->
+ navigationController.openPhoto(photo)
+ }
+ val linearLayoutManager = LinearLayoutManager(view.context).apply {
+ stackFromEnd = true
+ }
+ messages.run {
+ layoutManager = linearLayoutManager
+ adapter = messageAdapter
+ }
+
+ viewModel.contact.observe(viewLifecycleOwner, Observer { chat ->
+ if (chat == null) {
+ Toast.makeText(view.context, "Contact not found", Toast.LENGTH_SHORT).show()
+ fragmentManager?.popBackStack()
+ } else {
+ navigationController.updateAppBar { name, icon ->
+ name.text = chat.name
+ Glide.with(icon)
+ .load(chat.icon)
+ .apply(RequestOptions.circleCropTransform())
+ .dontAnimate()
+ .addListener(startPostponedTransitionOnEnd)
+ .into(icon)
+ }
+ }
+ })
+
+ viewModel.messages.observe(viewLifecycleOwner, Observer {
+ messageAdapter.submitList(it)
+ linearLayoutManager.scrollToPosition(it.size - 1)
+ })
+
+ voiceCall.setOnClickListener {
+ voiceCall()
+ }
+ send.setOnClickListener {
+ send()
+ }
+ input.setOnEditorActionListener { _, actionId, _ ->
+ if (actionId == EditorInfo.IME_ACTION_SEND) {
+ send()
+ true
+ } else {
+ false
+ }
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ val foreground = arguments?.getBoolean(ARG_FOREGROUND) == true
+ viewModel.foreground = foreground
+ }
+
+ override fun onStop() {
+ super.onStop()
+ viewModel.foreground = false
+ }
+
+ private fun voiceCall() {
+ val contact = viewModel.contact.value ?: return
+ startActivity(
+ Intent(requireActivity(), VoiceCallActivity::class.java)
+ .putExtra(VoiceCallActivity.EXTRA_NAME, contact.name)
+ .putExtra(VoiceCallActivity.EXTRA_ICON, contact.icon)
+ )
+ }
+
+ private fun send() {
+ val text = input.text.toString()
+ if (text.isNotEmpty()) {
+ input.text.clear()
+ viewModel.send(text)
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
+ inflater?.inflate(R.menu.chat, menu)
+ menu?.findItem(R.id.action_show_as_bubble)?.let { item ->
+ viewModel.showAsBubbleVisible.observe(viewLifecycleOwner, Observer {
+ item.isVisible = it
+ })
+ }
+ super.onCreateOptionsMenu(menu, inflater)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem?): Boolean {
+ return when (item?.itemId) {
+ R.id.action_show_as_bubble -> {
+ viewModel.showAsBubble()
+ fragmentManager?.popBackStack()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+}
diff --git a/notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/chat/ChatViewModel.kt b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/chat/ChatViewModel.kt
new file mode 100644
index 00000000..df8746ca
--- /dev/null
+++ b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/chat/ChatViewModel.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.bubbles.ui.chat
+
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Transformations
+import com.example.android.bubbles.data.ChatRepository
+import com.example.android.bubbles.data.Contact
+import com.example.android.bubbles.data.DefaultChatRepository
+import com.example.android.bubbles.data.Message
+
+class ChatViewModel @JvmOverloads constructor(
+ application: Application,
+ private val repository: ChatRepository = DefaultChatRepository.getInstance(application)
+) : AndroidViewModel(application) {
+
+ private val chatId = MutableLiveData<Long>()
+
+ /**
+ * We want to dismiss a notification when the corresponding chat screen is open. Setting this to `true` dismisses
+ * the current notification and suppresses further notifications.
+ *
+ * We do want to keep on showing and updating the notification when the chat screen is opened as an expanded bubble.
+ * [ChatFragment] should set this to false if it is launched in BubbleActivity. Otherwise, the expanding a bubble
+ * would remove the notification and the bubble.
+ */
+ var foreground = false
+ set(value) {
+ field = value
+ chatId.value?.let { id ->
+ if (value) {
+ repository.activateChat(id)
+ } else {
+ repository.deactivateChat(id)
+ }
+ }
+ }
+
+ /**
+ * The contact of this chat.
+ */
+ val contact: LiveData<Contact?> = Transformations.switchMap(chatId) { id ->
+ repository.findContact(id)
+ }
+
+ /**
+ * The list of all the messages in this chat.
+ */
+ val messages: LiveData<List<Message>> = Transformations.switchMap(chatId) { id ->
+ repository.findMessages(id)
+ }
+
+ /**
+ * Whether the "Show as Bubble" button should be shown.
+ */
+ val showAsBubbleVisible: LiveData<Boolean> = object: LiveData<Boolean>() {
+ override fun onActive() {
+ // We hide the "Show as Bubble" button if we are not allowed to show the bubble.
+ value = repository.canBubble()
+ }
+ }
+
+ fun setChatId(id: Long) {
+ chatId.value = id
+ if (foreground) {
+ repository.activateChat(id)
+ } else {
+ repository.deactivateChat(id)
+ }
+ }
+
+ fun send(text: String) {
+ val id = chatId.value
+ if (id != null && id != 0L) {
+ repository.sendMessage(id, text)
+ }
+ }
+
+ fun showAsBubble() {
+ chatId.value?.let { id ->
+ repository.showAsBubble(id)
+ }
+ }
+
+ override fun onCleared() {
+ chatId.value?.let { id -> repository.deactivateChat(id) }
+ }
+}
diff --git a/notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/chat/MessageAdapter.kt b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/chat/MessageAdapter.kt
new file mode 100644
index 00000000..9fd72428
--- /dev/null
+++ b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/chat/MessageAdapter.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.bubbles.ui.chat
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.core.view.ViewCompat
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.example.android.bubbles.R
+import com.example.android.bubbles.data.Message
+
+class MessageAdapter(
+ context: Context,
+ private val onPhotoClicked: (photo: Int) -> Unit
+) : ListAdapter<Message, MessageViewHolder>(DIFF_CALLBACK) {
+
+ private val tint = object {
+ val incoming: ColorStateList = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.incoming))
+ val outgoing: ColorStateList = ColorStateList.valueOf(
+ ContextCompat.getColor(context, R.color.outgoing)
+ )
+ }
+
+ private val padding = object {
+ val vertical: Int = context.resources.getDimensionPixelSize(R.dimen.message_padding_vertical)
+
+ val horizontalShort: Int = context.resources.getDimensionPixelSize(
+ R.dimen.message_padding_horizontal_short
+ )
+
+ val horizontalLong: Int = context.resources.getDimensionPixelSize(
+ R.dimen.message_padding_horizontal_long
+ )
+ }
+
+
+ init {
+ setHasStableIds(true)
+ }
+
+ override fun getItemId(position: Int): Long {
+ return getItem(position).id
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder {
+ val holder = MessageViewHolder(parent)
+ holder.message.setOnClickListener {
+ val photo: Int? = it.getTag(R.id.tag_photo) as Int?
+ if (photo != null) {
+ onPhotoClicked(photo)
+ }
+ }
+ return holder
+ }
+
+ override fun onBindViewHolder(holder: MessageViewHolder, position: Int) {
+ val message = getItem(position)
+ val lp = holder.message.layoutParams as FrameLayout.LayoutParams
+ if (message.isIncoming) {
+ holder.message.run {
+ setBackgroundResource(R.drawable.message_incoming)
+ ViewCompat.setBackgroundTintList(this, tint.incoming)
+ setPadding(
+ padding.horizontalLong, padding.vertical,
+ padding.horizontalShort, padding.vertical
+ )
+ layoutParams = lp.apply {
+ gravity = Gravity.START
+ }
+ if (message.photo != null) {
+ holder.message.setTag(R.id.tag_photo, message.photo)
+ setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, message.photo)
+ } else {
+ holder.message.setTag(R.id.tag_photo, null)
+ setCompoundDrawables(null, null, null, null)
+ }
+ }
+ } else {
+ holder.message.run {
+ setBackgroundResource(R.drawable.message_outgoing)
+ ViewCompat.setBackgroundTintList(this, tint.outgoing)
+ setPadding(
+ padding.horizontalShort, padding.vertical,
+ padding.horizontalLong, padding.vertical
+ )
+ layoutParams = lp.apply {
+ gravity = Gravity.END
+ }
+ }
+ }
+ holder.message.text = message.text
+ }
+}
+
+private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Message>() {
+
+ override fun areItemsTheSame(oldItem: Message, newItem: Message): Boolean {
+ return oldItem.id == newItem.id
+ }
+
+ override fun areContentsTheSame(oldItem: Message, newItem: Message): Boolean {
+ return oldItem == newItem
+ }
+
+}
+
+class MessageViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
+ LayoutInflater.from(parent.context).inflate(R.layout.message_item, parent, false)
+) {
+ val message: TextView = itemView.findViewById(R.id.message)
+}
diff --git a/notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/main/ContactAdapter.kt b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/main/ContactAdapter.kt
new file mode 100644
index 00000000..4badf9b5
--- /dev/null
+++ b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/main/ContactAdapter.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.bubbles.ui.main
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.bumptech.glide.Glide
+import com.bumptech.glide.request.RequestOptions
+import com.example.android.bubbles.R
+import com.example.android.bubbles.data.Contact
+
+class ContactAdapter(
+ private val onChatClicked: (id: Long) -> Unit
+) : ListAdapter<Contact, ContactViewHolder>(DIFF_CALLBACK) {
+
+ init {
+ setHasStableIds(true)
+ }
+
+ override fun getItemId(position: Int): Long {
+ return getItem(position).id
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ContactViewHolder {
+ val holder = ContactViewHolder(parent)
+ holder.itemView.setOnClickListener {
+ onChatClicked(holder.itemId)
+ }
+ return holder
+ }
+
+ override fun onBindViewHolder(holder: ContactViewHolder, position: Int) {
+ val contact: Contact = getItem(position)
+ Glide.with(holder.icon).load(contact.icon).apply(RequestOptions.circleCropTransform()).into(holder.icon)
+ holder.name.text = contact.name
+ }
+}
+
+private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Contact>() {
+ override fun areItemsTheSame(oldItem: Contact, newItem: Contact): Boolean {
+ return oldItem.id == newItem.id
+ }
+
+ override fun areContentsTheSame(oldItem: Contact, newItem: Contact): Boolean {
+ return oldItem == newItem
+ }
+}
+
+class ContactViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
+ LayoutInflater.from(parent.context).inflate(R.layout.chat_item, parent, false)
+) {
+ val icon: ImageView = itemView.findViewById(R.id.icon)
+ val name: TextView = itemView.findViewById(R.id.name)
+}
diff --git a/notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/main/MainFragment.kt b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/main/MainFragment.kt
new file mode 100644
index 00000000..ee4b9207
--- /dev/null
+++ b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/main/MainFragment.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.bubbles.ui.main
+
+import android.os.Bundle
+import android.transition.TransitionInflater
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProviders
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.example.android.bubbles.R
+import com.example.android.bubbles.getNavigationController
+
+/**
+ * The main chat list screen.
+ */
+class MainFragment : Fragment() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ exitTransition = TransitionInflater.from(context).inflateTransition(R.transition.slide_top)
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ return inflater.inflate(R.layout.main_fragment, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ val navigationController = getNavigationController()
+ navigationController.updateAppBar(false)
+ val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
+
+ val contactAdapter = ContactAdapter { id ->
+ navigationController.openChat(id)
+ }
+ viewModel.contacts.observe(viewLifecycleOwner, Observer { contacts ->
+ contactAdapter.submitList(contacts)
+ })
+
+ view.findViewById<RecyclerView>(R.id.contacts).run {
+ layoutManager = LinearLayoutManager(view.context)
+ setHasFixedSize(true)
+ adapter = contactAdapter
+ }
+ }
+}
diff --git a/notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/main/MainViewModel.kt b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/main/MainViewModel.kt
new file mode 100644
index 00000000..f959352c
--- /dev/null
+++ b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/main/MainViewModel.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.bubbles.ui.main
+
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import com.example.android.bubbles.data.ChatRepository
+import com.example.android.bubbles.data.DefaultChatRepository
+
+class MainViewModel @JvmOverloads constructor(
+ application: Application,
+ repository: ChatRepository = DefaultChatRepository.getInstance(application)
+) : AndroidViewModel(application) {
+
+ /**
+ * All the contacts.
+ */
+ val contacts = repository.getContacts()
+}
diff --git a/notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/photo/PhotoFragment.kt b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/photo/PhotoFragment.kt
new file mode 100644
index 00000000..14f6594d
--- /dev/null
+++ b/notification/Bubbles/app/src/main/java/com/example/android/bubbles/ui/photo/PhotoFragment.kt
@@ -0,0 +1,47 @@
+package com.example.android.bubbles.ui.photo
+
+import android.os.Bundle
+import android.transition.Fade
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.annotation.DrawableRes
+import androidx.fragment.app.Fragment
+import com.example.android.bubbles.R
+import com.example.android.bubbles.getNavigationController
+
+/**
+ * Shows the specified [DrawableRes] as a full-screen photo.
+ */
+class PhotoFragment : Fragment() {
+
+ companion object {
+ private const val ARG_PHOTO = "photo"
+
+ fun newInstance(@DrawableRes photo: Int) = PhotoFragment().apply {
+ arguments = Bundle().apply {
+ putInt(ARG_PHOTO, photo)
+ }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enterTransition = Fade()
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ return inflater.inflate(R.layout.photo_fragment, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ val photoResId = arguments?.getInt(ARG_PHOTO)
+ if (photoResId == null) {
+ fragmentManager?.popBackStack()
+ return
+ }
+ getNavigationController().updateAppBar(hidden = true)
+ view.findViewById<ImageView>(R.id.photo).setImageResource(photoResId)
+ }
+}
diff --git a/notification/Bubbles/app/src/main/res/drawable-nodpi/cat.jpg b/notification/Bubbles/app/src/main/res/drawable-nodpi/cat.jpg
new file mode 100644
index 00000000..61961708
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/drawable-nodpi/cat.jpg
Binary files differ
diff --git a/notification/Bubbles/app/src/main/res/drawable-nodpi/dog.jpg b/notification/Bubbles/app/src/main/res/drawable-nodpi/dog.jpg
new file mode 100644
index 00000000..df159007
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/drawable-nodpi/dog.jpg
Binary files differ
diff --git a/notification/Bubbles/app/src/main/res/drawable-nodpi/parrot.jpg b/notification/Bubbles/app/src/main/res/drawable-nodpi/parrot.jpg
new file mode 100644
index 00000000..8e2e18df
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/drawable-nodpi/parrot.jpg
Binary files differ
diff --git a/notification/Bubbles/app/src/main/res/drawable-nodpi/sheep.jpg b/notification/Bubbles/app/src/main/res/drawable-nodpi/sheep.jpg
new file mode 100644
index 00000000..39876f9c
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/drawable-nodpi/sheep.jpg
Binary files differ
diff --git a/notification/Bubbles/app/src/main/res/drawable-nodpi/sheep_full.jpg b/notification/Bubbles/app/src/main/res/drawable-nodpi/sheep_full.jpg
new file mode 100644
index 00000000..ad5e8c15
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/drawable-nodpi/sheep_full.jpg
Binary files differ
diff --git a/notification/Bubbles/app/src/main/res/drawable/ic_launcher_foreground.xml b/notification/Bubbles/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 00000000..f7cf56d0
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,18 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="55.644802"
+ android:viewportHeight="55.644802">
+ <group android:translateX="3.4717517"
+ android:translateY="3.4717517">
+ <group android:translateX="-1.2175325"
+ android:translateY="-1.2175325">
+ <group android:translateX="13.568182"
+ android:translateY="13.568182">
+ <path
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,14L6,14v-2h12v2zM18,11L6,11L6,9h12v2zM18,8L6,8L6,6h12v2z"/>
+ </group>
+ </group>
+ </group>
+</vector>
diff --git a/notification/Bubbles/app/src/main/res/drawable/ic_message.xml b/notification/Bubbles/app/src/main/res/drawable/ic_message.xml
new file mode 100644
index 00000000..d2876bfa
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/drawable/ic_message.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,14L6,14v-2h12v2zM18,11L6,11L6,9h12v2zM18,8L6,8L6,6h12v2z"/>
+</vector>
diff --git a/notification/Bubbles/app/src/main/res/drawable/ic_open_in_new.xml b/notification/Bubbles/app/src/main/res/drawable/ic_open_in_new.xml
new file mode 100644
index 00000000..bd528267
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/drawable/ic_open_in_new.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="#FFFFFF"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FFFFFF"
+ android:pathData="M19,19H5V5h7V3H5c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2v-7h-2v7zM14,3v2h3.59l-9.83,9.83 1.41,1.41L19,6.41V10h2V3h-7z" />
+</vector>
diff --git a/notification/Bubbles/app/src/main/res/drawable/ic_send.xml b/notification/Bubbles/app/src/main/res/drawable/ic_send.xml
new file mode 100644
index 00000000..e145ca83
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/drawable/ic_send.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/>
+</vector>
diff --git a/notification/Bubbles/app/src/main/res/drawable/ic_voice_call.xml b/notification/Bubbles/app/src/main/res/drawable/ic_voice_call.xml
new file mode 100644
index 00000000..ebf9de60
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/drawable/ic_voice_call.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M6.62,10.79c1.44,2.83 3.76,5.14 6.59,6.59l2.2,-2.2c0.27,-0.27 0.67,-0.36 1.02,-0.24 1.12,0.37 2.33,0.57 3.57,0.57 0.55,0 1,0.45 1,1V20c0,0.55 -0.45,1 -1,1 -9.39,0 -17,-7.61 -17,-17 0,-0.55 0.45,-1 1,-1h3.5c0.55,0 1,0.45 1,1 0,1.25 0.2,2.45 0.57,3.57 0.11,0.35 0.03,0.74 -0.25,1.02l-2.2,2.2z"/>
+</vector>
diff --git a/notification/Bubbles/app/src/main/res/drawable/message_incoming.xml b/notification/Bubbles/app/src/main/res/drawable/message_incoming.xml
new file mode 100644
index 00000000..94871b4b
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/drawable/message_incoming.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2019 Google Inc. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<layer-list
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <rotate
+ android:fromDegrees="-45"
+ android:pivotX="0%"
+ android:pivotY="0%"
+ android:toDegrees="0">
+ <shape android:shape="rectangle">
+ <solid android:color="#fff" />
+ </shape>
+ </rotate>
+ </item>
+ <item android:left="8dp">
+ <shape android:shape="rectangle">
+ <solid android:color="#fff" />
+ <corners android:radius="4dp" />
+ </shape>
+ </item>
+</layer-list>
diff --git a/notification/Bubbles/app/src/main/res/drawable/message_outgoing.xml b/notification/Bubbles/app/src/main/res/drawable/message_outgoing.xml
new file mode 100644
index 00000000..39cc7b1b
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/drawable/message_outgoing.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2017 Google Inc. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<layer-list
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <rotate
+ android:fromDegrees="45"
+ android:pivotX="100%"
+ android:pivotY="0%"
+ android:toDegrees="0">
+ <shape android:shape="rectangle">
+ <solid android:color="#fff" />
+ </shape>
+ </rotate>
+ </item>
+ <item android:right="8dp">
+ <shape android:shape="rectangle">
+ <solid android:color="#fff" />
+ <corners android:radius="4dp" />
+ </shape>
+ </item>
+</layer-list>
diff --git a/notification/Bubbles/app/src/main/res/layout/bubble_activity.xml b/notification/Bubbles/app/src/main/res/layout/bubble_activity.xml
new file mode 100644
index 00000000..6a766a07
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/layout/bubble_activity.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
diff --git a/notification/Bubbles/app/src/main/res/layout/chat_fragment.xml b/notification/Bubbles/app/src/main/res/layout/chat_fragment.xml
new file mode 100644
index 00000000..4ce86e4b
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/layout/chat_fragment.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/chat"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/messages"
+ style="?attr/buttonBarStyle"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:clipToPadding="false"
+ android:paddingTop="@dimen/spacing_small"
+ android:paddingBottom="@dimen/spacing_small"
+ android:scrollbars="vertical" />
+
+ <LinearLayout
+ android:id="@+id/input_bar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ android:background="?android:attr/windowBackground"
+ android:elevation="@dimen/app_bar_elevation"
+ android:orientation="horizontal">
+
+ <ImageButton
+ android:id="@+id/voice_call"
+ style="?attr/buttonBarNeutralButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/description_voice_call"
+ android:tint="?attr/colorAccent"
+ app:srcCompat="@drawable/ic_voice_call" />
+
+ <EditText
+ android:id="@+id/input"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:hint="@string/hint_input"
+ android:imeOptions="actionSend"
+ android:importantForAutofill="no"
+ android:inputType="textCapSentences" />
+
+ <ImageButton
+ android:id="@+id/send"
+ style="?attr/buttonBarNeutralButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/description_send"
+ android:tint="?attr/colorAccent"
+ app:srcCompat="@drawable/ic_send" />
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/notification/Bubbles/app/src/main/res/layout/chat_item.xml b/notification/Bubbles/app/src/main/res/layout/chat_item.xml
new file mode 100644
index 00000000..6f2bafcf
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/layout/chat_item.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/chat"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?attr/selectableItemBackground"
+ android:minHeight="?attr/listPreferredItemHeightLarge"
+ android:orientation="horizontal"
+ android:paddingStart="@dimen/spacing_small"
+ android:paddingEnd="@dimen/spacing_small"
+ tools:ignore="UseCompoundDrawables">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="64dp"
+ android:layout_height="64dp"
+ android:layout_gravity="center_vertical"
+ android:layout_margin="@dimen/spacing_small"
+ android:contentDescription="@string/description_icon"
+ tools:src="@drawable/cat" />
+
+ <TextView
+ android:id="@+id/name"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_margin="@dimen/spacing_small"
+ android:layout_weight="1"
+ android:maxLines="1"
+ android:textAppearance="@style/TextAppearance.AppCompat.Large"
+ tools:text="Cat" />
+
+</LinearLayout>
diff --git a/notification/Bubbles/app/src/main/res/layout/main_activity.xml b/notification/Bubbles/app/src/main/res/layout/main_activity.xml
new file mode 100644
index 00000000..df2a66e1
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/layout/main_activity.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/root"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ tools:context=".MainActivity">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/app_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?attr/colorPrimary"
+ android:elevation="@dimen/app_bar_elevation"
+ android:theme="@style/ThemeOverlay.AppCompat.Dark">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="@dimen/icon_size"
+ android:layout_height="@dimen/icon_size"
+ android:layout_marginVertical="@dimen/spacing_small"
+ android:layout_marginStart="@dimen/spacing_medium"
+ android:contentDescription="@string/description_icon"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:src="@drawable/cat" />
+
+ <TextView
+ android:id="@+id/name"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginStart="@dimen/spacing_medium"
+ android:gravity="center_vertical"
+ android:maxLines="1"
+ android:textAppearance="@style/TextAppearance.AppCompat.Large"
+ app:layout_constraintBottom_toBottomOf="@id/icon"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/icon"
+ app:layout_constraintTop_toTopOf="@id/icon"
+ tools:text="Cat" />
+
+ <androidx.appcompat.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+ <FrameLayout
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+
+</LinearLayout>
diff --git a/notification/Bubbles/app/src/main/res/layout/main_fragment.xml b/notification/Bubbles/app/src/main/res/layout/main_fragment.xml
new file mode 100644
index 00000000..36a89bc7
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/layout/main_fragment.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<androidx.recyclerview.widget.RecyclerView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/contacts"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false"
+ android:paddingTop="@dimen/spacing_small"
+ android:paddingBottom="@dimen/spacing_small" />
diff --git a/notification/Bubbles/app/src/main/res/layout/message_item.xml b/notification/Bubbles/app/src/main/res/layout/message_item.xml
new file mode 100644
index 00000000..b162a945
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/layout/message_item.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/spacing_small" />
+
+</FrameLayout>
diff --git a/notification/Bubbles/app/src/main/res/layout/photo_fragment.xml b/notification/Bubbles/app/src/main/res/layout/photo_fragment.xml
new file mode 100644
index 00000000..15b63d38
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/layout/photo_fragment.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<ImageView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/photo"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/black"
+ android:scaleType="fitCenter"
+ tools:src="@drawable/sheep_full"
+ android:contentDescription="@string/description_photo" />
diff --git a/notification/Bubbles/app/src/main/res/layout/voice_call_activity.xml b/notification/Bubbles/app/src/main/res/layout/voice_call_activity.xml
new file mode 100644
index 00000000..9326eb7d
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/layout/voice_call_activity.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/voice"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ tools:context=".VoiceCallActivity">
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="@dimen/icon_size"
+ android:layout_height="@dimen/icon_size"
+ android:layout_gravity="center_horizontal"
+ android:layout_margin="@dimen/spacing_medium"
+ android:contentDescription="@string/description_icon"
+ tools:src="@drawable/cat" />
+
+ <TextView
+ android:id="@+id/name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/spacing_medium"
+ android:gravity="center_horizontal"
+ android:textAppearance="@style/TextAppearance.AppCompat.Large"
+ tools:text="Cat" />
+
+ <TextView
+ android:id="@+id/explanation"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/spacing_medium"
+ android:gravity="center_horizontal"
+ android:text="@string/voice_call_explanation"
+ android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+
+</LinearLayout>
diff --git a/notification/Bubbles/app/src/main/res/menu/chat.xml b/notification/Bubbles/app/src/main/res/menu/chat.xml
new file mode 100644
index 00000000..f7b1269c
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/menu/chat.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/action_show_as_bubble"
+ android:icon="@drawable/ic_open_in_new"
+ android:title="@string/show_as_bubble"
+ app:showAsAction="ifRoom" />
+
+</menu>
diff --git a/notification/Bubbles/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/notification/Bubbles/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..7353dbd1
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@color/ic_launcher_background"/>
+ <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+</adaptive-icon> \ No newline at end of file
diff --git a/notification/Bubbles/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/notification/Bubbles/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..7353dbd1
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@color/ic_launcher_background"/>
+ <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+</adaptive-icon> \ No newline at end of file
diff --git a/notification/Bubbles/app/src/main/res/mipmap-hdpi/ic_launcher.png b/notification/Bubbles/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..add49201
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/notification/Bubbles/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/notification/Bubbles/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 00000000..36a7f52a
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/notification/Bubbles/app/src/main/res/mipmap-mdpi/ic_launcher.png b/notification/Bubbles/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..c1b13dd4
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/notification/Bubbles/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/notification/Bubbles/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 00000000..d7c39d6c
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/notification/Bubbles/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/notification/Bubbles/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..f7747291
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/notification/Bubbles/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/notification/Bubbles/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..f0e3e03e
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/notification/Bubbles/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/notification/Bubbles/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..85e929ff
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/notification/Bubbles/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/notification/Bubbles/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..156450ac
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/notification/Bubbles/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/notification/Bubbles/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..51b123c7
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/notification/Bubbles/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/notification/Bubbles/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..569927db
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/notification/Bubbles/app/src/main/res/transition/app_bar.xml b/notification/Bubbles/app/src/main/res/transition/app_bar.xml
new file mode 100644
index 00000000..462cfd4c
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/transition/app_bar.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<transitionSet
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="250"
+ android:interpolator="@android:interpolator/accelerate_decelerate"
+ android:transitionOrdering="together">
+
+ <fade />
+ <changeBounds />
+
+</transitionSet>
diff --git a/notification/Bubbles/app/src/main/res/transition/slide_bottom.xml b/notification/Bubbles/app/src/main/res/transition/slide_bottom.xml
new file mode 100644
index 00000000..f41bfb42
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/transition/slide_bottom.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<slide
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="250"
+ android:interpolator="@android:interpolator/accelerate_decelerate"
+ android:slideEdge="bottom" />
diff --git a/notification/Bubbles/app/src/main/res/transition/slide_top.xml b/notification/Bubbles/app/src/main/res/transition/slide_top.xml
new file mode 100644
index 00000000..9b68cd70
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/transition/slide_top.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<slide
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="250"
+ android:interpolator="@android:interpolator/accelerate_decelerate"
+ android:slideEdge="top" />
diff --git a/notification/Bubbles/app/src/main/res/values/colors.xml b/notification/Bubbles/app/src/main/res/values/colors.xml
new file mode 100644
index 00000000..b4ca87b3
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/values/colors.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <color name="primary">#008577</color>
+ <color name="primary_dark">#00574B</color>
+ <color name="accent">#D81B60</color>
+
+ <color name="incoming">#FBE9E7</color>
+ <color name="outgoing">#EEEEEE</color>
+</resources>
diff --git a/notification/Bubbles/app/src/main/res/values/dimens.xml b/notification/Bubbles/app/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..be251581
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/values/dimens.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <dimen name="spacing_medium">16dp</dimen>
+ <dimen name="spacing_small">8dp</dimen>
+ <dimen name="icon_size">64dp</dimen>
+ <dimen name="app_bar_elevation">4dp</dimen>
+ <dimen name="message_padding_vertical">16dp</dimen>
+ <dimen name="message_padding_horizontal_short">16dp</dimen>
+ <dimen name="message_padding_horizontal_long">24dp</dimen>
+ <dimen name="bubble_height">400dp</dimen>
+</resources>
diff --git a/notification/Bubbles/app/src/main/res/values/ic_launcher_background.xml b/notification/Bubbles/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 00000000..c1333504
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="ic_launcher_background">#1C7A71</color>
+</resources> \ No newline at end of file
diff --git a/notification/Bubbles/app/src/main/res/values/ids.xml b/notification/Bubbles/app/src/main/res/values/ids.xml
new file mode 100644
index 00000000..e205a5bf
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/values/ids.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <item name="tag_photo" type="id" />
+</resources>
diff --git a/notification/Bubbles/app/src/main/res/values/strings.xml b/notification/Bubbles/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..aae789ed
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/values/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <string name="app_name">Bubbles</string>
+ <string name="show_as_bubble">Show as Bubble</string>
+ <string name="description_icon">Profile icon</string>
+ <string name="description_voice_call">Make a voice call (dummy)</string>
+ <string name="description_send">Send</string>
+ <string name="description_photo">Photo</string>
+ <string name="hint_input">Type a message…</string>
+ <string name="channel_new_messages">New messages</string>
+ <string name="channel_new_messages_description">All new incoming messages.</string>
+ <string name="chat_with_contact">Chat with %s</string>
+ <string name="voice_call_explanation">This is a dummy voice call screen.</string>
+</resources>
diff --git a/notification/Bubbles/app/src/main/res/values/styles.xml b/notification/Bubbles/app/src/main/res/values/styles.xml
new file mode 100644
index 00000000..b85424d7
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/values/styles.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+
+ <style name="Theme.Bubbles" parent="Theme.AppCompat.Light.NoActionBar">
+ <item name="colorPrimary">@color/primary</item>
+ <item name="colorPrimaryDark">@color/primary_dark</item>
+ <item name="colorAccent">@color/accent</item>
+ </style>
+
+ <style name="Theme.Bubbles.Voice" parent="Theme.AppCompat.NoActionBar">
+ <item name="colorPrimary">@color/primary</item>
+ <item name="colorPrimaryDark">@color/primary_dark</item>
+ <item name="colorAccent">@color/accent</item>
+ </style>
+
+</resources>
diff --git a/notification/Bubbles/app/src/test/java/com/example/android/bubbles/LiveDataTestUtils.kt b/notification/Bubbles/app/src/test/java/com/example/android/bubbles/LiveDataTestUtils.kt
new file mode 100644
index 00000000..6c74bb36
--- /dev/null
+++ b/notification/Bubbles/app/src/test/java/com/example/android/bubbles/LiveDataTestUtils.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.bubbles
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Observer
+import androidx.test.platform.app.InstrumentationRegistry
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+/**
+ * Observes this [LiveData] and returns the value.
+ *
+ * @throws NullPointerException if the observed value is null.
+ */
+fun <T> LiveData<T>.observedValue(): T {
+ var result: T? = null
+ val latch = CountDownLatch(1)
+ val observer = Observer<T> {
+ result = it
+ latch.countDown()
+ }
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ observeForever(observer)
+ }
+ latch.await(3000L, TimeUnit.MILLISECONDS)
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ removeObserver(observer)
+ }
+ return result!!
+}
diff --git a/notification/Bubbles/app/src/test/java/com/example/android/bubbles/data/TestChatRepository.kt b/notification/Bubbles/app/src/test/java/com/example/android/bubbles/data/TestChatRepository.kt
new file mode 100644
index 00000000..65d8d9c9
--- /dev/null
+++ b/notification/Bubbles/app/src/test/java/com/example/android/bubbles/data/TestChatRepository.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.bubbles.data
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+
+/**
+ * This is like [DefaultChatRepository] except:
+ * - The initial chat history can be supplied as a constructor parameter.
+ * - It does not wait 5 seconds to receive a reply.
+ */
+class TestChatRepository(private val chats: Map<Long, Chat>) : ChatRepository {
+
+ var activatedId = 0L
+
+ var bubbleId = 0L
+
+ override fun getContacts(): LiveData<List<Contact>> {
+ return MutableLiveData<List<Contact>>().apply {
+ value = chats.values.map { it.contact }
+ }
+ }
+
+ override fun findContact(id: Long): LiveData<Contact?> {
+ return MutableLiveData<Contact>().apply {
+ value = Contact.CONTACTS.find { it.id == id }
+ }
+ }
+
+ override fun findMessages(id: Long): LiveData<List<Message>> {
+ val chat = chats.getValue(id)
+ return object : LiveData<List<Message>>() {
+
+ private val listener = { messages: List<Message> ->
+ postValue(messages)
+ }
+
+ override fun onActive() {
+ value = chat.messages
+ chat.addListener(listener)
+ }
+
+ override fun onInactive() {
+ chat.removeListener(listener)
+ }
+ }
+ }
+
+ override fun sendMessage(id: Long, text: String) {
+ val chat = chats.getValue(id)
+ chat.addMessage(Message.Builder().apply {
+ sender = 0L // User
+ this.text = text
+ timestamp = System.currentTimeMillis()
+ })
+ chat.addMessage(chat.contact.reply(text))
+ }
+
+ override fun activateChat(id: Long) {
+ activatedId = id
+ }
+
+ override fun deactivateChat(id: Long) {
+ activatedId = 0L
+ }
+
+ override fun showAsBubble(id: Long) {
+ bubbleId = id
+ }
+
+ override fun canBubble(): Boolean {
+ return true
+ }
+}
diff --git a/notification/Bubbles/app/src/test/java/com/example/android/bubbles/ui/chat/ChatViewModelTest.kt b/notification/Bubbles/app/src/test/java/com/example/android/bubbles/ui/chat/ChatViewModelTest.kt
new file mode 100644
index 00000000..6a136399
--- /dev/null
+++ b/notification/Bubbles/app/src/test/java/com/example/android/bubbles/ui/chat/ChatViewModelTest.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.bubbles.ui.chat
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.example.android.bubbles.data.Chat
+import com.example.android.bubbles.data.Contact
+import com.example.android.bubbles.data.TestChatRepository
+import com.example.android.bubbles.observedValue
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class ChatViewModelTest {
+
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ private val dummyContacts = Contact.CONTACTS
+
+ private lateinit var viewModel: ChatViewModel
+ private lateinit var repository: TestChatRepository
+
+ @Before
+ fun createViewModel() {
+ repository = TestChatRepository(dummyContacts.map { contact ->
+ contact.id to Chat(contact)
+ }.toMap())
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ viewModel = ChatViewModel(ApplicationProvider.getApplicationContext(), repository)
+ }
+ }
+
+ @Test
+ fun hasContactAndMessages() {
+ viewModel.setChatId(1L)
+ viewModel.foreground = true
+ assertThat(viewModel.contact.observedValue()).isEqualTo(dummyContacts.find { it.id == 1L })
+ assertThat(viewModel.messages.observedValue()).hasSize(2)
+ assertThat(repository.activatedId).isEqualTo(1L)
+ }
+
+ @Test
+ fun sendAndReceiveReply() {
+ viewModel.setChatId(1L)
+ viewModel.send("a")
+ val messages = viewModel.messages.observedValue()
+ assertThat(messages).hasSize(4)
+ assertThat(messages[2].text).isEqualTo("a")
+ assertThat(messages[3].text).isEqualTo("Meow")
+ }
+
+ @Test
+ fun showAsBubble() {
+ viewModel.setChatId(1L)
+ assertThat(repository.bubbleId).isEqualTo(0L)
+ viewModel.showAsBubble()
+ assertThat(repository.bubbleId).isEqualTo(1L)
+ }
+
+}
diff --git a/notification/Bubbles/app/src/test/java/com/example/android/bubbles/ui/main/MainViewModelTest.kt b/notification/Bubbles/app/src/test/java/com/example/android/bubbles/ui/main/MainViewModelTest.kt
new file mode 100644
index 00000000..4f1f5e6a
--- /dev/null
+++ b/notification/Bubbles/app/src/test/java/com/example/android/bubbles/ui/main/MainViewModelTest.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.bubbles.ui.main
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.example.android.bubbles.data.Chat
+import com.example.android.bubbles.data.Contact
+import com.example.android.bubbles.data.TestChatRepository
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class MainViewModelTest {
+
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ private val dummyContacts = Contact.CONTACTS
+
+ private fun createViewModel(): MainViewModel {
+ var viewModel: MainViewModel? = null
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ viewModel = MainViewModel(
+ ApplicationProvider.getApplicationContext(),
+ TestChatRepository(dummyContacts.map { contact ->
+ contact.id to Chat(contact)
+ }.toMap())
+ )
+ }
+ return viewModel!!
+ }
+
+ @Test
+ fun hasListOfContacts() {
+ val viewModel = createViewModel()
+ val contacts = viewModel.contacts.value
+ assertThat(contacts).isEqualTo(dummyContacts)
+ }
+
+}
diff --git a/notification/Bubbles/build.gradle b/notification/Bubbles/build.gradle
new file mode 100644
index 00000000..d1db5ac2
--- /dev/null
+++ b/notification/Bubbles/build.gradle
@@ -0,0 +1,36 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ ext.kotlin_version = '1.3.21'
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.3.2'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
+
+
+// BEGIN_EXCLUDE
+import com.example.android.samples.build.SampleGenPlugin
+
+apply plugin: SampleGenPlugin
+samplegen {
+ pathToBuild "../../../../build"
+ pathToSamplesCommon "../../common"
+}
+apply from: "../../../../build/build.gradle"
+// END_EXCLUDE
diff --git a/notification/Bubbles/buildSrc/build.gradle b/notification/Bubbles/buildSrc/build.gradle
new file mode 100644
index 00000000..8963e1f8
--- /dev/null
+++ b/notification/Bubbles/buildSrc/build.gradle
@@ -0,0 +1,18 @@
+
+repositories {
+ google()
+ jcenter()
+ mavenCentral()
+}
+dependencies {
+ compile 'org.freemarker:freemarker:2.3.20'
+}
+
+sourceSets {
+ main {
+ groovy {
+ srcDir new File(rootDir, "../../../../../build/buildSrc/src/main/groovy")
+ }
+ }
+}
+
diff --git a/notification/Bubbles/gradle.properties b/notification/Bubbles/gradle.properties
new file mode 100644
index 00000000..23339e0d
--- /dev/null
+++ b/notification/Bubbles/gradle.properties
@@ -0,0 +1,21 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
diff --git a/notification/Bubbles/gradle/wrapper/gradle-wrapper.jar b/notification/Bubbles/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..87b738cb
--- /dev/null
+++ b/notification/Bubbles/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/notification/Bubbles/gradle/wrapper/gradle-wrapper.properties b/notification/Bubbles/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..ae45383b
--- /dev/null
+++ b/notification/Bubbles/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.3-all.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/notification/Bubbles/gradlew b/notification/Bubbles/gradlew
new file mode 100755
index 00000000..af6708ff
--- /dev/null
+++ b/notification/Bubbles/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/notification/Bubbles/gradlew.bat b/notification/Bubbles/gradlew.bat
new file mode 100644
index 00000000..0f8d5937
--- /dev/null
+++ b/notification/Bubbles/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/notification/Bubbles/screenshots/bubble.png b/notification/Bubbles/screenshots/bubble.png
new file mode 100644
index 00000000..bfa55cfb
--- /dev/null
+++ b/notification/Bubbles/screenshots/bubble.png
Binary files differ
diff --git a/notification/Bubbles/screenshots/chat.png b/notification/Bubbles/screenshots/chat.png
new file mode 100644
index 00000000..55c475bb
--- /dev/null
+++ b/notification/Bubbles/screenshots/chat.png
Binary files differ
diff --git a/notification/Bubbles/screenshots/icon-web.png b/notification/Bubbles/screenshots/icon-web.png
new file mode 100644
index 00000000..2c18de9e
--- /dev/null
+++ b/notification/Bubbles/screenshots/icon-web.png
Binary files differ
diff --git a/notification/Bubbles/screenshots/main.png b/notification/Bubbles/screenshots/main.png
new file mode 100644
index 00000000..ab89c979
--- /dev/null
+++ b/notification/Bubbles/screenshots/main.png
Binary files differ
diff --git a/notification/Bubbles/settings.gradle b/notification/Bubbles/settings.gradle
new file mode 100644
index 00000000..e7b4def4
--- /dev/null
+++ b/notification/Bubbles/settings.gradle
@@ -0,0 +1 @@
+include ':app'
diff --git a/notification/Bubbles/template-params.xml b/notification/Bubbles/template-params.xml
new file mode 100644
index 00000000..7c59c345
--- /dev/null
+++ b/notification/Bubbles/template-params.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<sample>
+ <name>Bubbles</name>
+ <group>Notification</group>
+ <package>com.example.android.bubbles</package>
+
+ <minSdk>Q</minSdk>
+
+ <strings>
+ <intro>
+ <![CDATA[
+This sample demonstrates how to use Bubbles API to show notifications as bubbles.
+]]>
+ </intro>
+ </strings>
+
+ <tempate src="base-build" />
+
+ <metadata>
+ <status>PUBLISHED</status>
+ <categories>Notification</categories>
+ <technologies>Android</technologies>
+ <languages>Kotlin</languages>
+ <solutions>Mobile</solutions>
+ <level>INTERMEDIATE</level>
+ <icon>screenshots/icon-web.png</icon>
+ <screenshots>
+ <img>screenshots/main.png</img>
+ <img>screenshots/chat.png</img>
+ </screenshots>
+ <api_refs>
+ <android>android.app.Notification.BubbleMetadata</android>
+ </api_refs>
+ <description>
+ This sample demonstrates how to use Bubbles API to show notifications as bubbles.
+ </description>
+ <intro>
+<![CDATA[
+### API Usage
+
+In order to show a notification as a bubble, call [setBubbleMetadata][1] and set metadata to the
+Notification.Builder. [Notification.BubbleMetadata.Builder] can be used to create the metadata. You
+can set the icon and the desired height of the bubble. The notification has to be in a notification
+channel with [IMPORTANCE_HIGH][2] in order to prompt the user to allow bubbles.
+
+When the bubble is clicked, it expands to a small window. This is called an expanded bubble. An
+expanded bubble is an Activity. Use [setIntent][3] to specify an Activity to be lauched as the
+expanded bubble. The Activity must be [resizeable][4] and [embedded][5]. It also has to be able to
+launch as multiple instances, so set [documentLaunchMode][6] to "always".
+
+You might want to provide a feature to let users explicitly show an expanded bubble when they
+perform some actions, e.g. tapping on a button to show content in a bubble. For this, call
+[setAutoExpandBubble][7] on BubbleMetadata.Builder. It may also make sense to suppress the initial
+notification using [setSuppressInitialNotification][8] in this situation. These flags work only when
+your app is in the foreground. *This feature does not yet work in Android Q Beta 2.*
+
+Bubbles can also be shown explicitly when the app is in the foreground.
+
+[1]: https://developer.android.com/reference/android/app/Notification.Builder.html#setBubbleMetadata
+[2]: https://developer.android.com/reference/android/app/NotificationManager.html#IMPORTANCE_HIGH
+[3]: https://developer.android.com/reference/android/app/Notification.BubbleMetadata.Builder.html#setIntent
+[4]: https://developer.android.com/guide/topics/manifest/activity-element.html#resizeableActivity
+[5]: https://developer.android.com/guide/topics/manifest/activity-element.html#embedded
+[6]: https://developer.android.com/guide/topics/manifest/activity-element.html#dlmode
+[7]: https://developer.android.com/reference/android/app/Notification.BubbleMetadata.Builder.html#setAutoExpandBubble
+[8]: https://developer.android.com/reference/android/app/Notification.BubbleMetadata.Builder.html#setSuppressInitialNotification
+
+### When to use Bubbles instead of normal notifications
+
+Bubbles take up screen real estate and cover other app content. You should only send a notification
+as a bubble if it is important enough such as ongoing communications, or if the user has explicitly
+requested a bubble for some content.
+
+### When Bubble is disabled
+
+Note that the bubble can be disabled by the user. In that case, a bubble notification is shown as a
+normal notification. You should always make sure your bubble notification works as a normal
+notification as well.
+
+### Expanded Bubbles
+
+System UI allows the user to horizontally swipe through expanded bubbles. You should avoid
+horizontal scrolling content in your Activity for expanded bubbles.
+
+### About the sample
+
+The sample is a dummy chat app. Talk to one of the chat bots, and it will reply back to you in
+several seconds. The notification is not shown when the app is in the foreground, so just press back
+or home before you get a reply.
+
+- cat.jpg: Photo by [Erik-Jan Leusink](https://unsplash.com/photos/IbPxGLgJiMI?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/search/photos/cat?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
+- dog.jpg: Photo by [Lui Peng](https://unsplash.com/photos/ybHtKz5He9Y?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on Unsplash
+- parrot.jpg: Photo by [Nikolay Tchaouchev](https://unsplash.com/photos/jJuq6oNfgRo?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on Unsplash
+- sheep.jpg, sheep-full.jpg: Photo by [Luke Stackpoole](https://unsplash.com/photos/sfB_Nw9sggw?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on Unsplash
+]]>
+ </intro>
+ </metadata>
+
+</sample>
diff --git a/projects.txt b/projects.txt
index 84d6fc3d..6fa29dce 100644
--- a/projects.txt
+++ b/projects.txt
@@ -115,3 +115,4 @@ input/autofill/AutofillFramework
views/EmojiCompat
ui/fonts/DownloadableFonts
wearable/wear/WearComplicationProvidersTestSuite
+content/SharingShortcuts
diff --git a/security/FingerprintDialog/Application/src/main/Android.mk b/security/FingerprintDialog/Application/src/main/Android.mk
index 30420c04..54ec850e 100644
--- a/security/FingerprintDialog/Application/src/main/Android.mk
+++ b/security/FingerprintDialog/Application/src/main/Android.mk
@@ -14,16 +14,17 @@
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
-LOCAL_STATIC_JAVA_LIBRARIES := \
- android-support-v7-appcompat \
- android-support-v4 \
- android-support-annotations
+LOCAL_USE_AAPT2 := true
+LOCAL_STATIC_ANDROID_LIBRARIES := \
+ androidx.appcompat_appcompat \
+ androidx.legacy_legacy-support-v4 \
+ androidx.annotation_annotation
LOCAL_MODULE_TAGS := samples
LOCAL_SRC_FILES := $(call all-subdir-java-files)
LOCAL_PACKAGE_NAME := FingerprintDialog
LOCAL_SDK_VERSION := current
LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res \
- frameworks/support/v7/appcompat/res
+ prebuilts/sdk/current/support/v7/appcompat/res
LOCAL_AAPT_FLAGS := --auto-add-overlay \
--extra-packages android.support.v7.appcompat
diff --git a/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/MainActivity.java b/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/MainActivity.java
index e30b4c22..867f6f94 100644
--- a/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/MainActivity.java
+++ b/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/MainActivity.java
@@ -26,9 +26,9 @@ import android.preference.PreferenceManager;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyPermanentlyInvalidatedException;
import android.security.keystore.KeyProperties;
-import android.support.annotation.Nullable;
-import android.support.v7.app.AppCompatActivity;
-import android.support.v7.widget.Toolbar;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
import android.util.Base64;
import android.util.Log;
import android.view.Menu;
diff --git a/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/SettingsActivity.java b/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/SettingsActivity.java
index 4ac88462..5ae75185 100644
--- a/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/SettingsActivity.java
+++ b/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/SettingsActivity.java
@@ -18,7 +18,7 @@ package com.example.android.fingerprintdialog;
import android.os.Bundle;
import android.preference.PreferenceFragment;
-import android.support.v7.app.AppCompatActivity;
+import androidx.appcompat.app.AppCompatActivity;
public class SettingsActivity extends AppCompatActivity {
diff --git a/security/FingerprintDialog/Application/src/main/res/layout/activity_main.xml b/security/FingerprintDialog/Application/src/main/res/layout/activity_main.xml
index 130bc8c7..956f5761 100644
--- a/security/FingerprintDialog/Application/src/main/res/layout/activity_main.xml
+++ b/security/FingerprintDialog/Application/src/main/res/layout/activity_main.xml
@@ -24,7 +24,7 @@
android:layout_height="wrap_content"
android:orientation="vertical">
- <android.support.v7.widget.Toolbar
+ <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
diff --git a/ui/text/README.md b/ui/text/README.md
index e6119549..8d08bc8b 100644
--- a/ui/text/README.md
+++ b/ui/text/README.md
@@ -1,47 +1,19 @@
-Text Styling
-============
-These samples shows how to style text on Android using spans, in [Java](https://github.com/googlesamples/android-text/tree/master/TextStyling-Java) and in [Kotlin](https://github.com/googlesamples/android-text/tree/master/TextStyling-Kotlin).
+Android Text Samples
+====================
+These samples show how to work with text in Android.
+Explore the samples
+-------------------
+The **TextStyling** [Java](https://github.com/googlesamples/android-text/tree/master/TextStyling-Java) and [Kotlin](https://github.com/googlesamples/android-text/tree/master/TextStyling-Kotlin) samples show how to style text using spans.
-Introduction
-------------
-The difference between [TextStyling-Java](https://github.com/googlesamples/android-text/tree/master/TextStyling-Java) and [TextStyling-Kotlin](https://github.com/googlesamples/android-text/tree/master/TextStyling-Kotlin) is only in the language. They have the same set of features, same class names and similar ways of testing the functionality.
-## Features
-Parse some hardcoded text and do the following:
-* Paragraphs starting with “> ” are transformed into quotes.
-* Text enclosed in “```” will be transformed into inline code block.
-* Lines starting with “+ ” or “* ” will be transformed into bullet points.
-To update the text, modify the value of `R.string.display_text`.
-This project is not meant to fully cover the markdown capabilities and has several limitations; for example, quotes do not support nesting other elements.
-
-## Implementation
-The text is parsed in the `Parser.parse` method and the spans are created in the `MarkdownBuilder.markdownToSpans` method.
-To see how to apply a span, check out `MarkdownBuilder.buildCodeBlockSpan`. To see how to apply multiple spans on the same string, see `MarkdownBuilder.buildQuoteSpan`. For examples of creating custom spans, see `BulletPointSpan`, `CodeBlockSpan` or `FontSpan`.
-
-## Testing
-Text parsing is tested with JUnit tests in `ParserTest`. Span building is tested via Android JUnit tests, in `MarkdownBuilderTest`.
-
-
-Getting Started
----------------
-
-Clone this repository, enter the top level directory and run `./gradlew tasks`
-to get an overview of all the tasks available for this project.
-
-Some important tasks are:
-
-```
-assembleDebug - Assembles all Debug builds.
-installDebug - Installs the Debug build.
-connectedAndroidTest - Installs and runs the tests for Debug build on connected
-devices.
-test - Run all unit tests.
-```
+The **RoundedBackground** sample shows how to draw a rounded corner background on a text that extends across one or multiple lines, supporting right-to-left text also.
Screenshots
-----------
<img src="screenshots/main_activity.png" width="30%" />
+<img src="screenshots/rounded_bg.png" width="30%" />
+
Support
-------
- Stack Overflow: http://stackoverflow.com/questions/tagged/android-text
@@ -72,4 +44,3 @@ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations under
the License.
```
-
diff --git a/ui/text/RoundedBackground-Kotlin/README.md b/ui/text/RoundedBackground-Kotlin/README.md
new file mode 100644
index 00000000..8bd96e79
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/README.md
@@ -0,0 +1,93 @@
+Drawing a rounded corner background on text
+============
+
+This sample shows how to draw a **rounded** corner background on text. It supports the following cases:
+
+* Set the background on **one line** text
+
+<img src="../screenshots/single.png" width="30%" />
+
+* Set the background on text over **two or multiple lines**
+
+<img src="../screenshots/multi.png" width="30%" />
+
+* Set the background on **right-to-left** text
+
+<img src="../screenshots/rtl.png" width="30%" />
+
+
+Implementation
+---------------
+Depending on the position of the text, we need to draw four different drawables as text backgrounds:
+
+* Text fits on one line: we only need one drawable
+* Text fits on 2 lines: we need drawables for the start and end of the text
+* Text spans multiple lines: we need drawables for the start, middle and end of the text
+
+<img src="../screenshots/lines.png" width="30%" />
+
+The four drawables that need to be drawn depending on the position of the text:
+
+To position the background, we need to:
+* Determine whether the text spans multiple lines
+* Find the start and end lines
+* Find the start and end offset depending on the paragraph direction
+
+All of these can be computed based on the text Layout. To render the background behind the text we need access to the Canvas. A custom TextView has access to all of the information necessary to position the drawables and render them.
+
+Our solution involves splitting our problem into 4 parts and creating classes dealing with them individually:
+* **Marking the position of the background** is done in the XML resources via Annotation spans and then, in the code, we read them in the `TextRoundedBgHelper`
+* Providing the background **drawables as attributes** of the TextView - implemented in `TextRoundedBgAttributeReader`
+* **Rendering the drawables** depending on whether the text runs across **one or multiple lines** - `TextRoundedBgHelper` interface and its implementations: `SingleLineRenderer` and `MultiLineRenderer`
+* Supporting **custom drawing** on a TextView - `RoundedBgTextView`, a class that extends `AppCompatTextView`, reads the attributes with the help of `TextRoundedBgAttributeReader`, overrides `onDraw` where it uses `TextRoundedBgHelper` to draw the background.
+
+Getting Started
+---------------
+
+Clone this repository, enter the top level directory and run `./gradlew tasks`
+to get an overview of all the tasks available for this project.
+
+Some important tasks are:
+
+```
+assembleDebug - Assembles all Debug builds.
+installDebug - Installs the Debug build.
+connectedAndroidTest - Installs and runs the tests for Debug build on connected
+devices.
+test - Run all unit tests.
+```
+
+Screenshots
+-----------
+<img src="../screenshots/rounded_bg.png" width="30%" />
+
+Support
+-------
+- Stack Overflow: http://stackoverflow.com/questions/tagged/android-text
+
+If you've found an error in this sample, please file an issue:
+https://github.com/googlesamples/android-text/issues
+
+Patches are encouraged, and may be submitted by forking this project and
+submitting a pull request through GitHub.
+
+License
+--------
+```
+Copyright 2018 The Android Open Source Project
+
+Licensed to the Apache Software Foundation (ASF) under one or more contributor
+license agreements. See the NOTICE file distributed with this work for
+additional information regarding copyright ownership. The ASF licenses this
+file to you under the Apache License, Version 2.0 (the "License"); you may not
+use this file except in compliance with the License. You may obtain a copy of
+the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations under
+the License.
+```
diff --git a/ui/text/RoundedBackground-Kotlin/app/build.gradle b/ui/text/RoundedBackground-Kotlin/app/build.gradle
new file mode 100644
index 00000000..0b78ce1c
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/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.
+ */
+apply plugin: 'com.android.application'
+
+apply plugin: 'kotlin-android'
+
+
+android {
+ compileSdkVersion rootProject.compileSdkVersion
+
+ defaultConfig {
+ applicationId "com.android.example.text.styling.roundedbg"
+ minSdkVersion rootProject.minSdkVersion
+ targetSdkVersion rootProject.targetSdkVersion
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ implementation project(':lib')
+ implementation "androidx.appcompat:appcompat:$androidxVersion"
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+} \ No newline at end of file
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/AndroidManifest.xml b/ui/text/RoundedBackground-Kotlin/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..d7429d4b
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.example.text.styling.roundedbg.app">
+
+ <application
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.RoundedBackground">
+ <activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest> \ No newline at end of file
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/java/com/android/example/text/styling/roundedbg/app/MainActivity.kt b/ui/text/RoundedBackground-Kotlin/app/src/main/java/com/android/example/text/styling/roundedbg/app/MainActivity.kt
new file mode 100644
index 00000000..53ae397c
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/java/com/android/example/text/styling/roundedbg/app/MainActivity.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package com.android.example.text.styling.roundedbg.app
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+
+/**
+ * Sample activity that uses [com.android.example.text.styling.roundedbg.RoundedBgTextView].
+ */
+class MainActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+ }
+}
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/ui/text/RoundedBackground-Kotlin/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000..888da70e
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,50 @@
+<?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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportHeight="108"
+ android:viewportWidth="108">
+ <path
+ android:fillType="evenOdd"
+ android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
+ android:strokeColor="#00000000"
+ android:strokeWidth="1">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="78.5885"
+ android:endY="90.9159"
+ android:startX="48.7653"
+ android:startY="61.0927"
+ android:type="linear">
+ <item
+ android:color="#44000000"
+ android:offset="0.0" />
+ <item
+ android:color="#00000000"
+ android:offset="1.0" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:fillColor="#FFFFFF"
+ android:fillType="nonZero"
+ android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
+ android:strokeColor="#00000000"
+ android:strokeWidth="1" />
+</vector>
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/res/drawable/ic_launcher_background.xml b/ui/text/RoundedBackground-Kotlin/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..a5e49139
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,185 @@
+<?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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportHeight="108"
+ android:viewportWidth="108">
+ <path
+ android:fillColor="#26A69A"
+ android:pathData="M0,0h108v108h-108z" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M9,0L9,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,0L19,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,0L29,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,0L39,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,0L49,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,0L59,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,0L69,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,0L79,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M89,0L89,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M99,0L99,108"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,9L108,9"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,19L108,19"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,29L108,29"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,39L108,39"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,49L108,49"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,59L108,59"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,69L108,69"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,79L108,79"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,89L108,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,99L108,99"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,29L89,29"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,39L89,39"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,49L89,49"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,59L89,59"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,69L89,69"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,79L89,79"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,19L29,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,19L39,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,19L49,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,19L59,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,19L69,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,19L79,89"
+ android:strokeColor="#33FFFFFF"
+ android:strokeWidth="0.8" />
+</vector>
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/res/drawable/rounded.xml b/ui/text/RoundedBackground-Kotlin/app/src/main/res/drawable/rounded.xml
new file mode 100644
index 00000000..f69e6bf9
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/res/drawable/rounded.xml
@@ -0,0 +1,21 @@
+<?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.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/roundedBg"/>
+ <stroke android:width="@dimen/roundedBorderWidth" android:color="@color/roundedBorder"/>
+ <corners android:radius="@dimen/roundedBorderRadius"/>
+</shape> \ No newline at end of file
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/res/drawable/rounded_left.xml b/ui/text/RoundedBackground-Kotlin/app/src/main/res/drawable/rounded_left.xml
new file mode 100644
index 00000000..6883c63e
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/res/drawable/rounded_left.xml
@@ -0,0 +1,22 @@
+<?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.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/roundedBg"/>
+ <stroke android:width="@dimen/roundedBorderWidth" android:color="@color/roundedBorder"/>
+ <corners android:topLeftRadius="@dimen/roundedTextBorderRadius"
+ android:bottomLeftRadius="@dimen/roundedTextBorderRadius"/>
+</shape> \ No newline at end of file
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/res/drawable/rounded_mid.xml b/ui/text/RoundedBackground-Kotlin/app/src/main/res/drawable/rounded_mid.xml
new file mode 100644
index 00000000..b1714ecd
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/res/drawable/rounded_mid.xml
@@ -0,0 +1,20 @@
+<?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.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/roundedBg"/>
+ <stroke android:width="@dimen/roundedBorderWidth" android:color="@color/roundedBorder"/>
+</shape> \ No newline at end of file
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/res/drawable/rounded_right.xml b/ui/text/RoundedBackground-Kotlin/app/src/main/res/drawable/rounded_right.xml
new file mode 100644
index 00000000..8066d972
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/res/drawable/rounded_right.xml
@@ -0,0 +1,22 @@
+<?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.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/roundedBg"/>
+ <stroke android:width="@dimen/roundedBorderWidth" android:color="@color/roundedBorder"/>
+ <corners android:topRightRadius="@dimen/roundedBorderRadius"
+ android:bottomRightRadius="@dimen/roundedBorderRadius"/>
+</shape> \ No newline at end of file
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/res/layout/activity_main.xml b/ui/text/RoundedBackground-Kotlin/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..b3738cb3
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,131 @@
+<?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.
+ -->
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto"
+ tools:context=".MainActivity">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <com.android.example.text.styling.roundedbg.RoundedBgTextView
+ android:text="@string/ltr"
+ style="@style/Widget.RoundedBackground.SampleTextView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/textViewMargin"/>
+
+ <View style="@style/Widget.RoundedBackground.Divider"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/dividerHeight"/>
+
+ <com.android.example.text.styling.roundedbg.RoundedBgTextView
+ android:text="@string/ltr_multi"
+ style="@style/Widget.RoundedBackground.SampleTextView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/textViewMargin"/>
+
+ <View style="@style/Widget.RoundedBackground.Divider"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/dividerHeight"/>
+
+ <com.android.example.text.styling.roundedbg.RoundedBgTextView
+ android:text="@string/ltr_long"
+ style="@style/Widget.RoundedBackground.SampleTextView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/textViewMargin"
+ app:roundedTextDrawable="@drawable/rounded"
+ app:roundedTextDrawableLeft="@drawable/rounded_left"
+ app:roundedTextDrawableMid="@drawable/rounded_mid"
+ app:roundedTextDrawableRight="@drawable/rounded_right"/>
+
+ <View style="@style/Widget.RoundedBackground.Divider"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/dividerHeight"/>
+
+ <com.android.example.text.styling.roundedbg.RoundedBgTextView
+ android:text="@string/rtl"
+ style="@style/Widget.RoundedBackground.SampleTextView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/textViewMargin"/>
+
+ <View style="@style/Widget.RoundedBackground.Divider"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/dividerHeight"/>
+
+ <com.android.example.text.styling.roundedbg.RoundedBgTextView
+ android:text="@string/rtl_multi"
+ style="@style/Widget.RoundedBackground.SampleTextView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/textViewMargin"/>
+
+ <View style="@style/Widget.RoundedBackground.Divider"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/dividerHeight"/>
+
+ <com.android.example.text.styling.roundedbg.RoundedBgTextView
+ android:text="@string/lang1"
+ style="@style/Widget.RoundedBackground.SampleTextView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/textViewMargin"/>
+
+ <View style="@style/Widget.RoundedBackground.Divider"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/dividerHeight"/>
+
+ <com.android.example.text.styling.roundedbg.RoundedBgTextView
+ android:text="@string/lang2"
+ style="@style/Widget.RoundedBackground.SampleTextView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/textViewMargin"/>
+
+ <View style="@style/Widget.RoundedBackground.Divider"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/dividerHeight"/>
+
+ <com.android.example.text.styling.roundedbg.RoundedBgTextView
+ android:text="@string/lang3"
+ style="@style/Widget.RoundedBackground.SampleTextView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/textViewMargin"/>
+
+ <View style="@style/Widget.RoundedBackground.Divider"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/dividerHeight"/>
+
+ <com.android.example.text.styling.roundedbg.RoundedBgTextView
+ android:text="@string/lang4"
+ style="@style/Widget.RoundedBackground.SampleTextView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/textViewMargin"/>
+
+ </LinearLayout>
+
+
+</ScrollView>
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..d3441ca6
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,20 @@
+<?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.
+ -->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon> \ No newline at end of file
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..d3441ca6
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,20 @@
+<?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.
+ -->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon> \ No newline at end of file
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-hdpi/ic_launcher.png b/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..a2f59082
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 00000000..1b523998
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-mdpi/ic_launcher.png b/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..ff10afd6
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 00000000..115a4c76
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..dcd3cd80
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..459ca609
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..8ca12fe0
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..8e19b410
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..b824ebdd
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..4c19a13c
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/res/values/colors.xml b/ui/text/RoundedBackground-Kotlin/app/src/main/res/values/colors.xml
new file mode 100644
index 00000000..932c9e93
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/res/values/colors.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>
+ <color name="colorPrimary">#DDD</color>
+ <color name="colorPrimaryDark">#CCC</color>
+ <color name="colorAccent">#FF7F50</color>
+
+ <color name="divider">#CCC</color>
+
+ <color name="roundedBg">#FFF176</color>
+ <color name="roundedBorder">#CABF45</color>
+</resources>
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/res/values/dimens.xml b/ui/text/RoundedBackground-Kotlin/app/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..02814ded
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/res/values/dimens.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2018 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <dimen name="textViewMargin">8dp</dimen>
+ <dimen name="dividerHeight">1dp</dimen>
+
+ <dimen name="roundedBorderRadius">6dp</dimen>
+ <dimen name="roundedBorderWidth">2dp</dimen>
+</resources>
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/res/values/strings.xml b/ui/text/RoundedBackground-Kotlin/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..35cc087d
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/res/values/strings.xml
@@ -0,0 +1,32 @@
+<?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>
+ <string name="app_name">Rounded Multiline Bg</string>
+
+ <!-- Without the quotes at the begining and end Android strips the whitespace and also starts
+ the annotation at the wrong position. -->
+ <string name="ltr">"this is <annotation key="rounded">a regular</annotation> paragraph."</string>
+ <string name="ltr_multi">"this contains <annotation key="rounded">a\nline break</annotation> and continues."</string>
+ <string name="ltr_long">"this is <annotation key="rounded">a paragraph \nthat would cover more than\ntwo</annotation> lines."</string>
+ <string name="rtl"> هذا هو "<annotation key="rounded">الحق في</annotation>" الفقرة اليسرى </string>
+ <string name="rtl_multi"> هذا هو"<annotation key="rounded">حق\n لفقرة</annotation>" اليسار </string>
+ <string name="lang1">"ဟ <annotation key="rounded">ယ်လို\nဟယ်လို</annotation> ယ်လို"</string>
+ <string name="lang2">"អ <annotation key="rounded">៊ីតាលី\nអ៊ីតា</annotation> លី""</string>
+ <string name="lang3">"नमस्ते <annotation key="rounded">दुनिया\nनमस्ते</annotation> दुनिया"</string>
+ <string name="lang4">"สวัสดี <annotation key="rounded">ชาวโลก\nสวัสดีชาวโ</annotation> ลก"</string>
+
+</resources> \ No newline at end of file
diff --git a/ui/text/RoundedBackground-Kotlin/app/src/main/res/values/styles.xml b/ui/text/RoundedBackground-Kotlin/app/src/main/res/values/styles.xml
new file mode 100644
index 00000000..1997dad9
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/app/src/main/res/values/styles.xml
@@ -0,0 +1,42 @@
+<?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>
+
+ <!-- Base application theme. -->
+ <style name="Theme.RoundedBackground" parent="Theme.AppCompat.Light">
+ <item name="colorPrimary">@color/colorPrimary</item>
+ <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+ <item name="colorAccent">@color/colorAccent</item>
+ </style>
+
+ <style name="Widget.RoundedBackground" parent="android:Widget"/>
+
+ <style name="Widget.RoundedBackground.SampleTextView">
+ <item name="android:lineSpacingExtra">2dp</item>
+ <item name="android:lineSpacingMultiplier">1.5</item>
+ <item name="android:padding">4dp</item>
+ <item name="android:textSize">18sp</item>
+ <item name="android:includeFontPadding">true</item>
+ <item name="roundedTextHorizontalPadding">2dp</item>
+ <item name="roundedTextVerticalPadding">2dp</item>
+ </style>
+
+ <style name="Widget.RoundedBackground.Divider">
+ <item name="android:background">#CCC</item>
+ <item name="android:padding">2dp</item>
+ </style>
+</resources>
diff --git a/ui/text/RoundedBackground-Kotlin/build.gradle b/ui/text/RoundedBackground-Kotlin/build.gradle
new file mode 100644
index 00000000..f6df7566
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/build.gradle
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ ext.kotlin_version = '1.2.51'
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.3.0-alpha03'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
+
+ext {
+ // Sdk and tools
+ minSdkVersion = 15
+ targetSdkVersion = 28
+ compileSdkVersion = 28
+ // App dependencies
+ androidxVersion = '1.0.0-beta01'
+ ktxVersion = '1.0.0-alpha1'
+} \ No newline at end of file
diff --git a/ui/text/RoundedBackground-Kotlin/gradle.properties b/ui/text/RoundedBackground-Kotlin/gradle.properties
new file mode 100644
index 00000000..743d692c
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/gradle.properties
@@ -0,0 +1,13 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/ui/text/RoundedBackground-Kotlin/gradle/wrapper/gradle-wrapper.jar b/ui/text/RoundedBackground-Kotlin/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..7a3265ee
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/ui/text/RoundedBackground-Kotlin/gradle/wrapper/gradle-wrapper.properties b/ui/text/RoundedBackground-Kotlin/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..37fcd35b
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Jul 11 10:47:22 PDT 2018
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-bin.zip
diff --git a/ui/text/RoundedBackground-Kotlin/gradlew b/ui/text/RoundedBackground-Kotlin/gradlew
new file mode 100755
index 00000000..cccdd3d5
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/ui/text/RoundedBackground-Kotlin/gradlew.bat b/ui/text/RoundedBackground-Kotlin/gradlew.bat
new file mode 100644
index 00000000..e95643d6
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/ui/text/RoundedBackground-Kotlin/lib/.gitignore b/ui/text/RoundedBackground-Kotlin/lib/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/lib/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/ui/text/RoundedBackground-Kotlin/lib/build.gradle b/ui/text/RoundedBackground-Kotlin/lib/build.gradle
new file mode 100644
index 00000000..6c499867
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/lib/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.
+ */
+apply plugin: 'com.android.library'
+
+apply plugin: 'kotlin-android'
+
+android {
+ compileSdkVersion rootProject.compileSdkVersion
+
+ defaultConfig {
+ minSdkVersion rootProject.minSdkVersion
+ targetSdkVersion rootProject.targetSdkVersion
+ versionCode 1
+ versionName "1.0"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+}
+
+dependencies {
+ implementation "androidx.core:core-ktx:${ktxVersion}"
+ implementation "androidx.appcompat:appcompat:$androidxVersion"
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+} \ No newline at end of file
diff --git a/ui/text/RoundedBackground-Kotlin/lib/src/main/AndroidManifest.xml b/ui/text/RoundedBackground-Kotlin/lib/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..43a5eb06
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/lib/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?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="com.android.example.text.styling.roundedbg"/>
diff --git a/ui/text/RoundedBackground-Kotlin/lib/src/main/java/com/android/example/text/styling/roundedbg/LayoutExtensions.kt b/ui/text/RoundedBackground-Kotlin/lib/src/main/java/com/android/example/text/styling/roundedbg/LayoutExtensions.kt
new file mode 100644
index 00000000..f7478eb4
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/lib/src/main/java/com/android/example/text/styling/roundedbg/LayoutExtensions.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.example.text.styling.roundedbg
+
+import android.os.Build
+import android.text.Layout
+
+// Extension functions for Layout object
+
+/**
+ * Android system default line spacing extra
+ */
+private const val DEFAULT_LINESPACING_EXTRA = 0f
+
+/**
+ * Android system default line spacing multiplier
+ */
+private const val DEFAULT_LINESPACING_MULTIPLIER = 1f
+
+/**
+ * Get the line bottom discarding the line spacing added.
+ */
+fun Layout.getLineBottomWithoutSpacing(line: Int): Int {
+ val lineBottom = getLineBottom(line)
+ val lastLineSpacingNotAdded = Build.VERSION.SDK_INT >= 19
+ val isLastLine = line == lineCount - 1
+
+ val lineBottomWithoutSpacing: Int
+ val lineSpacingExtra = spacingAdd
+ val lineSpacingMultiplier = spacingMultiplier
+ val hasLineSpacing = lineSpacingExtra != DEFAULT_LINESPACING_EXTRA
+ || lineSpacingMultiplier != DEFAULT_LINESPACING_MULTIPLIER
+
+ if (!hasLineSpacing || isLastLine && lastLineSpacingNotAdded) {
+ lineBottomWithoutSpacing = lineBottom
+ } else {
+ val extra: Float
+ if (lineSpacingMultiplier.compareTo(DEFAULT_LINESPACING_MULTIPLIER) != 0) {
+ val lineHeight = getLineHeight(line)
+ extra = lineHeight - (lineHeight - lineSpacingExtra) / lineSpacingMultiplier
+ } else {
+ extra = lineSpacingExtra
+ }
+
+ lineBottomWithoutSpacing = (lineBottom - extra).toInt()
+ }
+
+ return lineBottomWithoutSpacing
+}
+
+/**
+ * Get the line height of a line.
+ */
+fun Layout.getLineHeight(line: Int): Int {
+ return getLineTop(line + 1) - getLineTop(line)
+}
+
+/**
+ * Returns the top of the Layout after removing the extra padding applied by the Layout.
+ */
+fun Layout.getLineTopWithoutPadding(line: Int): Int {
+ var lineTop = getLineTop(line)
+ if (line == 0) {
+ lineTop -= topPadding
+ }
+ return lineTop
+}
+
+/**
+ * Returns the bottom of the Layout after removing the extra padding applied by the Layout.
+ */
+fun Layout.getLineBottomWithoutPadding(line: Int): Int {
+ var lineBottom = getLineBottomWithoutSpacing(line)
+ if (line == lineCount - 1) {
+ lineBottom -= bottomPadding
+ }
+ return lineBottom
+} \ No newline at end of file
diff --git a/ui/text/RoundedBackground-Kotlin/lib/src/main/java/com/android/example/text/styling/roundedbg/RoundedBgTextView.kt b/ui/text/RoundedBackground-Kotlin/lib/src/main/java/com/android/example/text/styling/roundedbg/RoundedBgTextView.kt
new file mode 100644
index 00000000..9f0d2881
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/lib/src/main/java/com/android/example/text/styling/roundedbg/RoundedBgTextView.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 com.android.example.text.styling.roundedbg
+
+import android.content.Context
+import android.graphics.Canvas
+import android.text.Spanned
+import android.util.AttributeSet
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.core.graphics.withTranslation
+
+/**
+ * A TextView that can draw rounded background to the portions of the text. See
+ * [TextRoundedBgHelper] for more information.
+ *
+ * See [TextRoundedBgAttributeReader] for supported attributes.
+ */
+class RoundedBgTextView : AppCompatTextView {
+
+ private val textRoundedBgHelper: TextRoundedBgHelper
+
+ @JvmOverloads
+ constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = android.R.attr.textViewStyle
+ ) : super(context, attrs, defStyleAttr) {
+ val attributeReader = TextRoundedBgAttributeReader(context, attrs)
+ textRoundedBgHelper = TextRoundedBgHelper(
+ horizontalPadding = attributeReader.horizontalPadding,
+ verticalPadding = attributeReader.verticalPadding,
+ drawable = attributeReader.drawable,
+ drawableLeft = attributeReader.drawableLeft,
+ drawableMid = attributeReader.drawableMid,
+ drawableRight = attributeReader.drawableRight
+ )
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ // need to draw bg first so that text can be on top during super.onDraw()
+ if (text is Spanned && layout != null) {
+ canvas.withTranslation(totalPaddingLeft.toFloat(), totalPaddingTop.toFloat()) {
+ textRoundedBgHelper.draw(canvas, text as Spanned, layout)
+ }
+ }
+ super.onDraw(canvas)
+ }
+} \ No newline at end of file
diff --git a/ui/text/RoundedBackground-Kotlin/lib/src/main/java/com/android/example/text/styling/roundedbg/TextRoundedBgAttributeReader.kt b/ui/text/RoundedBackground-Kotlin/lib/src/main/java/com/android/example/text/styling/roundedbg/TextRoundedBgAttributeReader.kt
new file mode 100644
index 00000000..5ecc41d9
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/lib/src/main/java/com/android/example/text/styling/roundedbg/TextRoundedBgAttributeReader.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 com.android.example.text.styling.roundedbg
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import androidx.core.content.res.getDrawableOrThrow
+
+/**
+ * Reads default attributes that [TextRoundedBgHelper] needs from resources. The attributes read
+ * are:
+ *
+ * - chHorizontalPadding: the padding to be applied to left & right of the background
+ * - chVerticalPadding: the padding to be applied to top & bottom of the background
+ * - chDrawable: the drawable used to draw the background
+ * - chDrawableLeft: the drawable used to draw left edge of the background
+ * - chDrawableMid: the drawable used to draw for whole line
+ * - chDrawableRight: the drawable used to draw right edge of the background
+ */
+class TextRoundedBgAttributeReader(context: Context, attrs: AttributeSet?) {
+
+ val horizontalPadding: Int
+ val verticalPadding: Int
+ val drawable: Drawable
+ val drawableLeft: Drawable
+ val drawableMid: Drawable
+ val drawableRight: Drawable
+
+ init {
+ val typedArray = context.obtainStyledAttributes(
+ attrs,
+ R.styleable.TextRoundedBgHelper,
+ 0,
+ R.style.RoundedBgTextView
+ )
+ horizontalPadding = typedArray.getDimensionPixelSize(
+ R.styleable.TextRoundedBgHelper_roundedTextHorizontalPadding,
+ 0
+ )
+ verticalPadding = typedArray.getDimensionPixelSize(
+ R.styleable.TextRoundedBgHelper_roundedTextVerticalPadding,
+ 0
+ )
+ drawable = typedArray.getDrawableOrThrow(
+ R.styleable.TextRoundedBgHelper_roundedTextDrawable
+ )
+ drawableLeft = typedArray.getDrawableOrThrow(
+ R.styleable.TextRoundedBgHelper_roundedTextDrawableLeft
+ )
+ drawableMid = typedArray.getDrawableOrThrow(
+ R.styleable.TextRoundedBgHelper_roundedTextDrawableMid
+ )
+ drawableRight = typedArray.getDrawableOrThrow(
+ R.styleable.TextRoundedBgHelper_roundedTextDrawableRight
+ )
+ typedArray.recycle()
+ }
+}
diff --git a/ui/text/RoundedBackground-Kotlin/lib/src/main/java/com/android/example/text/styling/roundedbg/TextRoundedBgHelper.kt b/ui/text/RoundedBackground-Kotlin/lib/src/main/java/com/android/example/text/styling/roundedbg/TextRoundedBgHelper.kt
new file mode 100644
index 00000000..262a5fe9
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/lib/src/main/java/com/android/example/text/styling/roundedbg/TextRoundedBgHelper.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package com.android.example.text.styling.roundedbg
+
+import android.graphics.Canvas
+import android.graphics.drawable.Drawable
+import android.text.Annotation
+import android.text.Layout
+import android.text.Spanned
+
+/**
+ * Helper class to draw multi-line rounded background to certain parts of a text. The start/end
+ * positions of the backgrounds are annotated with [android.text.Annotation] class. Each annotation
+ * should have the annotation key set to **rounded**.
+ *
+ * i.e.:
+ * ```
+ * <!--without the quotes at the begining and end Android strips the whitespace and also starts
+ * the annotation at the wrong position-->
+ * <string name="ltr">"this is <annotation key="rounded">a regular</annotation> paragraph."</string>
+ * ```
+ *
+ * **Note:** BiDi text is not supported.
+ *
+ * @param horizontalPadding the padding to be applied to left & right of the background
+ * @param verticalPadding the padding to be applied to top & bottom of the background
+ * @param drawable the drawable used to draw the background
+ * @param drawableLeft the drawable used to draw left edge of the background
+ * @param drawableMid the drawable used to draw for whole line
+ * @param drawableRight the drawable used to draw right edge of the background
+ */
+class TextRoundedBgHelper(
+ val horizontalPadding: Int,
+ verticalPadding: Int,
+ drawable: Drawable,
+ drawableLeft: Drawable,
+ drawableMid: Drawable,
+ drawableRight: Drawable
+) {
+
+ private val singleLineRenderer: TextRoundedBgRenderer by lazy {
+ SingleLineRenderer(
+ horizontalPadding = horizontalPadding,
+ verticalPadding = verticalPadding,
+ drawable = drawable
+ )
+ }
+
+ private val multiLineRenderer: TextRoundedBgRenderer by lazy {
+ MultiLineRenderer(
+ horizontalPadding = horizontalPadding,
+ verticalPadding = verticalPadding,
+ drawableLeft = drawableLeft,
+ drawableMid = drawableMid,
+ drawableRight = drawableRight
+ )
+ }
+
+ /**
+ * Call this function during onDraw of another widget such as TextView.
+ *
+ * @param canvas Canvas to draw onto
+ * @param text
+ * @param layout Layout that contains the text
+ */
+ fun draw(canvas: Canvas, text: Spanned, layout: Layout) {
+ // ideally the calculations here should be cached since they are not cheap. However, proper
+ // invalidation of the cache is required whenever anything related to text has changed.
+ val spans = text.getSpans(0, text.length, Annotation::class.java)
+ spans.forEach { span ->
+ if (span.value.equals("rounded")) {
+ val spanStart = text.getSpanStart(span)
+ val spanEnd = text.getSpanEnd(span)
+ val startLine = layout.getLineForOffset(spanStart)
+ val endLine = layout.getLineForOffset(spanEnd)
+
+ // start can be on the left or on the right depending on the language direction.
+ val startOffset = (layout.getPrimaryHorizontal(spanStart)
+ + -1 * layout.getParagraphDirection(startLine) * horizontalPadding).toInt()
+ // end can be on the left or on the right depending on the language direction.
+ val endOffset = (layout.getPrimaryHorizontal(spanEnd)
+ + layout.getParagraphDirection(endLine) * horizontalPadding).toInt()
+
+ val renderer = if (startLine == endLine) singleLineRenderer else multiLineRenderer
+ renderer.draw(canvas, layout, startLine, endLine, startOffset, endOffset)
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/ui/text/RoundedBackground-Kotlin/lib/src/main/java/com/android/example/text/styling/roundedbg/TextRoundedBgRenderer.kt b/ui/text/RoundedBackground-Kotlin/lib/src/main/java/com/android/example/text/styling/roundedbg/TextRoundedBgRenderer.kt
new file mode 100644
index 00000000..37d814a7
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/lib/src/main/java/com/android/example/text/styling/roundedbg/TextRoundedBgRenderer.kt
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+package com.android.example.text.styling.roundedbg
+
+import android.graphics.Canvas
+import android.graphics.drawable.Drawable
+import android.text.Layout
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * Base class for single and multi line rounded background renderers.
+ *
+ * @param horizontalPadding the padding to be applied to left & right of the background
+ * @param verticalPadding the padding to be applied to top & bottom of the background
+ */
+internal abstract class TextRoundedBgRenderer(
+ val horizontalPadding: Int,
+ val verticalPadding: Int
+) {
+
+ /**
+ * Draw the background that starts at the {@code startOffset} and ends at {@code endOffset}.
+ *
+ * @param canvas Canvas to draw onto
+ * @param layout Layout that contains the text
+ * @param startLine the start line for the background
+ * @param endLine the end line for the background
+ * @param startOffset the character offset that the background should start at
+ * @param endOffset the character offset that the background should end at
+ */
+ abstract fun draw(
+ canvas: Canvas,
+ layout: Layout,
+ startLine: Int,
+ endLine: Int,
+ startOffset: Int,
+ endOffset: Int
+ )
+
+ /**
+ * Get the top offset of the line and add padding into account so that there is a gap between
+ * top of the background and top of the text.
+ *
+ * @param layout Layout object that contains the text
+ * @param line line number
+ */
+ protected fun getLineTop(layout: Layout, line: Int): Int {
+ return layout.getLineTopWithoutPadding(line) - verticalPadding
+ }
+
+ /**
+ * Get the bottom offset of the line and add padding into account so that there is a gap between
+ * bottom of the background and bottom of the text.
+ *
+ * @param layout Layout object that contains the text
+ * @param line line number
+ */
+ protected fun getLineBottom(layout: Layout, line: Int): Int {
+ return layout.getLineBottomWithoutPadding(line) + verticalPadding
+ }
+}
+
+/**
+ * Draws the background for text that starts and ends on the same line.
+ *
+ * @param horizontalPadding the padding to be applied to left & right of the background
+ * @param verticalPadding the padding to be applied to top & bottom of the background
+ * @param drawable the drawable used to draw the background
+ */
+internal class SingleLineRenderer(
+ horizontalPadding: Int,
+ verticalPadding: Int,
+ val drawable: Drawable
+) : TextRoundedBgRenderer(horizontalPadding, verticalPadding) {
+
+ override fun draw(
+ canvas: Canvas,
+ layout: Layout,
+ startLine: Int,
+ endLine: Int,
+ startOffset: Int,
+ endOffset: Int
+ ) {
+ val lineTop = getLineTop(layout, startLine)
+ val lineBottom = getLineBottom(layout, startLine)
+ // get min of start/end for left, and max of start/end for right since we don't
+ // the language direction
+ val left = min(startOffset, endOffset)
+ val right = max(startOffset, endOffset)
+ drawable.setBounds(left, lineTop, right, lineBottom)
+ drawable.draw(canvas)
+ }
+}
+
+/**
+ * Draws the background for text that starts and ends on different lines.
+ *
+ * @param horizontalPadding the padding to be applied to left & right of the background
+ * @param verticalPadding the padding to be applied to top & bottom of the background
+ * @param drawableLeft the drawable used to draw left edge of the background
+ * @param drawableMid the drawable used to draw for whole line
+ * @param drawableRight the drawable used to draw right edge of the background
+ */
+internal class MultiLineRenderer(
+ horizontalPadding: Int,
+ verticalPadding: Int,
+ val drawableLeft: Drawable,
+ val drawableMid: Drawable,
+ val drawableRight: Drawable
+) : TextRoundedBgRenderer(horizontalPadding, verticalPadding) {
+
+ override fun draw(
+ canvas: Canvas,
+ layout: Layout,
+ startLine: Int,
+ endLine: Int,
+ startOffset: Int,
+ endOffset: Int
+ ) {
+ // draw the first line
+ val paragDir = layout.getParagraphDirection(startLine)
+ val lineEndOffset = if (paragDir == Layout.DIR_RIGHT_TO_LEFT) {
+ layout.getLineLeft(startLine) - horizontalPadding
+ } else {
+ layout.getLineRight(startLine) + horizontalPadding
+ }.toInt()
+
+ var lineBottom = getLineBottom(layout, startLine)
+ var lineTop = getLineTop(layout, startLine)
+ drawStart(canvas, startOffset, lineTop, lineEndOffset, lineBottom)
+
+ // for the lines in the middle draw the mid drawable
+ for (line in startLine + 1 until endLine) {
+ lineTop = getLineTop(layout, line)
+ lineBottom = getLineBottom(layout, line)
+ drawableMid.setBounds(
+ (layout.getLineLeft(line).toInt() - horizontalPadding),
+ lineTop,
+ (layout.getLineRight(line).toInt() + horizontalPadding),
+ lineBottom
+ )
+ drawableMid.draw(canvas)
+ }
+
+ val lineStartOffset = if (paragDir == Layout.DIR_RIGHT_TO_LEFT) {
+ layout.getLineRight(startLine) + horizontalPadding
+ } else {
+ layout.getLineLeft(startLine) - horizontalPadding
+ }.toInt()
+
+ // draw the last line
+ lineBottom = getLineBottom(layout, endLine)
+ lineTop = getLineTop(layout, endLine)
+
+ drawEnd(canvas, lineStartOffset, lineTop, endOffset, lineBottom)
+ }
+
+ /**
+ * Draw the first line of a multiline annotation. Handles LTR/RTL.
+ *
+ * @param canvas Canvas to draw onto
+ * @param start start coordinate for the background
+ * @param top top coordinate for the background
+ * @param end end coordinate for the background
+ * @param bottom bottom coordinate for the background
+ */
+ private fun drawStart(canvas: Canvas, start: Int, top: Int, end: Int, bottom: Int) {
+ if (start > end) {
+ drawableRight.setBounds(end, top, start, bottom)
+ drawableRight.draw(canvas)
+ } else {
+ drawableLeft.setBounds(start, top, end, bottom)
+ drawableLeft.draw(canvas)
+ }
+ }
+
+ /**
+ * Draw the last line of a multiline annotation. Handles LTR/RTL.
+ *
+ * @param canvas Canvas to draw onto
+ * @param start start coordinate for the background
+ * @param top top position for the background
+ * @param end end coordinate for the background
+ * @param bottom bottom coordinate for the background
+ */
+ private fun drawEnd(canvas: Canvas, start: Int, top: Int, end: Int, bottom: Int) {
+ if (start > end) {
+ drawableLeft.setBounds(end, top, start, bottom)
+ drawableLeft.draw(canvas)
+ } else {
+ drawableRight.setBounds(start, top, end, bottom)
+ drawableRight.draw(canvas)
+ }
+ }
+} \ No newline at end of file
diff --git a/ui/text/RoundedBackground-Kotlin/lib/src/main/res/drawable/rounded_text_bg.xml b/ui/text/RoundedBackground-Kotlin/lib/src/main/res/drawable/rounded_text_bg.xml
new file mode 100644
index 00000000..0683b1b9
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/lib/src/main/res/drawable/rounded_text_bg.xml
@@ -0,0 +1,21 @@
+<?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.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/roundedTextBg"/>
+ <stroke android:width="@dimen/roundedTextBorderWidth" android:color="@color/roundedTextBorder"/>
+ <corners android:radius="@dimen/roundedTextBorderRadius"/>
+</shape> \ No newline at end of file
diff --git a/ui/text/RoundedBackground-Kotlin/lib/src/main/res/drawable/rounded_text_bg_left.xml b/ui/text/RoundedBackground-Kotlin/lib/src/main/res/drawable/rounded_text_bg_left.xml
new file mode 100644
index 00000000..5d02c93d
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/lib/src/main/res/drawable/rounded_text_bg_left.xml
@@ -0,0 +1,22 @@
+<?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.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/roundedTextBg"/>
+ <stroke android:width="@dimen/roundedTextBorderWidth" android:color="@color/roundedTextBorder"/>
+ <corners android:topLeftRadius="@dimen/roundedTextBorderRadius"
+ android:bottomLeftRadius="@dimen/roundedTextBorderRadius"/>
+</shape> \ No newline at end of file
diff --git a/ui/text/RoundedBackground-Kotlin/lib/src/main/res/drawable/rounded_text_bg_mid.xml b/ui/text/RoundedBackground-Kotlin/lib/src/main/res/drawable/rounded_text_bg_mid.xml
new file mode 100644
index 00000000..e4c2c570
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/lib/src/main/res/drawable/rounded_text_bg_mid.xml
@@ -0,0 +1,20 @@
+<?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.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/roundedTextBg"/>
+ <stroke android:width="@dimen/roundedTextBorderWidth" android:color="@color/roundedTextBorder"/>
+</shape> \ No newline at end of file
diff --git a/ui/text/RoundedBackground-Kotlin/lib/src/main/res/drawable/rounded_text_bg_right.xml b/ui/text/RoundedBackground-Kotlin/lib/src/main/res/drawable/rounded_text_bg_right.xml
new file mode 100644
index 00000000..cf011c5c
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/lib/src/main/res/drawable/rounded_text_bg_right.xml
@@ -0,0 +1,22 @@
+<?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.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/roundedTextBg"/>
+ <stroke android:width="@dimen/roundedTextBorderWidth" android:color="@color/roundedTextBorder"/>
+ <corners android:topRightRadius="@dimen/roundedTextBorderRadius"
+ android:bottomRightRadius="@dimen/roundedTextBorderRadius"/>
+</shape> \ No newline at end of file
diff --git a/ui/text/RoundedBackground-Kotlin/lib/src/main/res/values/attrs.xml b/ui/text/RoundedBackground-Kotlin/lib/src/main/res/values/attrs.xml
new file mode 100644
index 00000000..737ae07c
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/lib/src/main/res/values/attrs.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>
+ <declare-styleable name="TextRoundedBgHelper">
+ <attr name="roundedTextHorizontalPadding" format="dimension"/>
+ <attr name="roundedTextVerticalPadding" format="dimension"/>
+ <attr name="roundedTextDrawable" format="reference"/>
+ <attr name="roundedTextDrawableLeft" format="reference"/>
+ <attr name="roundedTextDrawableMid" format="reference"/>
+ <attr name="roundedTextDrawableRight" format="reference"/>
+ </declare-styleable>
+</resources> \ No newline at end of file
diff --git a/ui/text/RoundedBackground-Kotlin/lib/src/main/res/values/colors.xml b/ui/text/RoundedBackground-Kotlin/lib/src/main/res/values/colors.xml
new file mode 100644
index 00000000..c26ac6af
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/lib/src/main/res/values/colors.xml
@@ -0,0 +1,20 @@
+<?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>
+ <color name="roundedTextBg">#554fC3f7</color>
+ <color name="roundedTextBorder">#DD0277BD</color>
+</resources>
diff --git a/ui/text/RoundedBackground-Kotlin/lib/src/main/res/values/dimens.xml b/ui/text/RoundedBackground-Kotlin/lib/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..dbc7f49e
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/lib/src/main/res/values/dimens.xml
@@ -0,0 +1,20 @@
+<?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>
+ <dimen name="roundedTextBorderRadius">4dp</dimen>
+ <dimen name="roundedTextBorderWidth">1dp</dimen>
+</resources> \ No newline at end of file
diff --git a/ui/text/RoundedBackground-Kotlin/lib/src/main/res/values/styles.xml b/ui/text/RoundedBackground-Kotlin/lib/src/main/res/values/styles.xml
new file mode 100644
index 00000000..9bffef3d
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/lib/src/main/res/values/styles.xml
@@ -0,0 +1,28 @@
+<?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>
+
+ <style name="RoundedBgTextView" parent="@android:style/Widget.TextView">
+ <item name="roundedTextHorizontalPadding">2dp</item>
+ <item name="roundedTextVerticalPadding">2dp</item>
+ <item name="roundedTextDrawable">@drawable/rounded_text_bg</item>
+ <item name="roundedTextDrawableLeft">@drawable/rounded_text_bg_left</item>
+ <item name="roundedTextDrawableMid">@drawable/rounded_text_bg_mid</item>
+ <item name="roundedTextDrawableRight">@drawable/rounded_text_bg_right</item>
+ </style>
+
+</resources>
diff --git a/ui/text/RoundedBackground-Kotlin/settings.gradle b/ui/text/RoundedBackground-Kotlin/settings.gradle
new file mode 100644
index 00000000..3cbe2493
--- /dev/null
+++ b/ui/text/RoundedBackground-Kotlin/settings.gradle
@@ -0,0 +1 @@
+include ':app', ':lib'
diff --git a/ui/text/screenshots/lines.png b/ui/text/screenshots/lines.png
new file mode 100644
index 00000000..dbc86d17
--- /dev/null
+++ b/ui/text/screenshots/lines.png
Binary files differ
diff --git a/ui/text/screenshots/multi.png b/ui/text/screenshots/multi.png
new file mode 100644
index 00000000..e37d1e14
--- /dev/null
+++ b/ui/text/screenshots/multi.png
Binary files differ
diff --git a/ui/text/screenshots/rounded_bg.png b/ui/text/screenshots/rounded_bg.png
new file mode 100644
index 00000000..1e9acddf
--- /dev/null
+++ b/ui/text/screenshots/rounded_bg.png
Binary files differ
diff --git a/ui/text/screenshots/rtl.png b/ui/text/screenshots/rtl.png
new file mode 100644
index 00000000..6d8aba31
--- /dev/null
+++ b/ui/text/screenshots/rtl.png
Binary files differ
diff --git a/ui/text/screenshots/single.png b/ui/text/screenshots/single.png
new file mode 100644
index 00000000..b264682e
--- /dev/null
+++ b/ui/text/screenshots/single.png
Binary files differ