aboutsummaryrefslogtreecommitdiff
path: root/notification
diff options
context:
space:
mode:
authorYuichi Araki <yaraki@google.com>2019-02-20 18:50:50 +0900
committerYuichi Araki <yaraki@google.com>2019-03-22 11:13:10 +0900
commita62b08a858ceee11dcdae7983049be3fac0947a9 (patch)
tree063f3b2466730fc76b5c810ed9030486d9c3c26a /notification
parent1256015719ee1d92a09388019947cb8d71eb7460 (diff)
downloadandroid-a62b08a858ceee11dcdae7983049be3fac0947a9.tar.gz
Bubbles: Add a new sample
Test: MainViewModelTest, ChatViewModelTest Change-Id: I9576b645d7723ea1ddc7661487f13e1c9d16691e
Diffstat (limited to 'notification')
-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-v24/ic_launcher_foreground.xml34
-rw-r--r--notification/Bubbles/app/src/main/res/drawable/ic_launcher_background.xml74
-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 -> 2963 bytes
-rw-r--r--notification/Bubbles/app/src/main/res/mipmap-hdpi/ic_launcher_round.pngbin0 -> 4905 bytes
-rw-r--r--notification/Bubbles/app/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 2060 bytes
-rw-r--r--notification/Bubbles/app/src/main/res/mipmap-mdpi/ic_launcher_round.pngbin0 -> 2783 bytes
-rw-r--r--notification/Bubbles/app/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 4490 bytes
-rw-r--r--notification/Bubbles/app/src/main/res/mipmap-xhdpi/ic_launcher_round.pngbin0 -> 6895 bytes
-rw-r--r--notification/Bubbles/app/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 6387 bytes
-rw-r--r--notification/Bubbles/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.pngbin0 -> 10413 bytes
-rw-r--r--notification/Bubbles/app/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 9128 bytes
-rw-r--r--notification/Bubbles/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.pngbin0 -> 15132 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/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
82 files changed, 3211 insertions, 0 deletions
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-v24/ic_launcher_foreground.xml b/notification/Bubbles/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000..6348baae
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+<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/notification/Bubbles/app/src/main/res/drawable/ic_launcher_background.xml b/notification/Bubbles/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..a0ad202f
--- /dev/null
+++ b/notification/Bubbles/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="108dp"
+ android:width="108dp"
+ android:viewportHeight="108"
+ android:viewportWidth="108">
+ <path android:fillColor="#008577"
+ 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/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..bbd3e021
--- /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="@drawable/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..bbd3e021
--- /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="@drawable/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..898f3ed5
--- /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..dffca360
--- /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..64ba76f7
--- /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..dae5e082
--- /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..e5ed4659
--- /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..14ed0af3
--- /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..b0907cac
--- /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..d8ae0315
--- /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..2c18de9e
--- /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..beed3cdd
--- /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/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>