Initial checkin of AOSP Messaging app.
b/23110861 Change-Id: I9aa980d7569247d6b2ca78f5dcb4502e1eaadb8a
This commit is contained in:
@@ -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) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user