Initial checkin of AOSP Messaging app.

b/23110861

Change-Id: I9aa980d7569247d6b2ca78f5dcb4502e1eaadb8a
This commit is contained in:
Mike Dodd
2015-08-12 08:58:28 -07:00
parent 8b3e2b9c1b
commit 461a34b466
1645 changed files with 186271 additions and 0 deletions
@@ -0,0 +1,339 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.ui.conversationlist;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Fragment;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.view.View;
import com.android.messaging.R;
import com.android.messaging.datamodel.action.DeleteConversationAction;
import com.android.messaging.datamodel.action.UpdateConversationArchiveStatusAction;
import com.android.messaging.datamodel.action.UpdateConversationOptionsAction;
import com.android.messaging.datamodel.action.UpdateDestinationBlockedAction;
import com.android.messaging.datamodel.data.ConversationListData;
import com.android.messaging.datamodel.data.ConversationListItemData;
import com.android.messaging.ui.BugleActionBarActivity;
import com.android.messaging.ui.SnackBar;
import com.android.messaging.ui.SnackBarInteraction;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.ui.contact.AddContactsConfirmationDialog;
import com.android.messaging.ui.conversationlist.ConversationListFragment.ConversationListFragmentHost;
import com.android.messaging.ui.conversationlist.MultiSelectActionModeCallback.SelectedConversation;
import com.android.messaging.util.BugleGservices;
import com.android.messaging.util.BugleGservicesKeys;
import com.android.messaging.util.DebugUtils;
import com.android.messaging.util.PhoneUtils;
import com.android.messaging.util.Trace;
import com.android.messaging.util.UiUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.annotation.Nullable;
/**
* Base class for many Conversation List activities. This will handle the common actions of multi
* select and common launching of intents.
*/
public abstract class AbstractConversationListActivity extends BugleActionBarActivity
implements ConversationListFragmentHost, MultiSelectActionModeCallback.Listener {
private static final int REQUEST_SET_DEFAULT_SMS_APP = 1;
protected ConversationListFragment mConversationListFragment;
@Override
public void onAttachFragment(final Fragment fragment) {
Trace.beginSection("AbstractConversationListActivity.onAttachFragment");
// Fragment could be debug dialog
if (fragment instanceof ConversationListFragment) {
mConversationListFragment = (ConversationListFragment) fragment;
mConversationListFragment.setHost(this);
}
Trace.endSection();
}
@Override
public void onBackPressed() {
// If action mode is active dismiss it
if (getActionMode() != null) {
dismissActionMode();
return;
}
super.onBackPressed();
}
protected void startMultiSelectActionMode() {
startActionMode(new MultiSelectActionModeCallback(this));
}
protected void exitMultiSelectState() {
mConversationListFragment.showFab();
dismissActionMode();
mConversationListFragment.updateUi();
}
protected boolean isInConversationListSelectMode() {
return getActionModeCallback() instanceof MultiSelectActionModeCallback;
}
@Override
public boolean isSelectionMode() {
return isInConversationListSelectMode();
}
@Override
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
}
@Override
public void onActionBarDelete(final Collection<SelectedConversation> conversations) {
if (!PhoneUtils.getDefault().isDefaultSmsApp()) {
// TODO: figure out a good way to combine this with the implementation in
// ConversationFragment doing similar things
final Activity activity = this;
UiUtils.showSnackBarWithCustomAction(this,
getWindow().getDecorView().getRootView(),
getString(R.string.requires_default_sms_app),
SnackBar.Action.createCustomAction(new Runnable() {
@Override
public void run() {
final Intent intent =
UIIntents.get().getChangeDefaultSmsAppIntent(activity);
startActivityForResult(intent, REQUEST_SET_DEFAULT_SMS_APP);
}
},
getString(R.string.requires_default_sms_change_button)),
null /* interactions */,
null /* placement */);
return;
}
new AlertDialog.Builder(this)
.setTitle(getResources().getQuantityString(
R.plurals.delete_conversations_confirmation_dialog_title,
conversations.size()))
.setPositiveButton(R.string.delete_conversation_confirmation_button,
new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog,
final int button) {
for (final SelectedConversation conversation : conversations) {
DeleteConversationAction.deleteConversation(
conversation.conversationId,
conversation.timestamp);
}
exitMultiSelectState();
}
})
.setNegativeButton(R.string.delete_conversation_decline_button, null)
.show();
}
@Override
public void onActionBarArchive(final Iterable<SelectedConversation> conversations,
final boolean isToArchive) {
final ArrayList<String> conversationIds = new ArrayList<String>();
for (final SelectedConversation conversation : conversations) {
final String conversationId = conversation.conversationId;
conversationIds.add(conversationId);
if (isToArchive) {
UpdateConversationArchiveStatusAction.archiveConversation(conversationId);
} else {
UpdateConversationArchiveStatusAction.unarchiveConversation(conversationId);
}
}
final Runnable undoRunnable = new Runnable() {
@Override
public void run() {
for (final String conversationId : conversationIds) {
if (isToArchive) {
UpdateConversationArchiveStatusAction.unarchiveConversation(conversationId);
} else {
UpdateConversationArchiveStatusAction.archiveConversation(conversationId);
}
}
}
};
final int textId =
isToArchive ? R.string.archived_toast_message : R.string.unarchived_toast_message;
final String message = getResources().getString(textId, conversationIds.size());
UiUtils.showSnackBar(this, findViewById(android.R.id.list), message, undoRunnable,
SnackBar.Action.SNACK_BAR_UNDO,
mConversationListFragment.getSnackBarInteractions());
exitMultiSelectState();
}
@Override
public void onActionBarNotification(final Iterable<SelectedConversation> conversations,
final boolean isNotificationOn) {
for (final SelectedConversation conversation : conversations) {
UpdateConversationOptionsAction.enableConversationNotifications(
conversation.conversationId, isNotificationOn);
}
final int textId = isNotificationOn ?
R.string.notification_on_toast_message : R.string.notification_off_toast_message;
final String message = getResources().getString(textId, 1);
UiUtils.showSnackBar(this, findViewById(android.R.id.list), message,
null /* undoRunnable */,
SnackBar.Action.SNACK_BAR_UNDO, mConversationListFragment.getSnackBarInteractions());
exitMultiSelectState();
}
@Override
public void onActionBarAddContact(final SelectedConversation conversation) {
final Uri avatarUri;
if (conversation.icon != null) {
avatarUri = Uri.parse(conversation.icon);
} else {
avatarUri = null;
}
final AddContactsConfirmationDialog dialog = new AddContactsConfirmationDialog(
this, avatarUri, conversation.otherParticipantNormalizedDestination);
dialog.show();
exitMultiSelectState();
}
@Override
public void onActionBarBlock(final SelectedConversation conversation) {
final Resources res = getResources();
new AlertDialog.Builder(this)
.setTitle(res.getString(R.string.block_confirmation_title,
conversation.otherParticipantNormalizedDestination))
.setMessage(res.getString(R.string.block_confirmation_message))
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface arg0, final int arg1) {
final Context context = AbstractConversationListActivity.this;
final View listView = findViewById(android.R.id.list);
final List<SnackBarInteraction> interactions =
mConversationListFragment.getSnackBarInteractions();
final UpdateDestinationBlockedAction.UpdateDestinationBlockedActionListener
undoListener =
new UpdateDestinationBlockedActionSnackBar(
context, listView, null /* undoRunnable */,
interactions);
final Runnable undoRunnable = new Runnable() {
@Override
public void run() {
UpdateDestinationBlockedAction.updateDestinationBlocked(
conversation.otherParticipantNormalizedDestination, false,
conversation.conversationId,
undoListener);
}
};
final UpdateDestinationBlockedAction.UpdateDestinationBlockedActionListener
listener = new UpdateDestinationBlockedActionSnackBar(
context, listView, undoRunnable, interactions);
UpdateDestinationBlockedAction.updateDestinationBlocked(
conversation.otherParticipantNormalizedDestination, true,
conversation.conversationId,
listener);
exitMultiSelectState();
}
})
.create()
.show();
}
@Override
public void onConversationClick(final ConversationListData listData,
final ConversationListItemData conversationListItemData,
final boolean isLongClick,
final ConversationListItemView conversationView) {
if (isLongClick && !isInConversationListSelectMode()) {
startMultiSelectActionMode();
}
if (isInConversationListSelectMode()) {
final MultiSelectActionModeCallback multiSelectActionMode =
(MultiSelectActionModeCallback) getActionModeCallback();
multiSelectActionMode.toggleSelect(listData, conversationListItemData);
mConversationListFragment.updateUi();
} else {
final String conversationId = conversationListItemData.getConversationId();
Bundle sceneTransitionAnimationOptions = null;
boolean hasCustomTransitions = false;
UIIntents.get().launchConversationActivity(
this, conversationId, null,
sceneTransitionAnimationOptions,
hasCustomTransitions);
}
}
@Override
public void onCreateConversationClick() {
UIIntents.get().launchCreateNewConversationActivity(this, null);
}
@Override
public boolean isConversationSelected(final String conversationId) {
return isInConversationListSelectMode() &&
((MultiSelectActionModeCallback) getActionModeCallback()).isSelected(
conversationId);
}
public void onActionBarDebug() {
DebugUtils.showDebugOptions(this);
}
private static class UpdateDestinationBlockedActionSnackBar
implements UpdateDestinationBlockedAction.UpdateDestinationBlockedActionListener {
private final Context mContext;
private final View mParentView;
private final Runnable mUndoRunnable;
private final List<SnackBarInteraction> mInteractions;
UpdateDestinationBlockedActionSnackBar(final Context context,
@NonNull final View parentView, @Nullable final Runnable undoRunnable,
@Nullable List<SnackBarInteraction> interactions) {
mContext = context;
mParentView = parentView;
mUndoRunnable = undoRunnable;
mInteractions = interactions;
}
@Override
public void onUpdateDestinationBlockedAction(
final UpdateDestinationBlockedAction action,
final boolean success, final boolean block,
final String destination) {
if (success) {
final int messageId = block ? R.string.blocked_toast_message
: R.string.unblocked_toast_message;
final String message = mContext.getResources().getString(messageId, 1);
UiUtils.showSnackBar(mContext, mParentView, message, mUndoRunnable,
SnackBar.Action.SNACK_BAR_UNDO, mInteractions);
}
}
}
}
@@ -0,0 +1,96 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.ui.conversationlist;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.view.Menu;
import android.view.MenuItem;
import com.android.messaging.R;
import com.android.messaging.util.DebugUtils;
public class ArchivedConversationListActivity extends AbstractConversationListActivity {
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final ConversationListFragment fragment =
ConversationListFragment.createArchivedConversationListFragment();
getFragmentManager().beginTransaction().add(android.R.id.content, fragment).commit();
invalidateActionBar();
}
protected void updateActionBar(ActionBar actionBar) {
actionBar.setTitle(getString(R.string.archived_activity_title));
actionBar.setDisplayShowTitleEnabled(true);
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setBackgroundDrawable(new ColorDrawable(
getResources().getColor(
R.color.archived_conversation_action_bar_background_color_dark)));
actionBar.show();
super.updateActionBar(actionBar);
}
@Override
public void onBackPressed() {
if (isInConversationListSelectMode()) {
exitMultiSelectState();
} else {
super.onBackPressed();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
if (super.onCreateOptionsMenu(menu)) {
return true;
}
getMenuInflater().inflate(R.menu.archived_conversation_list_menu, menu);
final MenuItem item = menu.findItem(R.id.action_debug_options);
if (item != null) {
final boolean enableDebugItems = DebugUtils.isDebugEnabled();
item.setVisible(enableDebugItems).setEnabled(enableDebugItems);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem menuItem) {
switch(menuItem.getItemId()) {
case R.id.action_debug_options:
onActionBarDebug();
return true;
case android.R.id.home:
onActionBarHome();
return true;
default:
return super.onOptionsItemSelected(menuItem);
}
}
@Override
public void onActionBarHome() {
onBackPressed();
}
@Override
public boolean isSwipeAnimatable() {
return false;
}
}
@@ -0,0 +1,144 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.ui.conversationlist;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.view.Menu;
import android.view.MenuItem;
import com.android.messaging.R;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.util.DebugUtils;
import com.android.messaging.util.Trace;
public class ConversationListActivity extends AbstractConversationListActivity {
@Override
protected void onCreate(final Bundle savedInstanceState) {
Trace.beginSection("ConversationListActivity.onCreate");
super.onCreate(savedInstanceState);
setContentView(R.layout.conversation_list_activity);
Trace.endSection();
invalidateActionBar();
}
@Override
protected void updateActionBar(final ActionBar actionBar) {
actionBar.setTitle(getString(R.string.app_name));
actionBar.setDisplayShowTitleEnabled(true);
actionBar.setDisplayHomeAsUpEnabled(false);
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
actionBar.setBackgroundDrawable(new ColorDrawable(
getResources().getColor(R.color.action_bar_background_color)));
actionBar.show();
super.updateActionBar(actionBar);
}
@Override
public void onResume() {
super.onResume();
// Invalidate the menu as items that are based on settings may have changed
// while not in the app (e.g. Talkback enabled/disable affects new conversation
// button)
supportInvalidateOptionsMenu();
}
@Override
public void onBackPressed() {
if (isInConversationListSelectMode()) {
exitMultiSelectState();
} else {
super.onBackPressed();
}
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
if (super.onCreateOptionsMenu(menu)) {
return true;
}
getMenuInflater().inflate(R.menu.conversation_list_fragment_menu, menu);
final MenuItem item = menu.findItem(R.id.action_debug_options);
if (item != null) {
final boolean enableDebugItems = DebugUtils.isDebugEnabled();
item.setVisible(enableDebugItems).setEnabled(enableDebugItems);
}
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem menuItem) {
switch(menuItem.getItemId()) {
case R.id.action_start_new_conversation:
onActionBarStartNewConversation();
return true;
case R.id.action_settings:
onActionBarSettings();
return true;
case R.id.action_debug_options:
onActionBarDebug();
return true;
case R.id.action_show_archived:
onActionBarArchived();
return true;
case R.id.action_show_blocked_contacts:
onActionBarBlockedParticipants();
return true;
}
return super.onOptionsItemSelected(menuItem);
}
@Override
public void onActionBarHome() {
exitMultiSelectState();
}
public void onActionBarStartNewConversation() {
UIIntents.get().launchCreateNewConversationActivity(this, null);
}
public void onActionBarSettings() {
UIIntents.get().launchSettingsActivity(this);
}
public void onActionBarBlockedParticipants() {
UIIntents.get().launchBlockedParticipantsActivity(this);
}
public void onActionBarArchived() {
UIIntents.get().launchArchivedConversationsActivity(this);
}
@Override
public boolean isSwipeAnimatable() {
return !isInConversationListSelectMode();
}
@Override
public void onWindowFocusChanged(final boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
final ConversationListFragment conversationListFragment =
(ConversationListFragment) getFragmentManager().findFragmentById(
R.id.conversation_list_fragment);
// When the screen is turned on, the last used activity gets resumed, but it gets
// window focus only after the lock screen is unlocked.
if (hasFocus && conversationListFragment != null) {
conversationListFragment.setScrolledToNewestConversationIfNeeded();
}
}
}
@@ -0,0 +1,77 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.ui.conversationlist;
import android.content.Context;
import android.database.Cursor;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import com.android.messaging.R;
import com.android.messaging.ui.CursorRecyclerAdapter;
import com.android.messaging.ui.conversationlist.ConversationListItemView.HostInterface;
/**
* Provides an interface to expose Conversation List Cursor data to a UI widget like a ListView.
*/
public class ConversationListAdapter
extends CursorRecyclerAdapter<ConversationListAdapter.ConversationListViewHolder> {
private final ConversationListItemView.HostInterface mClivHostInterface;
public ConversationListAdapter(final Context context, final Cursor cursor,
final ConversationListItemView.HostInterface clivHostInterface) {
super(context, cursor, 0);
mClivHostInterface = clivHostInterface;
setHasStableIds(true);
}
/**
* @see com.android.messaging.ui.CursorRecyclerAdapter#bindViewHolder(
* android.support.v7.widget.RecyclerView.ViewHolder, android.content.Context,
* android.database.Cursor)
*/
@Override
public void bindViewHolder(final ConversationListViewHolder holder, final Context context,
final Cursor cursor) {
final ConversationListItemView conversationListItemView = holder.mView;
conversationListItemView.bind(cursor, mClivHostInterface);
}
@Override
public ConversationListViewHolder createViewHolder(final Context context,
final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(context);
final ConversationListItemView itemView =
(ConversationListItemView) layoutInflater.inflate(
R.layout.conversation_list_item_view, null);
return new ConversationListViewHolder(itemView);
}
/**
* ViewHolder that holds a ConversationListItemView.
*/
public static class ConversationListViewHolder extends RecyclerView.ViewHolder {
final ConversationListItemView mView;
public ConversationListViewHolder(final ConversationListItemView itemView) {
super(itemView);
mView = itemView;
}
}
}
@@ -0,0 +1,446 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.ui.conversationlist;
import android.app.Activity;
import android.app.Fragment;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewGroupCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.ViewPropertyAnimator;
import android.view.accessibility.AccessibilityManager;
import android.widget.AbsListView;
import android.widget.ImageView;
import com.android.messaging.R;
import com.android.messaging.annotation.VisibleForAnimation;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.binding.Binding;
import com.android.messaging.datamodel.binding.BindingBase;
import com.android.messaging.datamodel.data.ConversationListData;
import com.android.messaging.datamodel.data.ConversationListData.ConversationListDataListener;
import com.android.messaging.datamodel.data.ConversationListItemData;
import com.android.messaging.ui.BugleAnimationTags;
import com.android.messaging.ui.ListEmptyView;
import com.android.messaging.ui.SnackBarInteraction;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.util.AccessibilityUtil;
import com.android.messaging.util.Assert;
import com.android.messaging.util.ImeUtil;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.UiUtils;
import com.google.common.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
/**
* Shows a list of conversations.
*/
public class ConversationListFragment extends Fragment implements ConversationListDataListener,
ConversationListItemView.HostInterface {
private static final String BUNDLE_ARCHIVED_MODE = "archived_mode";
private static final String BUNDLE_FORWARD_MESSAGE_MODE = "forward_message_mode";
private static final boolean VERBOSE = false;
private MenuItem mShowBlockedMenuItem;
private boolean mArchiveMode;
private boolean mBlockedAvailable;
private boolean mForwardMessageMode;
public interface ConversationListFragmentHost {
public void onConversationClick(final ConversationListData listData,
final ConversationListItemData conversationListItemData,
final boolean isLongClick,
final ConversationListItemView conversationView);
public void onCreateConversationClick();
public boolean isConversationSelected(final String conversationId);
public boolean isSwipeAnimatable();
public boolean isSelectionMode();
public boolean hasWindowFocus();
}
private ConversationListFragmentHost mHost;
private RecyclerView mRecyclerView;
private ImageView mStartNewConversationButton;
private ListEmptyView mEmptyListMessageView;
private ConversationListAdapter mAdapter;
// Saved Instance State Data - only for temporal data which is nice to maintain but not
// critical for correctness.
private static final String SAVED_INSTANCE_STATE_LIST_VIEW_STATE_KEY =
"conversationListViewState";
private Parcelable mListState;
@VisibleForTesting
final Binding<ConversationListData> mListBinding = BindingBase.createBinding(this);
public static ConversationListFragment createArchivedConversationListFragment() {
return createConversationListFragment(BUNDLE_ARCHIVED_MODE);
}
public static ConversationListFragment createForwardMessageConversationListFragment() {
return createConversationListFragment(BUNDLE_FORWARD_MESSAGE_MODE);
}
public static ConversationListFragment createConversationListFragment(String modeKeyName) {
final ConversationListFragment fragment = new ConversationListFragment();
final Bundle bundle = new Bundle();
bundle.putBoolean(modeKeyName, true);
fragment.setArguments(bundle);
return fragment;
}
/**
* {@inheritDoc} from Fragment
*/
@Override
public void onCreate(final Bundle bundle) {
super.onCreate(bundle);
mListBinding.getData().init(getLoaderManager(), mListBinding);
mAdapter = new ConversationListAdapter(getActivity(), null, this);
}
@Override
public void onResume() {
super.onResume();
Assert.notNull(mHost);
setScrolledToNewestConversationIfNeeded();
updateUi();
}
public void setScrolledToNewestConversationIfNeeded() {
if (!mArchiveMode
&& !mForwardMessageMode
&& isScrolledToFirstConversation()
&& mHost.hasWindowFocus()) {
mListBinding.getData().setScrolledToNewestConversation(true);
}
}
private boolean isScrolledToFirstConversation() {
int firstItemPosition = ((LinearLayoutManager) mRecyclerView.getLayoutManager())
.findFirstCompletelyVisibleItemPosition();
return firstItemPosition == 0;
}
/**
* {@inheritDoc} from Fragment
*/
@Override
public void onDestroy() {
super.onDestroy();
mListBinding.unbind();
mHost = null;
}
/**
* {@inheritDoc} from Fragment
*/
@Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.conversation_list_fragment,
container, false);
mRecyclerView = (RecyclerView) rootView.findViewById(android.R.id.list);
mEmptyListMessageView = (ListEmptyView) rootView.findViewById(R.id.no_conversations_view);
mEmptyListMessageView.setImageHint(R.drawable.ic_oobe_conv_list);
// The default behavior for default layout param generation by LinearLayoutManager is to
// provide width and height of WRAP_CONTENT, but this is not desirable for
// ConversationListFragment; the view in each row should be a width of MATCH_PARENT so that
// the entire row is tappable.
final Activity activity = getActivity();
final LinearLayoutManager manager = new LinearLayoutManager(activity) {
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
};
mRecyclerView.setLayoutManager(manager);
mRecyclerView.setHasFixedSize(true);
mRecyclerView.setAdapter(mAdapter);
mRecyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
int mCurrentState = AbsListView.OnScrollListener.SCROLL_STATE_IDLE;
@Override
public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
if (mCurrentState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL
|| mCurrentState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) {
ImeUtil.get().hideImeKeyboard(getActivity(), mRecyclerView);
}
if (isScrolledToFirstConversation()) {
setScrolledToNewestConversationIfNeeded();
} else {
mListBinding.getData().setScrolledToNewestConversation(false);
}
}
@Override
public void onScrollStateChanged(final RecyclerView recyclerView, final int newState) {
mCurrentState = newState;
}
});
mRecyclerView.addOnItemTouchListener(new ConversationListSwipeHelper(mRecyclerView));
if (savedInstanceState != null) {
mListState = savedInstanceState.getParcelable(SAVED_INSTANCE_STATE_LIST_VIEW_STATE_KEY);
}
mStartNewConversationButton = (ImageView) rootView.findViewById(
R.id.start_new_conversation_button);
if (mArchiveMode) {
mStartNewConversationButton.setVisibility(View.GONE);
} else {
mStartNewConversationButton.setVisibility(View.VISIBLE);
mStartNewConversationButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View clickView) {
mHost.onCreateConversationClick();
}
});
}
ViewCompat.setTransitionName(mStartNewConversationButton, BugleAnimationTags.TAG_FABICON);
// The root view has a non-null background, which by default is deemed by the framework
// to be a "transition group," where all child views are animated together during an
// activity transition. However, we want each individual items in the recycler view to
// show explode animation themselves, so we explicitly tag the root view to be a non-group.
ViewGroupCompat.setTransitionGroup(rootView, false);
setHasOptionsMenu(true);
return rootView;
}
@Override
public void onAttach(final Activity activity) {
super.onAttach(activity);
if (VERBOSE) {
LogUtil.v(LogUtil.BUGLE_TAG, "Attaching List");
}
final Bundle arguments = getArguments();
if (arguments != null) {
mArchiveMode = arguments.getBoolean(BUNDLE_ARCHIVED_MODE, false);
mForwardMessageMode = arguments.getBoolean(BUNDLE_FORWARD_MESSAGE_MODE, false);
}
mListBinding.bind(DataModel.get().createConversationListData(activity, this, mArchiveMode));
}
@Override
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
if (mListState != null) {
outState.putParcelable(SAVED_INSTANCE_STATE_LIST_VIEW_STATE_KEY, mListState);
}
}
@Override
public void onPause() {
super.onPause();
mListState = mRecyclerView.getLayoutManager().onSaveInstanceState();
mListBinding.getData().setScrolledToNewestConversation(false);
}
/**
* Call this immediately after attaching the fragment
*/
public void setHost(final ConversationListFragmentHost host) {
Assert.isNull(mHost);
mHost = host;
}
@Override
public void onConversationListCursorUpdated(final ConversationListData data,
final Cursor cursor) {
mListBinding.ensureBound(data);
final Cursor oldCursor = mAdapter.swapCursor(cursor);
updateEmptyListUi(cursor == null || cursor.getCount() == 0);
if (mListState != null && cursor != null && oldCursor == null) {
mRecyclerView.getLayoutManager().onRestoreInstanceState(mListState);
}
}
@Override
public void setBlockedParticipantsAvailable(final boolean blockedAvailable) {
mBlockedAvailable = blockedAvailable;
if (mShowBlockedMenuItem != null) {
mShowBlockedMenuItem.setVisible(blockedAvailable);
}
}
public void updateUi() {
mAdapter.notifyDataSetChanged();
}
@Override
public void onPrepareOptionsMenu(final Menu menu) {
super.onPrepareOptionsMenu(menu);
final MenuItem startNewConversationMenuItem =
menu.findItem(R.id.action_start_new_conversation);
if (startNewConversationMenuItem != null) {
// It is recommended for the Floating Action button functionality to be duplicated as a
// menu
AccessibilityManager accessibilityManager = (AccessibilityManager)
getActivity().getSystemService(Context.ACCESSIBILITY_SERVICE);
startNewConversationMenuItem.setVisible(accessibilityManager
.isTouchExplorationEnabled());
}
final MenuItem archive = menu.findItem(R.id.action_show_archived);
if (archive != null) {
archive.setVisible(true);
}
}
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
if (!isAdded()) {
// Guard against being called before we're added to the activity
return;
}
mShowBlockedMenuItem = menu.findItem(R.id.action_show_blocked_contacts);
if (mShowBlockedMenuItem != null) {
mShowBlockedMenuItem.setVisible(mBlockedAvailable);
}
}
/**
* {@inheritDoc} from ConversationListItemView.HostInterface
*/
@Override
public void onConversationClicked(final ConversationListItemData conversationListItemData,
final boolean isLongClick, final ConversationListItemView conversationView) {
final ConversationListData listData = mListBinding.getData();
mHost.onConversationClick(listData, conversationListItemData, isLongClick,
conversationView);
}
/**
* {@inheritDoc} from ConversationListItemView.HostInterface
*/
@Override
public boolean isConversationSelected(final String conversationId) {
return mHost.isConversationSelected(conversationId);
}
@Override
public boolean isSwipeAnimatable() {
return mHost.isSwipeAnimatable();
}
// Show and hide empty list UI as needed with appropriate text based on view specifics
private void updateEmptyListUi(final boolean isEmpty) {
if (isEmpty) {
int emptyListText;
if (!mListBinding.getData().getHasFirstSyncCompleted()) {
emptyListText = R.string.conversation_list_first_sync_text;
} else if (mArchiveMode) {
emptyListText = R.string.archived_conversation_list_empty_text;
} else {
emptyListText = R.string.conversation_list_empty_text;
}
mEmptyListMessageView.setTextHint(emptyListText);
mEmptyListMessageView.setVisibility(View.VISIBLE);
mEmptyListMessageView.setIsImageVisible(true);
mEmptyListMessageView.setIsVerticallyCentered(true);
} else {
mEmptyListMessageView.setVisibility(View.GONE);
}
}
@Override
public List<SnackBarInteraction> getSnackBarInteractions() {
final List<SnackBarInteraction> interactions = new ArrayList<SnackBarInteraction>(1);
final SnackBarInteraction fabInteraction =
new SnackBarInteraction.BasicSnackBarInteraction(mStartNewConversationButton);
interactions.add(fabInteraction);
return interactions;
}
private ViewPropertyAnimator getNormalizedFabAnimator() {
return mStartNewConversationButton.animate()
.setInterpolator(UiUtils.DEFAULT_INTERPOLATOR)
.setDuration(getActivity().getResources().getInteger(
R.integer.fab_animation_duration_ms));
}
public ViewPropertyAnimator dismissFab() {
// To prevent clicking while animating.
mStartNewConversationButton.setEnabled(false);
final MarginLayoutParams lp =
(MarginLayoutParams) mStartNewConversationButton.getLayoutParams();
final float fabWidthWithLeftRightMargin = mStartNewConversationButton.getWidth()
+ lp.leftMargin + lp.rightMargin;
final int direction = AccessibilityUtil.isLayoutRtl(mStartNewConversationButton) ? -1 : 1;
return getNormalizedFabAnimator().translationX(direction * fabWidthWithLeftRightMargin);
}
public ViewPropertyAnimator showFab() {
return getNormalizedFabAnimator().translationX(0).withEndAction(new Runnable() {
@Override
public void run() {
// Re-enable clicks after the animation.
mStartNewConversationButton.setEnabled(true);
}
});
}
public View getHeroElementForTransition() {
return mArchiveMode ? null : mStartNewConversationButton;
}
@VisibleForAnimation
public RecyclerView getRecyclerView() {
return mRecyclerView;
}
@Override
public void startFullScreenPhotoViewer(
final Uri initialPhoto, final Rect initialPhotoBounds, final Uri photosUri) {
UIIntents.get().launchFullScreenPhotoViewer(
getActivity(), initialPhoto, initialPhotoBounds, photosUri);
}
@Override
public void startFullScreenVideoViewer(final Uri videoUri) {
UIIntents.get().launchFullScreenVideoViewer(getActivity(), videoUri);
}
@Override
public boolean isSelectionMode() {
return mHost != null && mHost.isSelectionMode();
}
}
@@ -0,0 +1,643 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.ui.conversationlist;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.net.Uri;
import android.support.v4.text.BidiFormatter;
import android.support.v4.text.TextDirectionHeuristicsCompat;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLayoutChangeListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.messaging.Factory;
import com.android.messaging.R;
import com.android.messaging.annotation.VisibleForAnimation;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.action.UpdateConversationArchiveStatusAction;
import com.android.messaging.datamodel.data.ConversationListItemData;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.datamodel.media.UriImageRequestDescriptor;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.ui.AsyncImageView;
import com.android.messaging.ui.AudioAttachmentView;
import com.android.messaging.ui.ContactIconView;
import com.android.messaging.ui.SnackBar;
import com.android.messaging.ui.SnackBarInteraction;
import com.android.messaging.util.Assert;
import com.android.messaging.util.ContentType;
import com.android.messaging.util.ImageUtils;
import com.android.messaging.util.OsUtil;
import com.android.messaging.util.PhoneUtils;
import com.android.messaging.util.Typefaces;
import com.android.messaging.util.UiUtils;
import com.android.messaging.util.UriUtil;
import java.util.List;
/**
* The view for a single entry in a conversation list.
*/
public class ConversationListItemView extends FrameLayout implements OnClickListener,
OnLongClickListener, OnLayoutChangeListener {
static final int UNREAD_SNIPPET_LINE_COUNT = 3;
static final int NO_UNREAD_SNIPPET_LINE_COUNT = 1;
private int mListItemReadColor;
private int mListItemUnreadColor;
private Typeface mListItemReadTypeface;
private Typeface mListItemUnreadTypeface;
private static String sPlusOneString;
private static String sPlusNString;
public interface HostInterface {
boolean isConversationSelected(final String conversationId);
void onConversationClicked(final ConversationListItemData conversationListItemData,
boolean isLongClick, final ConversationListItemView conversationView);
boolean isSwipeAnimatable();
List<SnackBarInteraction> getSnackBarInteractions();
void startFullScreenPhotoViewer(final Uri initialPhoto, final Rect initialPhotoBounds,
final Uri photosUri);
void startFullScreenVideoViewer(final Uri videoUri);
boolean isSelectionMode();
}
private final OnClickListener fullScreenPreviewClickListener = new OnClickListener() {
@Override
public void onClick(final View v) {
final String previewType = mData.getShowDraft() ?
mData.getDraftPreviewContentType() : mData.getPreviewContentType();
Assert.isTrue(ContentType.isImageType(previewType) ||
ContentType.isVideoType(previewType));
final Uri previewUri = mData.getShowDraft() ?
mData.getDraftPreviewUri() : mData.getPreviewUri();
if (ContentType.isImageType(previewType)) {
final Uri imagesUri = mData.getShowDraft() ?
MessagingContentProvider.buildDraftImagesUri(mData.getConversationId()) :
MessagingContentProvider
.buildConversationImagesUri(mData.getConversationId());
final Rect previewImageBounds = UiUtils.getMeasuredBoundsOnScreen(v);
mHostInterface.startFullScreenPhotoViewer(
previewUri, previewImageBounds, imagesUri);
} else {
mHostInterface.startFullScreenVideoViewer(previewUri);
}
}
};
private final ConversationListItemData mData;
private int mAnimatingCount;
private ViewGroup mSwipeableContainer;
private ViewGroup mCrossSwipeBackground;
private ViewGroup mSwipeableContent;
private TextView mConversationNameView;
private TextView mSnippetTextView;
private TextView mSubjectTextView;
private TextView mTimestampTextView;
private ContactIconView mContactIconView;
private ImageView mContactCheckmarkView;
private ImageView mNotificationBellView;
private ImageView mFailedStatusIconView;
private ImageView mCrossSwipeArchiveLeftImageView;
private ImageView mCrossSwipeArchiveRightImageView;
private AsyncImageView mImagePreviewView;
private AudioAttachmentView mAudioAttachmentView;
private HostInterface mHostInterface;
public ConversationListItemView(final Context context, final AttributeSet attrs) {
super(context, attrs);
mData = new ConversationListItemData();
final Resources res = context.getResources();
}
@Override
protected void onFinishInflate() {
mSwipeableContainer = (ViewGroup) findViewById(R.id.swipeableContainer);
mCrossSwipeBackground = (ViewGroup) findViewById(R.id.crossSwipeBackground);
mSwipeableContent = (ViewGroup) findViewById(R.id.swipeableContent);
mConversationNameView = (TextView) findViewById(R.id.conversation_name);
mSnippetTextView = (TextView) findViewById(R.id.conversation_snippet);
mSubjectTextView = (TextView) findViewById(R.id.conversation_subject);
mTimestampTextView = (TextView) findViewById(R.id.conversation_timestamp);
mContactIconView = (ContactIconView) findViewById(R.id.conversation_icon);
mContactCheckmarkView = (ImageView) findViewById(R.id.conversation_checkmark);
mNotificationBellView = (ImageView) findViewById(R.id.conversation_notification_bell);
mFailedStatusIconView = (ImageView) findViewById(R.id.conversation_failed_status_icon);
mCrossSwipeArchiveLeftImageView = (ImageView) findViewById(R.id.crossSwipeArchiveIconLeft);
mCrossSwipeArchiveRightImageView =
(ImageView) findViewById(R.id.crossSwipeArchiveIconRight);
mImagePreviewView = (AsyncImageView) findViewById(R.id.conversation_image_preview);
mAudioAttachmentView = (AudioAttachmentView) findViewById(R.id.audio_attachment_view);
mConversationNameView.addOnLayoutChangeListener(this);
mSnippetTextView.addOnLayoutChangeListener(this);
final Resources resources = getContext().getResources();
mListItemReadColor = resources.getColor(R.color.conversation_list_item_read);
mListItemUnreadColor = resources.getColor(R.color.conversation_list_item_unread);
mListItemReadTypeface = Typefaces.getRobotoNormal();
mListItemUnreadTypeface = Typefaces.getRobotoBold();
if (OsUtil.isAtLeastL()) {
setTransitionGroup(true);
}
}
@Override
public void onLayoutChange(final View v, final int left, final int top, final int right,
final int bottom, final int oldLeft, final int oldTop, final int oldRight,
final int oldBottom) {
if (v == mConversationNameView) {
setConversationName();
} else if (v == mSnippetTextView) {
setSnippet();
} else if (v == mSubjectTextView) {
setSubject();
}
}
private void setConversationName() {
if (mData.getIsRead() || mData.getShowDraft()) {
mConversationNameView.setTextColor(mListItemReadColor);
mConversationNameView.setTypeface(mListItemReadTypeface);
} else {
mConversationNameView.setTextColor(mListItemUnreadColor);
mConversationNameView.setTypeface(mListItemUnreadTypeface);
}
final String conversationName = mData.getName();
// For group conversations, ellipsize the group members that do not fit
final CharSequence ellipsizedName = UiUtils.commaEllipsize(
conversationName,
mConversationNameView.getPaint(),
mConversationNameView.getMeasuredWidth(),
getPlusOneString(),
getPlusNString());
// RTL : To format conversation name if it happens to be phone number.
final BidiFormatter bidiFormatter = BidiFormatter.getInstance();
final String bidiFormattedName = bidiFormatter.unicodeWrap(
ellipsizedName.toString(),
TextDirectionHeuristicsCompat.LTR);
mConversationNameView.setText(bidiFormattedName);
}
private static String getPlusOneString() {
if (sPlusOneString == null) {
sPlusOneString = Factory.get().getApplicationContext().getResources()
.getString(R.string.plus_one);
}
return sPlusOneString;
}
private static String getPlusNString() {
if (sPlusNString == null) {
sPlusNString = Factory.get().getApplicationContext().getResources()
.getString(R.string.plus_n);
}
return sPlusNString;
}
private void setSubject() {
final String subjectText = mData.getShowDraft() ?
mData.getDraftSubject() :
MmsUtils.cleanseMmsSubject(getContext().getResources(), mData.getSubject());
if (!TextUtils.isEmpty(subjectText)) {
final String subjectPrepend = getResources().getString(R.string.subject_label);
mSubjectTextView.setText(TextUtils.concat(subjectPrepend, subjectText));
mSubjectTextView.setVisibility(VISIBLE);
} else {
mSubjectTextView.setVisibility(GONE);
}
}
private void setSnippet() {
mSnippetTextView.setText(getSnippetText());
}
// Resource Ids of content descriptions prefixes for different message status.
private static final int [][][] sPrimaryContentDescriptions = {
// 1:1 conversation
{
// Incoming message
{
R.string.one_on_one_incoming_failed_message_prefix,
R.string.one_on_one_incoming_successful_message_prefix
},
// Outgoing message
{
R.string.one_on_one_outgoing_failed_message_prefix,
R.string.one_on_one_outgoing_successful_message_prefix,
R.string.one_on_one_outgoing_draft_message_prefix,
R.string.one_on_one_outgoing_sending_message_prefix,
}
},
// Group conversation
{
// Incoming message
{
R.string.group_incoming_failed_message_prefix,
R.string.group_incoming_successful_message_prefix,
},
// Outgoing message
{
R.string.group_outgoing_failed_message_prefix,
R.string.group_outgoing_successful_message_prefix,
R.string.group_outgoing_draft_message_prefix,
R.string.group_outgoing_sending_message_prefix,
}
}
};
// Resource Id of the secondary part of the content description for an edge case of a message
// which is in both draft status and failed status.
private static final int sSecondaryContentDescription =
R.string.failed_message_content_description;
// 1:1 versus group
private static final int CONV_TYPE_ONE_ON_ONE_INDEX = 0;
private static final int CONV_TYPE_ONE_GROUP_INDEX = 1;
// Direction
private static final int DIRECTION_INCOMING_INDEX = 0;
private static final int DIRECTION_OUTGOING_INDEX = 1;
// Message status
private static final int MESSAGE_STATUS_FAILED_INDEX = 0;
private static final int MESSAGE_STATUS_SUCCESSFUL_INDEX = 1;
private static final int MESSAGE_STATUS_DRAFT_INDEX = 2;
private static final int MESSAGE_STATUS_SENDING_INDEX = 3;
private static final int WIDTH_FOR_ACCESSIBLE_CONVERSATION_NAME = 600;
public static String buildContentDescription(final Resources resources,
final ConversationListItemData data, final TextPaint conversationNameViewPaint) {
int messageStatusIndex;
boolean outgoingSnippet = data.getIsMessageTypeOutgoing() || data.getShowDraft();
if (outgoingSnippet) {
if (data.getShowDraft()) {
messageStatusIndex = MESSAGE_STATUS_DRAFT_INDEX;
} else if (data.getIsSendRequested()) {
messageStatusIndex = MESSAGE_STATUS_SENDING_INDEX;
} else {
messageStatusIndex = data.getIsFailedStatus() ? MESSAGE_STATUS_FAILED_INDEX
: MESSAGE_STATUS_SUCCESSFUL_INDEX;
}
} else {
messageStatusIndex = data.getIsFailedStatus() ? MESSAGE_STATUS_FAILED_INDEX
: MESSAGE_STATUS_SUCCESSFUL_INDEX;
}
int resId = sPrimaryContentDescriptions
[data.getIsGroup() ? CONV_TYPE_ONE_GROUP_INDEX : CONV_TYPE_ONE_ON_ONE_INDEX]
[outgoingSnippet ? DIRECTION_OUTGOING_INDEX : DIRECTION_INCOMING_INDEX]
[messageStatusIndex];
final String snippetText = data.getShowDraft() ?
data.getDraftSnippetText() : data.getSnippetText();
final String conversationName = data.getName();
String senderOrConvName = outgoingSnippet ? conversationName : data.getSnippetSenderName();
String primaryContentDescription = resources.getString(resId, senderOrConvName,
snippetText == null ? "" : snippetText,
data.getFormattedTimestamp(),
// This is used only for incoming group messages
conversationName);
String contentDescription = primaryContentDescription;
// An edge case : for an outgoing message, it might be in both draft status and
// failed status.
if (outgoingSnippet && data.getShowDraft() && data.getIsFailedStatus()) {
StringBuilder contentDescriptionBuilder = new StringBuilder();
contentDescriptionBuilder.append(primaryContentDescription);
String secondaryContentDescription =
resources.getString(sSecondaryContentDescription);
contentDescriptionBuilder.append(" ");
contentDescriptionBuilder.append(secondaryContentDescription);
contentDescription = contentDescriptionBuilder.toString();
}
return contentDescription;
}
/**
* Fills in the data associated with this view.
*
* @param cursor The cursor from a ConversationList that this view is in, pointing to its
* entry.
*/
public void bind(final Cursor cursor, final HostInterface hostInterface) {
// Update our UI model
mHostInterface = hostInterface;
mData.bind(cursor);
resetAnimatingState();
mSwipeableContainer.setOnClickListener(this);
mSwipeableContainer.setOnLongClickListener(this);
final Resources resources = getContext().getResources();
int color;
final int maxLines;
final Typeface typeface;
final int typefaceStyle = mData.getShowDraft() ? Typeface.ITALIC : Typeface.NORMAL;
final String snippetText = getSnippetText();
if (mData.getIsRead() || mData.getShowDraft()) {
maxLines = TextUtils.isEmpty(snippetText) ? 0 : NO_UNREAD_SNIPPET_LINE_COUNT;
color = mListItemReadColor;
typeface = mListItemReadTypeface;
} else {
maxLines = TextUtils.isEmpty(snippetText) ? 0 : UNREAD_SNIPPET_LINE_COUNT;
color = mListItemUnreadColor;
typeface = mListItemUnreadTypeface;
}
mSnippetTextView.setMaxLines(maxLines);
mSnippetTextView.setTextColor(color);
mSnippetTextView.setTypeface(typeface, typefaceStyle);
mSubjectTextView.setTextColor(color);
mSubjectTextView.setTypeface(typeface, typefaceStyle);
setSnippet();
setConversationName();
setSubject();
setContentDescription(buildContentDescription(resources, mData,
mConversationNameView.getPaint()));
final boolean isDefaultSmsApp = PhoneUtils.getDefault().isDefaultSmsApp();
// don't show the error state unless we're the default sms app
if (mData.getIsFailedStatus() && isDefaultSmsApp) {
mTimestampTextView.setTextColor(resources.getColor(R.color.conversation_list_error));
mTimestampTextView.setTypeface(mListItemReadTypeface, typefaceStyle);
int failureMessageId = R.string.message_status_download_failed;
if (mData.getIsMessageTypeOutgoing()) {
failureMessageId = MmsUtils.mapRawStatusToErrorResourceId(mData.getMessageStatus(),
mData.getMessageRawTelephonyStatus());
}
mTimestampTextView.setText(resources.getString(failureMessageId));
} else if (mData.getShowDraft()
|| mData.getMessageStatus() == MessageData.BUGLE_STATUS_OUTGOING_DRAFT
// also check for unknown status which we get because sometimes the conversation
// row is left with a latest_message_id of a no longer existing message and
// therefore the join values come back as null (or in this case zero).
|| mData.getMessageStatus() == MessageData.BUGLE_STATUS_UNKNOWN) {
mTimestampTextView.setTextColor(mListItemReadColor);
mTimestampTextView.setTypeface(mListItemReadTypeface, typefaceStyle);
mTimestampTextView.setText(resources.getString(
R.string.conversation_list_item_view_draft_message));
} else {
mTimestampTextView.setTextColor(mListItemReadColor);
mTimestampTextView.setTypeface(mListItemReadTypeface, typefaceStyle);
final String formattedTimestamp = mData.getFormattedTimestamp();
if (mData.getIsSendRequested()) {
mTimestampTextView.setText(R.string.message_status_sending);
} else {
mTimestampTextView.setText(formattedTimestamp);
}
}
final boolean isSelected = mHostInterface.isConversationSelected(mData.getConversationId());
setSelected(isSelected);
Uri iconUri = null;
int contactIconVisibility = GONE;
int checkmarkVisiblity = GONE;
int failStatusVisiblity = GONE;
if (isSelected) {
checkmarkVisiblity = VISIBLE;
} else {
contactIconVisibility = VISIBLE;
// Only show the fail icon if it is not a group conversation.
// And also require that we be the default sms app.
if (mData.getIsFailedStatus() && !mData.getIsGroup() && isDefaultSmsApp) {
failStatusVisiblity = VISIBLE;
}
}
if (mData.getIcon() != null) {
iconUri = Uri.parse(mData.getIcon());
}
mContactIconView.setImageResourceUri(iconUri, mData.getParticipantContactId(),
mData.getParticipantLookupKey(), mData.getOtherParticipantNormalizedDestination());
mContactIconView.setVisibility(contactIconVisibility);
mContactIconView.setOnLongClickListener(this);
mContactIconView.setClickable(!mHostInterface.isSelectionMode());
mContactIconView.setLongClickable(!mHostInterface.isSelectionMode());
mContactCheckmarkView.setVisibility(checkmarkVisiblity);
mFailedStatusIconView.setVisibility(failStatusVisiblity);
final Uri previewUri = mData.getShowDraft() ?
mData.getDraftPreviewUri() : mData.getPreviewUri();
final String previewContentType = mData.getShowDraft() ?
mData.getDraftPreviewContentType() : mData.getPreviewContentType();
OnClickListener previewClickListener = null;
Uri previewImageUri = null;
int previewImageVisibility = GONE;
int audioPreviewVisiblity = GONE;
if (previewUri != null && !TextUtils.isEmpty(previewContentType)) {
if (ContentType.isAudioType(previewContentType)) {
mAudioAttachmentView.bind(previewUri, false);
audioPreviewVisiblity = VISIBLE;
} else if (ContentType.isVideoType(previewContentType)) {
previewImageUri = UriUtil.getUriForResourceId(
getContext(), R.drawable.ic_preview_play);
previewClickListener = fullScreenPreviewClickListener;
previewImageVisibility = VISIBLE;
} else if (ContentType.isImageType(previewContentType)) {
previewImageUri = previewUri;
previewClickListener = fullScreenPreviewClickListener;
previewImageVisibility = VISIBLE;
}
}
final int imageSize = resources.getDimensionPixelSize(
R.dimen.conversation_list_image_preview_size);
mImagePreviewView.setImageResourceId(
new UriImageRequestDescriptor(previewImageUri, imageSize, imageSize,
true /* allowCompression */, false /* isStatic */, false /*cropToCircle*/,
ImageUtils.DEFAULT_CIRCLE_BACKGROUND_COLOR /* circleBackgroundColor */,
ImageUtils.DEFAULT_CIRCLE_STROKE_COLOR /* circleStrokeColor */));
mImagePreviewView.setOnLongClickListener(this);
mImagePreviewView.setVisibility(previewImageVisibility);
mImagePreviewView.setOnClickListener(previewClickListener);
mAudioAttachmentView.setOnLongClickListener(this);
mAudioAttachmentView.setVisibility(audioPreviewVisiblity);
final int notificationBellVisiblity = mData.getNotificationEnabled() ? GONE : VISIBLE;
mNotificationBellView.setVisibility(notificationBellVisiblity);
}
public boolean isSwipeAnimatable() {
return mHostInterface.isSwipeAnimatable();
}
@VisibleForAnimation
public float getSwipeTranslationX() {
return mSwipeableContainer.getTranslationX();
}
@VisibleForAnimation
public void setSwipeTranslationX(final float translationX) {
mSwipeableContainer.setTranslationX(translationX);
if (translationX == 0) {
mCrossSwipeBackground.setVisibility(View.GONE);
mCrossSwipeArchiveLeftImageView.setVisibility(GONE);
mCrossSwipeArchiveRightImageView.setVisibility(GONE);
mSwipeableContainer.setBackgroundColor(Color.TRANSPARENT);
} else {
mCrossSwipeBackground.setVisibility(View.VISIBLE);
if (translationX > 0) {
mCrossSwipeArchiveLeftImageView.setVisibility(VISIBLE);
mCrossSwipeArchiveRightImageView.setVisibility(GONE);
} else {
mCrossSwipeArchiveLeftImageView.setVisibility(GONE);
mCrossSwipeArchiveRightImageView.setVisibility(VISIBLE);
}
mSwipeableContainer.setBackgroundResource(R.drawable.swipe_shadow_drag);
}
}
public void onSwipeComplete() {
final String conversationId = mData.getConversationId();
UpdateConversationArchiveStatusAction.archiveConversation(conversationId);
final Runnable undoRunnable = new Runnable() {
@Override
public void run() {
UpdateConversationArchiveStatusAction.unarchiveConversation(conversationId);
}
};
final String message = getResources().getString(R.string.archived_toast_message, 1);
UiUtils.showSnackBar(getContext(), getRootView(), message, undoRunnable,
SnackBar.Action.SNACK_BAR_UNDO,
mHostInterface.getSnackBarInteractions());
}
private void setShortAndLongClickable(final boolean clickable) {
setClickable(clickable);
setLongClickable(clickable);
}
private void resetAnimatingState() {
mAnimatingCount = 0;
setShortAndLongClickable(true);
setSwipeTranslationX(0);
}
/**
* Notifies this view that it is undergoing animation. This view should disable its click
* targets.
*
* The animating counter is used to reset the swipe controller when the counter becomes 0. A
* positive counter also makes the view not clickable.
*/
public final void setAnimating(final boolean animating) {
final int oldAnimatingCount = mAnimatingCount;
if (animating) {
mAnimatingCount++;
} else {
mAnimatingCount--;
if (mAnimatingCount < 0) {
mAnimatingCount = 0;
}
}
if (mAnimatingCount == 0) {
// New count is 0. All animations ended.
setShortAndLongClickable(true);
} else if (oldAnimatingCount == 0) {
// New count is > 0. Waiting for some animations to end.
setShortAndLongClickable(false);
}
}
public boolean isAnimating() {
return mAnimatingCount > 0;
}
/**
* {@inheritDoc} from OnClickListener
*/
@Override
public void onClick(final View v) {
processClick(v, false);
}
/**
* {@inheritDoc} from OnLongClickListener
*/
@Override
public boolean onLongClick(final View v) {
return processClick(v, true);
}
private boolean processClick(final View v, final boolean isLongClick) {
Assert.isTrue(v == mSwipeableContainer || v == mContactIconView || v == mImagePreviewView);
Assert.notNull(mData.getName());
if (mHostInterface != null) {
mHostInterface.onConversationClicked(mData, isLongClick, this);
return true;
}
return false;
}
public View getSwipeableContent() {
return mSwipeableContent;
}
public View getContactIconView() {
return mContactIconView;
}
private String getSnippetText() {
String snippetText = mData.getShowDraft() ?
mData.getDraftSnippetText() : mData.getSnippetText();
final String previewContentType = mData.getShowDraft() ?
mData.getDraftPreviewContentType() : mData.getPreviewContentType();
if (TextUtils.isEmpty(snippetText)) {
Resources resources = getResources();
// Use the attachment type as a snippet so the preview doesn't look odd
if (ContentType.isAudioType(previewContentType)) {
snippetText = resources.getString(R.string.conversation_list_snippet_audio_clip);
} else if (ContentType.isImageType(previewContentType)) {
snippetText = resources.getString(R.string.conversation_list_snippet_picture);
} else if (ContentType.isVideoType(previewContentType)) {
snippetText = resources.getString(R.string.conversation_list_snippet_video);
} else if (ContentType.isVCardType(previewContentType)) {
snippetText = resources.getString(R.string.conversation_list_snippet_vcard);
}
}
return snippetText;
}
}
@@ -0,0 +1,462 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.ui.conversationlist;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.content.Context;
import android.content.res.Resources;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.OnItemTouchListener;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import com.android.messaging.R;
import com.android.messaging.util.Assert;
import com.android.messaging.util.UiUtils;
/**
* Animation and touch helper class for Conversation List swipe.
*/
public class ConversationListSwipeHelper implements OnItemTouchListener {
private static final int UNIT_SECONDS = 1000;
private static final boolean ANIMATING = true;
private static final float ERROR_FACTOR_MULTIPLIER = 1.2f;
private static final float PERCENTAGE_OF_WIDTH_TO_DISMISS = 0.4f;
private static final float FLING_PERCENTAGE_OF_WIDTH_TO_DISMISS = 0.05f;
private static final int SWIPE_DIRECTION_NONE = 0;
private static final int SWIPE_DIRECTION_LEFT = 1;
private static final int SWIPE_DIRECTION_RIGHT = 2;
private final RecyclerView mRecyclerView;
private final long mDefaultRestoreAnimationDuration;
private final long mDefaultDismissAnimationDuration;
private final long mMaxTranslationAnimationDuration;
private final int mTouchSlop;
private final int mMinimumFlingVelocity;
private final int mMaximumFlingVelocity;
/* Valid throughout a single gesture. */
private VelocityTracker mVelocityTracker;
private float mInitialX;
private float mInitialY;
private boolean mIsSwiping;
private ConversationListItemView mListItemView;
public ConversationListSwipeHelper(final RecyclerView recyclerView) {
mRecyclerView = recyclerView;
final Context context = mRecyclerView.getContext();
final Resources res = context.getResources();
mDefaultRestoreAnimationDuration = res.getInteger(R.integer.swipe_duration_ms);
mDefaultDismissAnimationDuration = res.getInteger(R.integer.swipe_duration_ms);
mMaxTranslationAnimationDuration = res.getInteger(R.integer.swipe_duration_ms);
final ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
mTouchSlop = viewConfiguration.getScaledPagingTouchSlop();
mMaximumFlingVelocity = Math.min(
viewConfiguration.getScaledMaximumFlingVelocity(),
res.getInteger(R.integer.swipe_max_fling_velocity_px_per_s));
mMinimumFlingVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
}
@Override
public boolean onInterceptTouchEvent(final RecyclerView recyclerView, final MotionEvent event) {
if (event.getPointerCount() > 1) {
// Ignore subsequent pointers.
return false;
}
// We are not yet tracking a swipe gesture. Begin detection by spying on
// touch events bubbling down to our children.
final int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN:
if (!hasGestureSwipeTarget()) {
onGestureStart();
mVelocityTracker.addMovement(event);
mInitialX = event.getX();
mInitialY = event.getY();
final View viewAtPoint = mRecyclerView.findChildViewUnder(mInitialX, mInitialY);
final ConversationListItemView child = (ConversationListItemView) viewAtPoint;
if (viewAtPoint instanceof ConversationListItemView &&
child != null && child.isSwipeAnimatable()) {
// Begin detecting swipe on the target for the rest of the gesture.
mListItemView = child;
if (mListItemView.isAnimating()) {
mListItemView = null;
}
} else {
mListItemView = null;
}
}
break;
case MotionEvent.ACTION_MOVE:
if (hasValidGestureSwipeTarget()) {
mVelocityTracker.addMovement(event);
final int historicalCount = event.getHistorySize();
// First consume the historical events, then consume the current ones.
for (int i = 0; i < historicalCount + 1; i++) {
float currX;
float currY;
if (i < historicalCount) {
currX = event.getHistoricalX(i);
currY = event.getHistoricalY(i);
} else {
currX = event.getX();
currY = event.getY();
}
final float deltaX = currX - mInitialX;
final float deltaY = currY - mInitialY;
final float absDeltaX = Math.abs(deltaX);
final float absDeltaY = Math.abs(deltaY);
if (!mIsSwiping && absDeltaY > mTouchSlop
&& absDeltaY > (ERROR_FACTOR_MULTIPLIER * absDeltaX)) {
// Stop detecting swipe for the remainder of this gesture.
onGestureEnd();
return false;
}
if (absDeltaX > mTouchSlop) {
// Swipe detected. Return true so we can handle the gesture in
// onTouchEvent.
mIsSwiping = true;
// We don't want to suddenly jump the slop distance.
mInitialX = event.getX();
mInitialY = event.getY();
onSwipeGestureStart(mListItemView);
return true;
}
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (hasGestureSwipeTarget()) {
onGestureEnd();
}
break;
}
// Start intercepting touch events from children if we detect a swipe.
return mIsSwiping;
}
@Override
public void onTouchEvent(final RecyclerView recyclerView, final MotionEvent event) {
// We should only be here if we intercepted the touch due to swipe.
Assert.isTrue(mIsSwiping);
// We are now tracking a swipe gesture.
mVelocityTracker.addMovement(event);
final int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_OUTSIDE:
case MotionEvent.ACTION_MOVE:
if (hasValidGestureSwipeTarget()) {
mListItemView.setSwipeTranslationX(event.getX() - mInitialX);
}
break;
case MotionEvent.ACTION_UP:
if (hasValidGestureSwipeTarget()) {
final float maxVelocity = mMaximumFlingVelocity;
mVelocityTracker.computeCurrentVelocity(UNIT_SECONDS, maxVelocity);
final float velocityX = getLastComputedXVelocity();
final float translationX = mListItemView.getSwipeTranslationX();
int swipeDirection = SWIPE_DIRECTION_NONE;
if (translationX != 0) {
swipeDirection =
translationX > 0 ? SWIPE_DIRECTION_RIGHT : SWIPE_DIRECTION_LEFT;
} else if (velocityX != 0) {
swipeDirection =
velocityX > 0 ? SWIPE_DIRECTION_RIGHT : SWIPE_DIRECTION_LEFT;
}
final boolean fastEnough = isTargetSwipedFastEnough();
final boolean farEnough = isTargetSwipedFarEnough();
final boolean shouldDismiss = (fastEnough || farEnough);
if (shouldDismiss) {
if (fastEnough) {
animateDismiss(mListItemView, velocityX);
} else {
animateDismiss(mListItemView, swipeDirection);
}
} else {
animateRestore(mListItemView, velocityX);
}
onSwipeGestureEnd(mListItemView,
shouldDismiss ? swipeDirection : SWIPE_DIRECTION_NONE);
} else {
onGestureEnd();
}
break;
case MotionEvent.ACTION_CANCEL:
if (hasValidGestureSwipeTarget()) {
animateRestore(mListItemView, 0f);
onSwipeGestureEnd(mListItemView, SWIPE_DIRECTION_NONE);
} else {
onGestureEnd();
}
break;
}
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
/**
* We have started to intercept a series of touch events.
*/
private void onGestureStart() {
mIsSwiping = false;
// Work around bug in RecyclerView that sends two identical ACTION_DOWN
// events to #onInterceptTouchEvent.
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.clear();
}
/**
* The series of touch events has been detected as a swipe.
*
* Now that the gesture is a swipe, we will begin translating the view of the
* given viewHolder.
*/
private void onSwipeGestureStart(final ConversationListItemView itemView) {
mRecyclerView.getParent().requestDisallowInterceptTouchEvent(true);
setHardwareAnimatingLayerType(itemView, ANIMATING);
itemView.setAnimating(true);
}
/**
* The current swipe gesture is complete.
*/
private void onSwipeGestureEnd(final ConversationListItemView itemView,
final int swipeDirection) {
if (swipeDirection == SWIPE_DIRECTION_RIGHT || swipeDirection == SWIPE_DIRECTION_LEFT) {
itemView.onSwipeComplete();
}
// Balances out onSwipeGestureStart.
itemView.setAnimating(false);
onGestureEnd();
}
/**
* The series of touch events has ended in an {@link MotionEvent#ACTION_UP}
* or {@link MotionEvent#ACTION_CANCEL}.
*/
private void onGestureEnd() {
mVelocityTracker.recycle();
mVelocityTracker = null;
mIsSwiping = false;
mListItemView = null;
}
/**
* A swipe animation has started.
*/
private void onSwipeAnimationStart(final ConversationListItemView itemView) {
// Disallow interactions.
itemView.setAnimating(true);
ViewCompat.setHasTransientState(itemView, true);
setHardwareAnimatingLayerType(itemView, ANIMATING);
}
/**
* The swipe animation has ended.
*/
private void onSwipeAnimationEnd(final ConversationListItemView itemView) {
// Restore interactions.
itemView.setAnimating(false);
ViewCompat.setHasTransientState(itemView, false);
setHardwareAnimatingLayerType(itemView, !ANIMATING);
}
/**
* Animate the dismissal of the given item. The given velocityX is taken into consideration for
* the animation duration. Whether the item is dismissed to the left or right is dependent on
* the given velocityX.
*/
private void animateDismiss(final ConversationListItemView itemView, final float velocityX) {
Assert.isTrue(velocityX != 0);
final int direction = velocityX > 0 ? SWIPE_DIRECTION_RIGHT : SWIPE_DIRECTION_LEFT;
animateDismiss(itemView, direction, velocityX);
}
/**
* Animate the dismissal of the given item. The velocityX is assumed to be 0.
*/
private void animateDismiss(final ConversationListItemView itemView, final int swipeDirection) {
animateDismiss(itemView, swipeDirection, 0f);
}
/**
* Animate the dismissal of the given item.
*/
private void animateDismiss(final ConversationListItemView itemView,
final int swipeDirection, final float velocityX) {
Assert.isTrue(swipeDirection != SWIPE_DIRECTION_NONE);
onSwipeAnimationStart(itemView);
final float animateTo = (swipeDirection == SWIPE_DIRECTION_RIGHT) ?
mRecyclerView.getWidth() : -mRecyclerView.getWidth();
final long duration;
if (velocityX != 0) {
final float deltaX = animateTo - itemView.getSwipeTranslationX();
duration = calculateTranslationDuration(deltaX, velocityX);
} else {
duration = mDefaultDismissAnimationDuration;
}
final ObjectAnimator animator = getSwipeTranslationXAnimator(
itemView, animateTo, duration, UiUtils.DEFAULT_INTERPOLATOR);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(final Animator animation) {
onSwipeAnimationEnd(itemView);
}
});
animator.start();
}
/**
* Animate the bounce back of the given item.
*/
private void animateRestore(final ConversationListItemView itemView,
final float velocityX) {
onSwipeAnimationStart(itemView);
final float translationX = itemView.getSwipeTranslationX();
final long duration;
if (velocityX != 0 // Has velocity.
&& velocityX > 0 != translationX > 0) { // Right direction.
duration = calculateTranslationDuration(translationX, velocityX);
} else {
duration = mDefaultRestoreAnimationDuration;
}
final ObjectAnimator animator = getSwipeTranslationXAnimator(
itemView, 0f, duration, UiUtils.DEFAULT_INTERPOLATOR);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(final Animator animation) {
onSwipeAnimationEnd(itemView);
}
});
animator.start();
}
/**
* Create and start an animator that animates the given view's translationX
* from its current value to the value given by animateTo.
*/
private ObjectAnimator getSwipeTranslationXAnimator(final ConversationListItemView itemView,
final float animateTo, final long duration, final TimeInterpolator interpolator) {
final ObjectAnimator animator =
ObjectAnimator.ofFloat(itemView, "swipeTranslationX", animateTo);
animator.setDuration(duration);
animator.setInterpolator(interpolator);
return animator;
}
/**
* Determine if the swipe has enough velocity to be dismissed.
*/
private boolean isTargetSwipedFastEnough() {
final float velocityX = getLastComputedXVelocity();
final float velocityY = mVelocityTracker.getYVelocity();
final float minVelocity = mMinimumFlingVelocity;
final float translationX = mListItemView.getSwipeTranslationX();
final float width = mListItemView.getWidth();
return (Math.abs(velocityX) > minVelocity) // Fast enough.
&& (Math.abs(velocityX) > Math.abs(velocityY)) // Not unintentional.
&& (velocityX > 0) == (translationX > 0) // Right direction.
&& Math.abs(translationX) >
FLING_PERCENTAGE_OF_WIDTH_TO_DISMISS * width; // Enough movement.
}
/**
* Only used during a swipe gesture. Determine if the swipe has enough distance to be
* dismissed.
*/
private boolean isTargetSwipedFarEnough() {
final float velocityX = getLastComputedXVelocity();
final float translationX = mListItemView.getSwipeTranslationX();
final float width = mListItemView.getWidth();
return (velocityX >= 0) == (translationX > 0) // Right direction.
&& Math.abs(translationX) >
PERCENTAGE_OF_WIDTH_TO_DISMISS * width; // Enough movement.
}
private long calculateTranslationDuration(final float deltaPosition, final float velocity) {
Assert.isTrue(velocity != 0);
final float durationInSeconds = Math.abs(deltaPosition / velocity);
return Math.min((int) (durationInSeconds * UNIT_SECONDS), mMaxTranslationAnimationDuration);
}
private boolean hasGestureSwipeTarget() {
return mListItemView != null;
}
private boolean hasValidGestureSwipeTarget() {
return hasGestureSwipeTarget() && mListItemView.getParent() == mRecyclerView;
}
/**
* Enable a hardware layer for the it view and build that layer.
*/
private void setHardwareAnimatingLayerType(final ConversationListItemView itemView,
final boolean animating) {
if (animating) {
itemView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
if (itemView.getWindowToken() != null) {
itemView.buildLayer();
}
} else {
itemView.setLayerType(View.LAYER_TYPE_NONE, null);
}
}
private float getLastComputedXVelocity() {
return mVelocityTracker.getXVelocity();
}
}
@@ -0,0 +1,81 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.ui.conversationlist;
import android.app.Fragment;
import android.os.Bundle;
import com.android.messaging.datamodel.data.ConversationListData;
import com.android.messaging.datamodel.data.ConversationListItemData;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.ui.BaseBugleActivity;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.ui.conversationlist.ConversationListFragment.ConversationListFragmentHost;
import com.android.messaging.util.Assert;
/**
* An activity that lets the user forward a SMS/MMS message by picking from a conversation in the
* conversation list.
*/
public class ForwardMessageActivity extends BaseBugleActivity
implements ConversationListFragmentHost {
private MessageData mDraftMessage;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final ConversationListFragment fragment =
ConversationListFragment.createForwardMessageConversationListFragment();
getFragmentManager().beginTransaction().add(android.R.id.content, fragment).commit();
mDraftMessage = getIntent().getParcelableExtra(UIIntents.UI_INTENT_EXTRA_DRAFT_DATA);
}
@Override
public void onAttachFragment(final Fragment fragment) {
Assert.isTrue(fragment instanceof ConversationListFragment);
final ConversationListFragment clf = (ConversationListFragment) fragment;
clf.setHost(this);
}
@Override
public void onConversationClick(final ConversationListData listData,
final ConversationListItemData conversationListItemData,
final boolean isLongClick, final ConversationListItemView converastionView) {
UIIntents.get().launchConversationActivity(
this, conversationListItemData.getConversationId(), mDraftMessage);
}
@Override
public void onCreateConversationClick() {
UIIntents.get().launchCreateNewConversationActivity(this, mDraftMessage);
}
@Override
public boolean isConversationSelected(final String conversationId) {
return false;
}
@Override
public boolean isSwipeAnimatable() {
return false;
}
@Override
public boolean isSelectionMode() {
return false;
}
}
@@ -0,0 +1,219 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.ui.conversationlist;
import android.support.v4.util.ArrayMap;
import android.text.TextUtils;
import android.view.ActionMode;
import android.view.ActionMode.Callback;
import android.view.Menu;
import android.view.MenuItem;
import com.android.messaging.R;
import com.android.messaging.datamodel.data.ConversationListData;
import com.android.messaging.datamodel.data.ConversationListItemData;
import com.android.messaging.util.Assert;
import java.util.Collection;
import java.util.HashSet;
public class MultiSelectActionModeCallback implements Callback {
private HashSet<String> mBlockedSet;
public interface Listener {
void onActionBarDelete(Collection<SelectedConversation> conversations);
void onActionBarArchive(Iterable<SelectedConversation> conversations,
boolean isToArchive);
void onActionBarNotification(Iterable<SelectedConversation> conversations,
boolean isNotificationOn);
void onActionBarAddContact(final SelectedConversation conversation);
void onActionBarBlock(final SelectedConversation conversation);
void onActionBarHome();
}
static class SelectedConversation {
public final String conversationId;
public final long timestamp;
public final String icon;
public final String otherParticipantNormalizedDestination;
public final CharSequence participantLookupKey;
public final boolean isGroup;
public final boolean isArchived;
public final boolean notificationEnabled;
public SelectedConversation(ConversationListItemData data) {
conversationId = data.getConversationId();
timestamp = data.getTimestamp();
icon = data.getIcon();
otherParticipantNormalizedDestination = data.getOtherParticipantNormalizedDestination();
participantLookupKey = data.getParticipantLookupKey();
isGroup = data.getIsGroup();
isArchived = data.getIsArchived();
notificationEnabled = data.getNotificationEnabled();
}
}
private final ArrayMap<String, SelectedConversation> mSelectedConversations;
private Listener mListener;
private MenuItem mArchiveMenuItem;
private MenuItem mUnarchiveMenuItem;
private MenuItem mAddContactMenuItem;
private MenuItem mBlockMenuItem;
private MenuItem mNotificationOnMenuItem;
private MenuItem mNotificationOffMenuItem;
private boolean mHasInflated;
public MultiSelectActionModeCallback(final Listener listener) {
mListener = listener;
mSelectedConversations = new ArrayMap<>();
}
@Override
public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
actionMode.getMenuInflater().inflate(R.menu.conversation_list_fragment_select_menu, menu);
mArchiveMenuItem = menu.findItem(R.id.action_archive);
mUnarchiveMenuItem = menu.findItem(R.id.action_unarchive);
mAddContactMenuItem = menu.findItem(R.id.action_add_contact);
mBlockMenuItem = menu.findItem(R.id.action_block);
mNotificationOffMenuItem = menu.findItem(R.id.action_notification_off);
mNotificationOnMenuItem = menu.findItem(R.id.action_notification_on);
mHasInflated = true;
updateActionIconsVisiblity();
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
return true;
}
@Override
public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
switch(menuItem.getItemId()) {
case R.id.action_delete:
mListener.onActionBarDelete(mSelectedConversations.values());
return true;
case R.id.action_archive:
mListener.onActionBarArchive(mSelectedConversations.values(), true);
return true;
case R.id.action_unarchive:
mListener.onActionBarArchive(mSelectedConversations.values(), false);
return true;
case R.id.action_notification_off:
mListener.onActionBarNotification(mSelectedConversations.values(), false);
return true;
case R.id.action_notification_on:
mListener.onActionBarNotification(mSelectedConversations.values(), true);
return true;
case R.id.action_add_contact:
Assert.isTrue(mSelectedConversations.size() == 1);
mListener.onActionBarAddContact(mSelectedConversations.valueAt(0));
return true;
case R.id.action_block:
Assert.isTrue(mSelectedConversations.size() == 1);
mListener.onActionBarBlock(mSelectedConversations.valueAt(0));
return true;
case android.R.id.home:
mListener.onActionBarHome();
return true;
default:
return false;
}
}
@Override
public void onDestroyActionMode(ActionMode actionMode) {
mListener = null;
mSelectedConversations.clear();
mHasInflated = false;
}
public void toggleSelect(final ConversationListData listData,
final ConversationListItemData conversationListItemData) {
Assert.notNull(conversationListItemData);
mBlockedSet = listData.getBlockedParticipants();
final String id = conversationListItemData.getConversationId();
if (mSelectedConversations.containsKey(id)) {
mSelectedConversations.remove(id);
} else {
mSelectedConversations.put(id, new SelectedConversation(conversationListItemData));
}
if (mSelectedConversations.isEmpty()) {
mListener.onActionBarHome();
} else {
updateActionIconsVisiblity();
}
}
public boolean isSelected(final String selectedId) {
return mSelectedConversations.containsKey(selectedId);
}
private void updateActionIconsVisiblity() {
if (!mHasInflated) {
return;
}
if (mSelectedConversations.size() == 1) {
final SelectedConversation conversation = mSelectedConversations.valueAt(0);
// The look up key is a key given to us by contacts app, so if we have a look up key,
// we know that the participant is already in contacts.
final boolean isInContacts = !TextUtils.isEmpty(conversation.participantLookupKey);
mAddContactMenuItem.setVisible(!conversation.isGroup && !isInContacts);
// ParticipantNormalizedDestination is always null for group conversations.
final String otherParticipant = conversation.otherParticipantNormalizedDestination;
mBlockMenuItem.setVisible(otherParticipant != null
&& !mBlockedSet.contains(otherParticipant));
} else {
mBlockMenuItem.setVisible(false);
mAddContactMenuItem.setVisible(false);
}
boolean hasCurrentlyArchived = false;
boolean hasCurrentlyUnarchived = false;
boolean hasCurrentlyOnNotification = false;
boolean hasCurrentlyOffNotification = false;
final Iterable<SelectedConversation> conversations = mSelectedConversations.values();
for (final SelectedConversation conversation : conversations) {
if (conversation.notificationEnabled) {
hasCurrentlyOnNotification = true;
} else {
hasCurrentlyOffNotification = true;
}
if (conversation.isArchived) {
hasCurrentlyArchived = true;
} else {
hasCurrentlyUnarchived = true;
}
// If we found at least one of each example we don't need to keep looping.
if (hasCurrentlyOffNotification && hasCurrentlyOnNotification &&
hasCurrentlyArchived && hasCurrentlyUnarchived) {
break;
}
}
// If we have notification off conversations we show on button, if we have notification on
// conversation we show off button. We can show both if we have a mixture.
mNotificationOffMenuItem.setVisible(hasCurrentlyOnNotification);
mNotificationOnMenuItem.setVisible(hasCurrentlyOffNotification);
mArchiveMenuItem.setVisible(hasCurrentlyUnarchived);
mUnarchiveMenuItem.setVisible(hasCurrentlyArchived);
}
}
@@ -0,0 +1,177 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.ui.conversationlist;
import android.app.Fragment;
import android.content.ContentResolver;
import android.content.Intent;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import com.android.messaging.Factory;
import com.android.messaging.datamodel.data.ConversationListItemData;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.datamodel.data.PendingAttachmentData;
import com.android.messaging.ui.BaseBugleActivity;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.util.Assert;
import com.android.messaging.util.ContentType;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.MediaMetadataRetrieverWrapper;
import java.io.IOException;
import java.util.ArrayList;
public class ShareIntentActivity extends BaseBugleActivity implements
ShareIntentFragment.HostInterface {
private MessageData mDraftMessage;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Intent intent = getIntent();
if (Intent.ACTION_SEND.equals(intent.getAction()) &&
(!TextUtils.isEmpty(intent.getStringExtra("address")) ||
!TextUtils.isEmpty(intent.getStringExtra(Intent.EXTRA_EMAIL)))) {
// This is really more like a SENDTO intent because a destination is supplied.
// It's coming through the SEND intent because that's the intent that is used
// when invoking the chooser with Intent.createChooser().
final Intent convIntent = UIIntents.get().getLaunchConversationActivityIntent(this);
// Copy the important items from the original intent to the new intent.
convIntent.putExtras(intent);
convIntent.setAction(Intent.ACTION_SENDTO);
convIntent.setDataAndType(intent.getData(), intent.getType());
// We have to fire off the intent and finish before trying to show the fragment,
// otherwise we get some flashing.
startActivity(convIntent);
finish();
return;
}
new ShareIntentFragment().show(getFragmentManager(), "ShareIntentFragment");
}
@Override
public void onAttachFragment(final Fragment fragment) {
final Intent intent = getIntent();
final String action = intent.getAction();
if (Intent.ACTION_SEND.equals(action)) {
final Uri contentUri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
final String contentType = extractContentType(contentUri, intent.getType());
if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) {
LogUtil.d(LogUtil.BUGLE_TAG, String.format(
"onAttachFragment: contentUri=%s, intent.getType()=%s, inferredType=%s",
contentUri, intent.getType(), contentType));
}
if (ContentType.TEXT_PLAIN.equals(contentType)) {
final String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
if (sharedText != null) {
mDraftMessage = MessageData.createSharedMessage(sharedText);
} else {
mDraftMessage = null;
}
} else if (ContentType.isImageType(contentType) ||
ContentType.isVCardType(contentType) ||
ContentType.isAudioType(contentType) ||
ContentType.isVideoType(contentType)) {
if (contentUri != null) {
mDraftMessage = MessageData.createSharedMessage(null);
addSharedImagePartToDraft(contentType, contentUri);
} else {
mDraftMessage = null;
}
} else {
// Unsupported content type.
Assert.fail("Unsupported shared content type for " + contentUri + ": " + contentType
+ " (" + intent.getType() + ")");
}
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
final String contentType = intent.getType();
if (ContentType.isImageType(contentType)) {
// Handle sharing multiple images.
final ArrayList<Uri> imageUris = intent.getParcelableArrayListExtra(
Intent.EXTRA_STREAM);
if (imageUris != null && imageUris.size() > 0) {
mDraftMessage = MessageData.createSharedMessage(null);
for (final Uri imageUri : imageUris) {
final String actualContentType = extractContentType(imageUri, contentType);
addSharedImagePartToDraft(actualContentType, imageUri);
}
} else {
mDraftMessage = null;
}
} else {
// Unsupported content type.
Assert.fail("Unsupported shared content type: " + contentType);
}
} else {
// Unsupported action.
Assert.fail("Unsupported action type for sharing: " + action);
}
}
private static String extractContentType(final Uri uri, final String contentType) {
if (uri == null) {
return contentType;
}
// First try looking at file extension. This is less reliable in some ways but it's
// recommended by
// https://developer.android.com/training/secure-file-sharing/retrieve-info.html
// Some implementations of MediaMetadataRetriever get things horribly
// wrong for common formats such as jpeg (reports as video/ffmpeg)
final ContentResolver resolver = Factory.get().getApplicationContext().getContentResolver();
final String typeFromExtension = resolver.getType(uri);
if (typeFromExtension != null) {
return typeFromExtension;
}
final MediaMetadataRetrieverWrapper retriever = new MediaMetadataRetrieverWrapper();
try {
retriever.setDataSource(uri);
final String extractedType = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_MIMETYPE);
if (extractedType != null) {
return extractedType;
}
} catch (final IOException e) {
LogUtil.i(LogUtil.BUGLE_TAG, "Could not determine type of " + uri, e);
} finally {
retriever.release();
}
return contentType;
}
private void addSharedImagePartToDraft(final String contentType, final Uri imageUri) {
mDraftMessage.addPart(PendingAttachmentData.createPendingAttachmentData(contentType,
imageUri));
}
@Override
public void onConversationClick(final ConversationListItemData conversationListItemData) {
UIIntents.get().launchConversationActivity(
this, conversationListItemData.getConversationId(), mDraftMessage);
finish();
}
@Override
public void onCreateConversationClick() {
UIIntents.get().launchCreateNewConversationActivity(this, mDraftMessage);
finish();
}
}
@@ -0,0 +1,138 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.ui.conversationlist;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import com.android.messaging.R;
import com.android.messaging.datamodel.data.ConversationListItemData;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.datamodel.data.PersonItemData;
import com.android.messaging.ui.CursorRecyclerAdapter;
import com.android.messaging.ui.PersonItemView;
import com.android.messaging.ui.PersonItemView.PersonItemViewListener;
import com.android.messaging.util.PhoneUtils;
/**
* Turn conversation rows into PeopleItemViews
*/
public class ShareIntentAdapter
extends CursorRecyclerAdapter<ShareIntentAdapter.ShareIntentViewHolder> {
public interface HostInterface {
void onConversationClicked(final ConversationListItemData conversationListItemData);
}
private final HostInterface mHostInterface;
public ShareIntentAdapter(final Context context, final Cursor cursor,
final HostInterface hostInterface) {
super(context, cursor, 0);
mHostInterface = hostInterface;
setHasStableIds(true);
}
@Override
public void bindViewHolder(final ShareIntentViewHolder holder, final Context context,
final Cursor cursor) {
holder.bind(cursor);
}
@Override
public ShareIntentViewHolder createViewHolder(final Context context,
final ViewGroup parent, final int viewType) {
final PersonItemView itemView = (PersonItemView) LayoutInflater.from(context).inflate(
R.layout.people_list_item_view, null);
return new ShareIntentViewHolder(itemView);
}
/**
* Holds a PersonItemView and keeps it synced with a ConversationListItemData.
*/
public class ShareIntentViewHolder extends RecyclerView.ViewHolder implements
PersonItemView.PersonItemViewListener {
private final ConversationListItemData mData = new ConversationListItemData();
private final PersonItemData mItemData = new PersonItemData() {
@Override
public Uri getAvatarUri() {
return mData.getIcon() == null ? null : Uri.parse(mData.getIcon());
}
@Override
public String getDisplayName() {
return mData.getName();
}
@Override
public String getDetails() {
final String conversationName = mData.getName();
final String conversationPhone = PhoneUtils.getDefault().formatForDisplay(
mData.getOtherParticipantNormalizedDestination());
if (conversationPhone == null || conversationPhone.equals(conversationName)) {
return null;
}
return conversationPhone;
}
@Override
public Intent getClickIntent() {
return null;
}
@Override
public long getContactId() {
return ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED;
}
@Override
public String getLookupKey() {
return null;
}
@Override
public String getNormalizedDestination() {
return null;
}
};
public ShareIntentViewHolder(final PersonItemView itemView) {
super(itemView);
itemView.setListener(this);
}
public void bind(Cursor cursor) {
mData.bind(cursor);
((PersonItemView) itemView).bind(mItemData);
}
@Override
public void onPersonClicked(PersonItemData data) {
mHostInterface.onConversationClicked(mData);
}
@Override
public boolean onPersonLongClicked(PersonItemData data) {
return false;
}
}
}
@@ -0,0 +1,163 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.ui.conversationlist;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.android.messaging.R;
import com.android.messaging.datamodel.binding.Binding;
import com.android.messaging.datamodel.binding.BindingBase;
import com.android.messaging.datamodel.data.ConversationListData;
import com.android.messaging.datamodel.data.ConversationListItemData;
import com.android.messaging.datamodel.data.ConversationListData.ConversationListDataListener;
import com.android.messaging.ui.ListEmptyView;
import com.android.messaging.datamodel.DataModel;
/**
* Allow user to pick conversation to which an incoming attachment will be shared.
*/
public class ShareIntentFragment extends DialogFragment implements ConversationListDataListener,
ShareIntentAdapter.HostInterface {
public static final String HIDE_NEW_CONVERSATION_BUTTON_KEY = "hide_conv_button_key";
public interface HostInterface {
public void onConversationClick(final ConversationListItemData conversationListItemData);
public void onCreateConversationClick();
}
private final Binding<ConversationListData> mListBinding = BindingBase.createBinding(this);
private RecyclerView mRecyclerView;
private ListEmptyView mEmptyListMessageView;
private ShareIntentAdapter mAdapter;
private HostInterface mHost;
private boolean mDismissed;
/**
* {@inheritDoc} from Fragment
*/
@Override
public Dialog onCreateDialog(final Bundle bundle) {
final Activity activity = getActivity();
final LayoutInflater inflater = activity.getLayoutInflater();
View view = inflater.inflate(R.layout.share_intent_conversation_list_view, null);
mEmptyListMessageView = (ListEmptyView) view.findViewById(R.id.no_conversations_view);
mEmptyListMessageView.setImageHint(R.drawable.ic_oobe_conv_list);
// The default behavior for default layout param generation by LinearLayoutManager is to
// provide width and height of WRAP_CONTENT, but this is not desirable for
// ShareIntentFragment; the view in each row should be a width of MATCH_PARENT so that
// the entire row is tappable.
final LinearLayoutManager manager = new LinearLayoutManager(activity) {
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
};
mListBinding.getData().init(getLoaderManager(), mListBinding);
mAdapter = new ShareIntentAdapter(activity, null, this);
mRecyclerView = (RecyclerView) view.findViewById(android.R.id.list);
mRecyclerView.setLayoutManager(manager);
mRecyclerView.setHasFixedSize(true);
mRecyclerView.setAdapter(mAdapter);
final Builder dialogBuilder = new AlertDialog.Builder(activity)
.setView(view)
.setTitle(R.string.share_intent_activity_label);
final Bundle arguments = getArguments();
if (arguments == null || !arguments.getBoolean(HIDE_NEW_CONVERSATION_BUTTON_KEY)) {
dialogBuilder.setPositiveButton(R.string.share_new_message, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mDismissed = true;
mHost.onCreateConversationClick();
}
});
}
return dialogBuilder.setNegativeButton(R.string.share_cancel, null)
.create();
}
@Override
public void onDismiss(DialogInterface dialog) {
if (!mDismissed) {
final Activity activity = getActivity();
if (activity != null) {
activity.finish();
}
}
}
/**
* {@inheritDoc} from Fragment
*/
@Override
public void onDestroy() {
super.onDestroy();
mListBinding.unbind();
}
@Override
public void onAttach(final Activity activity) {
super.onAttach(activity);
if (activity instanceof HostInterface) {
mHost = (HostInterface) activity;
}
mListBinding.bind(DataModel.get().createConversationListData(activity, this, false));
}
@Override
public void onConversationListCursorUpdated(final ConversationListData data,
final Cursor cursor) {
mListBinding.ensureBound(data);
mAdapter.swapCursor(cursor);
updateEmptyListUi(cursor == null || cursor.getCount() == 0);
}
/**
* {@inheritDoc} from SharIntentItemView.HostInterface
*/
@Override
public void onConversationClicked(final ConversationListItemData conversationListItemData) {
mHost.onConversationClick(conversationListItemData);
}
// Show and hide empty list UI as needed with appropriate text based on view specifics
private void updateEmptyListUi(final boolean isEmpty) {
if (isEmpty) {
mEmptyListMessageView.setTextHint(R.string.conversation_list_empty_text);
mEmptyListMessageView.setVisibility(View.VISIBLE);
} else {
mEmptyListMessageView.setVisibility(View.GONE);
}
}
@Override
public void setBlockedParticipantsAvailable(boolean blockedAvailable) {
}
}