diff options
author | Yuichi Araki <yaraki@google.com> | 2019-02-20 18:50:50 +0900 |
---|---|---|
committer | Yuichi Araki <yaraki@google.com> | 2019-03-22 11:13:10 +0900 |
commit | a62b08a858ceee11dcdae7983049be3fac0947a9 (patch) | |
tree | 063f3b2466730fc76b5c810ed9030486d9c3c26a /notification | |
parent | 1256015719ee1d92a09388019947cb8d71eb7460 (diff) | |
download | android-a62b08a858ceee11dcdae7983049be3fac0947a9.tar.gz |
Bubbles: Add a new sample
Test: MainViewModelTest, ChatViewModelTest
Change-Id: I9576b645d7723ea1ddc7661487f13e1c9d16691e
Diffstat (limited to 'notification')
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 Binary files differnew file mode 100644 index 00000000..61961708 --- /dev/null +++ b/notification/Bubbles/app/src/main/res/drawable-nodpi/cat.jpg diff --git a/notification/Bubbles/app/src/main/res/drawable-nodpi/dog.jpg b/notification/Bubbles/app/src/main/res/drawable-nodpi/dog.jpg Binary files differnew file mode 100644 index 00000000..df159007 --- /dev/null +++ b/notification/Bubbles/app/src/main/res/drawable-nodpi/dog.jpg diff --git a/notification/Bubbles/app/src/main/res/drawable-nodpi/parrot.jpg b/notification/Bubbles/app/src/main/res/drawable-nodpi/parrot.jpg Binary files differnew file mode 100644 index 00000000..8e2e18df --- /dev/null +++ b/notification/Bubbles/app/src/main/res/drawable-nodpi/parrot.jpg diff --git a/notification/Bubbles/app/src/main/res/drawable-nodpi/sheep.jpg b/notification/Bubbles/app/src/main/res/drawable-nodpi/sheep.jpg Binary files differnew file mode 100644 index 00000000..39876f9c --- /dev/null +++ b/notification/Bubbles/app/src/main/res/drawable-nodpi/sheep.jpg 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 Binary files differnew file mode 100644 index 00000000..ad5e8c15 --- /dev/null +++ b/notification/Bubbles/app/src/main/res/drawable-nodpi/sheep_full.jpg 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 Binary files differnew file mode 100644 index 00000000..898f3ed5 --- /dev/null +++ b/notification/Bubbles/app/src/main/res/mipmap-hdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 00000000..dffca360 --- /dev/null +++ b/notification/Bubbles/app/src/main/res/mipmap-hdpi/ic_launcher_round.png 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 Binary files differnew file mode 100644 index 00000000..64ba76f7 --- /dev/null +++ b/notification/Bubbles/app/src/main/res/mipmap-mdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 00000000..dae5e082 --- /dev/null +++ b/notification/Bubbles/app/src/main/res/mipmap-mdpi/ic_launcher_round.png 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 Binary files differnew file mode 100644 index 00000000..e5ed4659 --- /dev/null +++ b/notification/Bubbles/app/src/main/res/mipmap-xhdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 00000000..14ed0af3 --- /dev/null +++ b/notification/Bubbles/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png 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 Binary files differnew file mode 100644 index 00000000..b0907cac --- /dev/null +++ b/notification/Bubbles/app/src/main/res/mipmap-xxhdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 00000000..d8ae0315 --- /dev/null +++ b/notification/Bubbles/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png 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 Binary files differnew file mode 100644 index 00000000..2c18de9e --- /dev/null +++ b/notification/Bubbles/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 00000000..beed3cdd --- /dev/null +++ b/notification/Bubbles/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png 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 Binary files differnew file mode 100644 index 00000000..87b738cb --- /dev/null +++ b/notification/Bubbles/gradle/wrapper/gradle-wrapper.jar 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 Binary files differnew file mode 100644 index 00000000..bfa55cfb --- /dev/null +++ b/notification/Bubbles/screenshots/bubble.png diff --git a/notification/Bubbles/screenshots/chat.png b/notification/Bubbles/screenshots/chat.png Binary files differnew file mode 100644 index 00000000..55c475bb --- /dev/null +++ b/notification/Bubbles/screenshots/chat.png diff --git a/notification/Bubbles/screenshots/icon-web.png b/notification/Bubbles/screenshots/icon-web.png Binary files differnew file mode 100644 index 00000000..2c18de9e --- /dev/null +++ b/notification/Bubbles/screenshots/icon-web.png diff --git a/notification/Bubbles/screenshots/main.png b/notification/Bubbles/screenshots/main.png Binary files differnew file mode 100644 index 00000000..ab89c979 --- /dev/null +++ b/notification/Bubbles/screenshots/main.png 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> |