diff options
author | Benjamin Baxter <benbaxter@google.com> | 2017-03-22 16:03:01 -0700 |
---|---|---|
committer | Benjamin Baxter <benbaxter@google.com> | 2017-04-20 19:50:46 -0700 |
commit | bf82f6671608e5abc123c501cbe2709ceea9b739 (patch) | |
tree | ef957504bb65bb54ed4121dfb3a4245794682a14 /wearable/wear/WearMessagingApp | |
parent | 2635b32fcaaaf2eccb4c177dd45bcfb48bccfa2f (diff) | |
download | android-bf82f6671608e5abc123c501cbe2709ceea9b739.tar.gz |
Adding chat list module
Bug: 34841755
Change-Id: I9a4472519fa959c814396dbe65e1bfdd7c773e03
Diffstat (limited to 'wearable/wear/WearMessagingApp')
7 files changed, 500 insertions, 3 deletions
diff --git a/wearable/wear/WearMessagingApp/Wearable/src/main/java/com/example/android/wearable/wear/messaging/GoogleSignedInActivity.java b/wearable/wear/WearMessagingApp/Wearable/src/main/java/com/example/android/wearable/wear/messaging/GoogleSignedInActivity.java index 494c9c7b..8bad9f8e 100644 --- a/wearable/wear/WearMessagingApp/Wearable/src/main/java/com/example/android/wearable/wear/messaging/GoogleSignedInActivity.java +++ b/wearable/wear/WearMessagingApp/Wearable/src/main/java/com/example/android/wearable/wear/messaging/GoogleSignedInActivity.java @@ -21,6 +21,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.wearable.activity.WearableActivity; import android.util.Log; +import com.example.android.wearable.wear.messaging.mock.MockDatabase; import com.example.android.wearable.wear.messaging.model.Profile; import com.example.android.wearable.wear.messaging.util.SharedPreferencesHelper; import com.google.android.gms.auth.api.Auth; @@ -57,6 +58,7 @@ public abstract class GoogleSignedInActivity extends WearableActivity protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setAmbientEnabled(); + MockDatabase.init(this); // Try to get the user if they don't exist, return to sign in. try { diff --git a/wearable/wear/WearMessagingApp/Wearable/src/main/java/com/example/android/wearable/wear/messaging/SignInActivity.java b/wearable/wear/WearMessagingApp/Wearable/src/main/java/com/example/android/wearable/wear/messaging/SignInActivity.java index af3d74be..0189930c 100644 --- a/wearable/wear/WearMessagingApp/Wearable/src/main/java/com/example/android/wearable/wear/messaging/SignInActivity.java +++ b/wearable/wear/WearMessagingApp/Wearable/src/main/java/com/example/android/wearable/wear/messaging/SignInActivity.java @@ -23,6 +23,7 @@ import android.support.wearable.activity.WearableActivity; import android.util.Log; import android.view.View; import android.widget.Toast; +import com.example.android.wearable.wear.messaging.chatlist.ChatListActivity; import com.example.android.wearable.wear.messaging.mock.MockDatabase; import com.example.android.wearable.wear.messaging.model.Profile; import com.google.android.gms.auth.api.Auth; @@ -185,8 +186,8 @@ public class SignInActivity extends WearableActivity mSignInButton.setEnabled(false); } -// Intent chatActivityIntent = new Intent(this, ChatListActivity.class); -// startActivity(chatActivityIntent); + Intent chatActivityIntent = new Intent(this, ChatListActivity.class); + startActivity(chatActivityIntent); finish(); } diff --git a/wearable/wear/WearMessagingApp/Wearable/src/main/java/com/example/android/wearable/wear/messaging/chat/ChatAdapter.java b/wearable/wear/WearMessagingApp/Wearable/src/main/java/com/example/android/wearable/wear/messaging/chat/ChatAdapter.java index 6e2831b8..5cca0872 100644 --- a/wearable/wear/WearMessagingApp/Wearable/src/main/java/com/example/android/wearable/wear/messaging/chat/ChatAdapter.java +++ b/wearable/wear/WearMessagingApp/Wearable/src/main/java/com/example/android/wearable/wear/messaging/chat/ChatAdapter.java @@ -143,7 +143,7 @@ class ChatAdapter extends WearableRecyclerView.Adapter<ChatAdapter.MessageViewHo } /** - * Converts time since epoch to Month Date Time + * Converts time since epoch to Month Date Time. * * @param time since epoch * @return String formatted in Month Date HH:MM diff --git a/wearable/wear/WearMessagingApp/Wearable/src/main/java/com/example/android/wearable/wear/messaging/chatlist/ChatListActivity.java b/wearable/wear/WearMessagingApp/Wearable/src/main/java/com/example/android/wearable/wear/messaging/chatlist/ChatListActivity.java new file mode 100644 index 00000000..56727d8d --- /dev/null +++ b/wearable/wear/WearMessagingApp/Wearable/src/main/java/com/example/android/wearable/wear/messaging/chatlist/ChatListActivity.java @@ -0,0 +1,135 @@ +/* + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.wearable.wear.messaging.chatlist; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.widget.LinearLayoutManager; +import android.support.wearable.activity.WearableActivity; +import android.support.wearable.view.WearableRecyclerView; +import android.util.Log; +import com.example.android.wearable.wear.messaging.GoogleSignedInActivity; +import com.example.android.wearable.wear.messaging.R; +import com.example.android.wearable.wear.messaging.chat.ChatActivity; +import com.example.android.wearable.wear.messaging.contacts.ContactsListActivity; +import com.example.android.wearable.wear.messaging.mock.MockDatabase; +import com.example.android.wearable.wear.messaging.model.Chat; +import com.example.android.wearable.wear.messaging.model.Profile; +import com.example.android.wearable.wear.messaging.util.Constants; +import com.example.android.wearable.wear.messaging.util.DividerItemDecoration; +import com.example.android.wearable.wear.messaging.util.SharedPreferencesHelper; +import java.io.IOException; +import java.util.ArrayList; + +/** + * Displays list of active chats of user. + * + * <p>Uses a simple mocked backend solution with shared preferences. + * + * TODO: Processes database activities on the UI thread, move to async. + */ +public class ChatListActivity extends GoogleSignedInActivity { + + private static final String TAG = "ChatListActivity"; + + // Triggered by contact selection in ContactsListActivity. + private static final int CONTACTS_SELECTED_REQUEST_CODE = 9004; + + private WearableRecyclerView mRecyclerView; + private ChatListAdapter mRecyclerAdapter; + private Profile mUser; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setTheme(R.style.BlueTheme); + setContentView(R.layout.activity_chat_list); + + mRecyclerView = (WearableRecyclerView) findViewById(R.id.recycler_view); + + mRecyclerView.addItemDecoration(new DividerItemDecoration(this, R.drawable.divider)); + LinearLayoutManager layoutManager = new LinearLayoutManager(this); + mRecyclerView.setLayoutManager(layoutManager); + mRecyclerView.setHasFixedSize(true); + + mRecyclerAdapter = new ChatListAdapter(this, new MyChatListAdapterListener(this)); + mRecyclerView.setAdapter(mRecyclerAdapter); + } + + @Override + protected void onStart() { + super.onStart(); + Log.d(TAG, "onStart"); + + // Try to get the user if they don't exist, return to login. + try { + mUser = SharedPreferencesHelper.readUserFromJsonPref(this); + } catch (IOException e) { + Log.e(TAG, "User is not stored locally"); + } + if (mUser == null) { + onGoogleSignInFailure(); + } else { + mRecyclerAdapter.setChats(MockDatabase.getAllChats()); + } + } + + /** + * A handler for the adapter to launch the correct activities based on the type of item selected + */ + private class MyChatListAdapterListener implements ChatListAdapter.ChatAdapterListener { + + private final WearableActivity activity; + + MyChatListAdapterListener(WearableActivity activity) { + this.activity = activity; + } + + @Override + public void newChatSelected() { + Intent intent = new Intent(activity, ContactsListActivity.class); + activity.startActivityForResult(intent, CONTACTS_SELECTED_REQUEST_CODE); + } + + @Override + public void openChat(Chat chat) { + Intent startChat = new Intent(activity, ChatActivity.class); + startChat.putExtra(Constants.EXTRA_CHAT, chat); + activity.startActivity(startChat); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == CONTACTS_SELECTED_REQUEST_CODE) { + // Selected contacts confirmed. + if (resultCode == RESULT_OK) { + ArrayList<Profile> contacts = + data.getParcelableArrayListExtra(Constants.RESULT_CONTACTS_KEY); + // TODO: this should be moved to the background, + Chat newChatWithSelectedContacts = MockDatabase.createChat(contacts, getUser()); + + Log.d(TAG, String.format("Starting chat with %d contact(s)", contacts.size())); + + // Launch ChatActivity with new chat. + Intent startChat = new Intent(this, ChatActivity.class); + startChat.putExtra(Constants.EXTRA_CHAT, newChatWithSelectedContacts); + startActivity(startChat); + } + } + } +} diff --git a/wearable/wear/WearMessagingApp/Wearable/src/main/java/com/example/android/wearable/wear/messaging/chatlist/ChatListAdapter.java b/wearable/wear/WearMessagingApp/Wearable/src/main/java/com/example/android/wearable/wear/messaging/chatlist/ChatListAdapter.java new file mode 100644 index 00000000..7fb8edb4 --- /dev/null +++ b/wearable/wear/WearMessagingApp/Wearable/src/main/java/com/example/android/wearable/wear/messaging/chatlist/ChatListAdapter.java @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.wearable.wear.messaging.chatlist; + +import android.content.Context; +import android.support.v7.util.SortedList; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import com.bumptech.glide.Glide; +import com.example.android.wearable.wear.messaging.R; +import com.example.android.wearable.wear.messaging.model.Chat; +import com.example.android.wearable.wear.messaging.model.Message; +import com.example.android.wearable.wear.messaging.model.Profile; +import de.hdodenhof.circleimageview.CircleImageView; +import java.util.Collection; + +/** + * Adapter for list of chats. + * + * <p>If chat is empty, displays icon to start a new chat. Otherwise, activity displays the title of + * chat as first element followed by the rest of the conversation. + * + * <p>The RecyclerView holds the title so it can scroll off the screen as a user scrolls down the + * page. + */ +public class ChatListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { + + private static final String TAG = "ChatListAdapter"; + + private static final int TYPE_ADD = 0; + private static final int TYPE_CONTENT = 1; + + private static final int INDEX_OFFSET = 1; + + private final Context context; + private final ChatAdapterListener listener; + private final SortedList<Chat> mChats; + + public ChatListAdapter(Context context, ChatAdapterListener listener) { + this.context = context; + this.listener = listener; + mChats = + new SortedList<>( + Chat.class, + new SortedList.Callback<Chat>() { + + // Descending list based on chat's last message time. + @Override + public int compare(Chat chat1, Chat chat2) { + Long diff = + (chat2.getLastMessage().getSentTime() + - chat1.getLastMessage().getSentTime()); + return diff.intValue(); + } + + @Override + public void onInserted(int position, int count) { + notifyItemRangeInserted(position + INDEX_OFFSET, count); + } + + @Override + public void onRemoved(int position, int count) { + notifyItemRangeRemoved(position + INDEX_OFFSET, count); + } + + @Override + public void onMoved(int fromPosition, int toPosition) { + notifyItemMoved( + fromPosition + INDEX_OFFSET, toPosition + INDEX_OFFSET); + } + + @Override + public void onChanged(int position, int count) { + Log.d( + TAG, + String.format("Item changed %d", position + INDEX_OFFSET)); + // Since position 0 is the title + notifyItemRangeChanged(position + INDEX_OFFSET, count); + } + + @Override + public boolean areContentsTheSame(Chat oldItem, Chat newItem) { + return oldItem.equals(newItem); + } + + @Override + public boolean areItemsTheSame(Chat item1, Chat item2) { + return item1.getId().equals(item2.getId()); + } + }); + } + + /** + * A listener for a client to receive events about which actionable items occurred in the + * adapter; i.e., either the user wants a brand new chat started or they are opening an existing + * chat. + */ + public interface ChatAdapterListener { + void newChatSelected(); + + void openChat(Chat chat); + } + + @Override + public int getItemViewType(int position) { + if (position == 0) { + return TYPE_ADD; + } + return TYPE_CONTENT; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if (viewType == TYPE_ADD) { + return new AddChatViewHolder( + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.action_new_chat, parent, false)); + } else { + return new ChatItemViewHolder( + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.chat_list_item, parent, false)); + } + } + + @Override + public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) { + + if (holder instanceof AddChatViewHolder) { + ((AddChatViewHolder) holder) + .row.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + Log.d(TAG, "New chat has been selected"); + listener.newChatSelected(); + } + }); + } else if (holder instanceof ChatItemViewHolder) { + final ChatItemViewHolder chatItemViewHolder = (ChatItemViewHolder) holder; + + //need to account for having the add button before the list of mChats + final Chat chat = mChats.get(position - INDEX_OFFSET); + + chatItemViewHolder.row.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + listener.openChat(chat); + } + }); + + chatItemViewHolder.alias.setText(chat.getAlias()); + + Message lastMessage = chat.getLastMessage(); + if (lastMessage != null) { + Profile lastMessageSender; + if (chat.getParticipants().size() == 1) { + lastMessageSender = chat.getParticipants().values().iterator().next(); + } else { + lastMessageSender = chat.getParticipants().get(lastMessage.getSenderId()); + } + + if (lastMessageSender == null) { + chatItemViewHolder.aliasImage.setImageResource(R.drawable.ic_face_white_24dp); + // Blank out any text that may be left after updating the view holder contents. + chatItemViewHolder.lastMessage.setText(""); + } else { + String lastMessageText = lastMessage.getText(); + String messageString = lastMessageSender.getName() + ": " + lastMessageText; + + chatItemViewHolder.lastMessage.setText(messageString); + + Glide.with(context) + .load(lastMessageSender.getProfileImageUri()) + .placeholder(R.drawable.ic_face_white_24dp) + .into(chatItemViewHolder.aliasImage); + } + } + + // Show group icon if more than 1 participant + int numParticipants = chat.getParticipants().size(); + if (numParticipants > 1) { + chatItemViewHolder.aliasImage.setImageResource(R.drawable.ic_group_white_48dp); + } + } + } + + @Override + public int getItemCount() { + return mChats.size() + INDEX_OFFSET; + } + + public void setChats(final Collection<Chat> chats) { + // There is a bug with {@link SortedList#addAll} that will add new items to the end + // of the list allowing duplications in the view which is unexpected behavior + // https://code.google.com/p/android/issues/detail?id=201618 + // so we will mimic the add all operation (add individually but execute in one batch) + mChats.beginBatchedUpdates(); + for (Chat chat : chats) { + mChats.add(chat); + } + mChats.endBatchedUpdates(); + } + + private class AddChatViewHolder extends RecyclerView.ViewHolder { + + private final ViewGroup row; + + AddChatViewHolder(View itemView) { + super(itemView); + + row = (ViewGroup) itemView.findViewById(R.id.percent_layout); + } + } + + private class ChatItemViewHolder extends RecyclerView.ViewHolder { + + private ViewGroup row; + private final TextView alias; + private final CircleImageView aliasImage; + private final TextView lastMessage; + + ChatItemViewHolder(View itemView) { + super(itemView); + + row = (ViewGroup) itemView.findViewById(R.id.layout_chat_list_item); + alias = (TextView) itemView.findViewById(R.id.text_alias); + aliasImage = (CircleImageView) itemView.findViewById(R.id.profile); + lastMessage = (TextView) itemView.findViewById(R.id.text_last_message); + } + } +} diff --git a/wearable/wear/WearMessagingApp/Wearable/src/main/res/layout/activity_chat_list.xml b/wearable/wear/WearMessagingApp/Wearable/src/main/res/layout/activity_chat_list.xml new file mode 100644 index 00000000..e703e1b4 --- /dev/null +++ b/wearable/wear/WearMessagingApp/Wearable/src/main/res/layout/activity_chat_list.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2017 Google Inc. + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT 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" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/mainFrameLayout" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context="com.example.android.wearable.wear.messaging.chatlist.ChatListActivity"> + + <android.support.wearable.view.WearableRecyclerView + android:id="@+id/recycler_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@null" + android:clipToPadding="false" + android:paddingBottom="@dimen/list_item_height" + android:paddingTop="@dimen/list_item_height" + android:scrollbars="vertical" /> + +</FrameLayout>
\ No newline at end of file diff --git a/wearable/wear/WearMessagingApp/Wearable/src/main/res/layout/chat_list_item.xml b/wearable/wear/WearMessagingApp/Wearable/src/main/res/layout/chat_list_item.xml new file mode 100644 index 00000000..8135586a --- /dev/null +++ b/wearable/wear/WearMessagingApp/Wearable/src/main/res/layout/chat_list_item.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2017 Google Inc. + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<android.support.percent.PercentRelativeLayout 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/layout_chat_list_item" + android:paddingTop="@dimen/vertical_spacing" + android:paddingBottom="@dimen/vertical_spacing" + android:layout_marginBottom="@dimen/divider" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <RelativeLayout + android:id="@+id/profile_layout" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:layout_centerVertical="true" + app:layout_marginStartPercent="@dimen/padding_15"> + + <de.hdodenhof.circleimageview.CircleImageView + android:id="@+id/profile" + android:layout_width="@dimen/circle_image_diameter" + android:layout_height="@dimen/circle_image_diameter" + android:layout_alignParentStart="true" + android:layout_centerVertical="true" + tools:src="@drawable/ic_face_white_24dp" + app:circle_border_color="@color/blue_15" + app:circle_border_width="2dp" + app:layout_marginStartPercent="@dimen/padding_15" + android:layout_centerInParent="true" /> + + </RelativeLayout> + + <LinearLayout + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:layout_centerVertical="true" + android:layout_toEndOf="@id/profile_layout" + android:orientation="vertical" + android:layout_marginStart="@dimen/keyline_large" + app:layout_marginEndPercent="@dimen/padding_10"> + <TextView + android:id="@+id/text_alias" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ellipsize="end" + android:includeFontPadding="false" + android:maxLines="1" + tools:text="Alias" + android:layout_marginBottom="@dimen/vertical_spacing_small"/> + <TextView + android:id="@+id/text_last_message" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ellipsize="end" + android:includeFontPadding="false" + android:maxLines="1" + tools:text="@string/holder_text" /> + </LinearLayout> +</android.support.percent.PercentRelativeLayout>
\ No newline at end of file |