530 lines
19 KiB
Java
530 lines
19 KiB
Java
/*
|
|
* Copyright (C) 2015 The Android Open Source Project
|
|
* Copyright (C) 2024 The LineageOS 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.conversation;
|
|
|
|
import android.content.Context;
|
|
import android.os.Bundle;
|
|
import android.widget.EditText;
|
|
|
|
import androidx.appcompat.app.ActionBar;
|
|
import androidx.fragment.app.FragmentManager;
|
|
|
|
import com.android.messaging.R;
|
|
import com.android.messaging.datamodel.binding.BindingBase;
|
|
import com.android.messaging.datamodel.binding.ImmutableBindingRef;
|
|
import com.android.messaging.datamodel.data.ConversationData;
|
|
import com.android.messaging.datamodel.data.ConversationData.ConversationDataListener;
|
|
import com.android.messaging.datamodel.data.ConversationData.SimpleConversationDataListener;
|
|
import com.android.messaging.datamodel.data.DraftMessageData;
|
|
import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageSubscriptionDataProvider;
|
|
import com.android.messaging.datamodel.data.MessagePartData;
|
|
import com.android.messaging.datamodel.data.PendingAttachmentData;
|
|
import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry;
|
|
import com.android.messaging.ui.ConversationDrawables;
|
|
import com.android.messaging.ui.mediapicker.MediaPicker;
|
|
import com.android.messaging.ui.mediapicker.MediaPicker.MediaPickerListener;
|
|
import com.android.messaging.util.Assert;
|
|
import com.android.messaging.util.ImeUtil;
|
|
import com.android.messaging.util.ImeUtil.ImeStateHost;
|
|
|
|
import java.util.Collection;
|
|
|
|
/**
|
|
* Manages showing/hiding/persisting different mutually exclusive UI components nested in
|
|
* ConversationFragment that take user inputs, i.e. media picker, SIM selector and
|
|
* IME keyboard (the IME keyboard is not owned by Bugle, but we try to model it the same way
|
|
* as the other components).
|
|
*/
|
|
public class ConversationInputManager implements ConversationInput.ConversationInputBase {
|
|
/**
|
|
* The host component where all input components are contained. This is typically the
|
|
* conversation fragment but may be mocked in test code.
|
|
*/
|
|
public interface ConversationInputHost extends DraftMessageSubscriptionDataProvider {
|
|
void invalidateActionBar();
|
|
void setOptionsMenuVisibility(boolean visible);
|
|
void dismissActionMode();
|
|
void selectSim(SubscriptionListEntry subscriptionData);
|
|
void onStartComposeMessage();
|
|
SimSelectorView getSimSelectorView();
|
|
MediaPicker createMediaPicker();
|
|
void showHideSimSelector(boolean show);
|
|
int getSimSelectorItemLayoutId();
|
|
}
|
|
|
|
/**
|
|
* The "sink" component where all inputs components will direct the user inputs to. This is
|
|
* typically the ComposeMessageView but may be mocked in test code.
|
|
*/
|
|
public interface ConversationInputSink {
|
|
void onMediaItemsSelected(Collection<MessagePartData> items);
|
|
void onMediaItemsUnselected(MessagePartData item);
|
|
void onPendingAttachmentAdded(PendingAttachmentData pendingItem);
|
|
void resumeComposeMessage();
|
|
EditText getComposeEditText();
|
|
void setAccessibility(boolean enabled);
|
|
}
|
|
|
|
private final ConversationInputHost mHost;
|
|
private final ConversationInputSink mSink;
|
|
|
|
/** Dependencies injected from the host during construction */
|
|
private final FragmentManager mFragmentManager;
|
|
private final Context mContext;
|
|
private final ImeStateHost mImeStateHost;
|
|
private final ImmutableBindingRef<ConversationData> mConversationDataModel;
|
|
private final ImmutableBindingRef<DraftMessageData> mDraftDataModel;
|
|
|
|
private final ConversationInput[] mInputs;
|
|
private final ConversationMediaPicker mMediaInput;
|
|
private final ConversationSimSelector mSimInput;
|
|
private final ConversationImeKeyboard mImeInput;
|
|
private int mUpdateCount;
|
|
|
|
private final ImeUtil.ImeStateObserver mImeStateObserver = new ImeUtil.ImeStateObserver() {
|
|
@Override
|
|
public void onImeStateChanged(final boolean imeOpen) {
|
|
mImeInput.onVisibilityChanged(imeOpen);
|
|
}
|
|
};
|
|
|
|
private final ConversationDataListener mDataListener = new SimpleConversationDataListener() {
|
|
@Override
|
|
public void onConversationParticipantDataLoaded(ConversationData data) {
|
|
mConversationDataModel.ensureBound(data);
|
|
}
|
|
|
|
@Override
|
|
public void onSubscriptionListDataLoaded(ConversationData data) {
|
|
mConversationDataModel.ensureBound(data);
|
|
mSimInput.onSubscriptionListDataLoaded(data.getSubscriptionListData());
|
|
}
|
|
};
|
|
|
|
public ConversationInputManager(
|
|
final Context context,
|
|
final ConversationInputHost host,
|
|
final ConversationInputSink sink,
|
|
final ImeStateHost imeStateHost,
|
|
final FragmentManager fm,
|
|
final BindingBase<ConversationData> conversationDataModel,
|
|
final BindingBase<DraftMessageData> draftDataModel,
|
|
final Bundle savedState) {
|
|
mHost = host;
|
|
mSink = sink;
|
|
mFragmentManager = fm;
|
|
mContext = context;
|
|
mImeStateHost = imeStateHost;
|
|
mConversationDataModel = BindingBase.createBindingReference(conversationDataModel);
|
|
mDraftDataModel = BindingBase.createBindingReference(draftDataModel);
|
|
|
|
// Register listeners on dependencies.
|
|
mImeStateHost.registerImeStateObserver(mImeStateObserver);
|
|
mConversationDataModel.getData().addConversationDataListener(mDataListener);
|
|
|
|
// Initialize the inputs
|
|
mMediaInput = new ConversationMediaPicker(this);
|
|
mSimInput = new SimSelector(this);
|
|
mImeInput = new ConversationImeKeyboard(this, mImeStateHost.isImeOpen());
|
|
mInputs = new ConversationInput[] { mMediaInput, mSimInput, mImeInput };
|
|
|
|
if (savedState != null) {
|
|
for (int i = 0; i < mInputs.length; i++) {
|
|
mInputs[i].restoreState(savedState);
|
|
}
|
|
}
|
|
updateHostOptionsMenu();
|
|
}
|
|
|
|
public void onDetach() {
|
|
mImeStateHost.unregisterImeStateObserver(mImeStateObserver);
|
|
// Don't need to explicitly unregister for data model events. It will unregister all
|
|
// listeners automagically on unbind.
|
|
}
|
|
|
|
public void onSaveInputState(final Bundle savedState) {
|
|
for (int i = 0; i < mInputs.length; i++) {
|
|
mInputs[i].saveState(savedState);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public String getInputStateKey(final ConversationInput input) {
|
|
return input.getClass().getCanonicalName() + "_savedstate_";
|
|
}
|
|
|
|
public boolean onBackPressed() {
|
|
for (int i = 0; i < mInputs.length; i++) {
|
|
if (mInputs[i].onBackPressed()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public boolean onNavigationUpPressed() {
|
|
for (int i = 0; i < mInputs.length; i++) {
|
|
if (mInputs[i].onNavigationUpPressed()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public void resetMediaPickerState() {
|
|
mMediaInput.resetViewHolderState();
|
|
}
|
|
|
|
public void showHideMediaPicker(final boolean show, final boolean animate) {
|
|
showHideInternal(mMediaInput, show, animate);
|
|
}
|
|
|
|
/**
|
|
* Show or hide the sim selector
|
|
* @param show visibility
|
|
* @param animate whether to animate the change in visibility
|
|
* @return true if the state of the visibility was changed
|
|
*/
|
|
public boolean showHideSimSelector(final boolean show, final boolean animate) {
|
|
return showHideInternal(mSimInput, show, animate);
|
|
}
|
|
|
|
public void showHideImeKeyboard(final boolean show, final boolean animate) {
|
|
showHideInternal(mImeInput, show, animate);
|
|
}
|
|
|
|
public void hideAllInputs(final boolean animate) {
|
|
beginUpdate();
|
|
for (int i = 0; i < mInputs.length; i++) {
|
|
showHideInternal(mInputs[i], false, animate);
|
|
}
|
|
endUpdate();
|
|
}
|
|
|
|
/**
|
|
* Toggle the visibility of the sim selector.
|
|
* @return true if the view is now shown, false if it now hidden
|
|
*/
|
|
public boolean toggleSimSelector(final boolean animate, final SubscriptionListEntry subEntry) {
|
|
mSimInput.setSelected(subEntry);
|
|
return mSimInput.toggle(animate);
|
|
}
|
|
|
|
public boolean updateActionBar(final ActionBar actionBar) {
|
|
for (int i = 0; i < mInputs.length; i++) {
|
|
if (mInputs[i].mShowing) {
|
|
return mInputs[i].updateActionBar(actionBar);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* returns true if the state of the visibility was actually changed
|
|
*/
|
|
@Override
|
|
public boolean showHideInternal(final ConversationInput target, final boolean show,
|
|
final boolean animate) {
|
|
if (!mConversationDataModel.isBound()) {
|
|
return false;
|
|
}
|
|
|
|
if (target.mShowing == show) {
|
|
return false;
|
|
}
|
|
beginUpdate();
|
|
boolean success;
|
|
if (!show) {
|
|
success = target.hide(animate);
|
|
} else {
|
|
success = target.show(animate);
|
|
}
|
|
|
|
if (success) {
|
|
target.onVisibilityChanged(show);
|
|
}
|
|
endUpdate();
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public void handleOnShow(final ConversationInput target) {
|
|
if (!mConversationDataModel.isBound()) {
|
|
return;
|
|
}
|
|
beginUpdate();
|
|
|
|
// All inputs are mutually exclusive. Showing one will hide everything else.
|
|
// The one exception, is that the keyboard and location media chooser can be open at the
|
|
// time to enable searching within that chooser
|
|
for (int i = 0; i < mInputs.length; i++) {
|
|
final ConversationInput currInput = mInputs[i];
|
|
if (currInput != target) {
|
|
// TODO : If there's more exceptions we will want to make this more
|
|
// generic
|
|
if (currInput instanceof ConversationMediaPicker &&
|
|
target instanceof ConversationImeKeyboard &&
|
|
mMediaInput.getExistingOrCreateMediaPicker() != null &&
|
|
mMediaInput.getExistingOrCreateMediaPicker().canShowIme()) {
|
|
// Allow the keyboard and location mediaPicker to be open at the same time,
|
|
// but ensure the media picker is full screen to allow enough room
|
|
mMediaInput.getExistingOrCreateMediaPicker().setFullScreen(true);
|
|
continue;
|
|
}
|
|
showHideInternal(currInput, false /* show */, false /* animate */);
|
|
}
|
|
}
|
|
// Always dismiss action mode on show.
|
|
mHost.dismissActionMode();
|
|
// Invoking any non-keyboard input UI is treated as starting message compose.
|
|
if (target != mImeInput) {
|
|
mHost.onStartComposeMessage();
|
|
}
|
|
endUpdate();
|
|
}
|
|
|
|
@Override
|
|
public void beginUpdate() {
|
|
mUpdateCount++;
|
|
}
|
|
|
|
@Override
|
|
public void endUpdate() {
|
|
Assert.isTrue(mUpdateCount > 0);
|
|
if (--mUpdateCount == 0) {
|
|
// Always try to update the host action bar after every update cycle.
|
|
mHost.invalidateActionBar();
|
|
}
|
|
}
|
|
|
|
private void updateHostOptionsMenu() {
|
|
mHost.setOptionsMenuVisibility(!mMediaInput.isOpen());
|
|
}
|
|
|
|
/**
|
|
* Manages showing/hiding the media picker in conversation.
|
|
*/
|
|
private class ConversationMediaPicker extends ConversationInput {
|
|
public ConversationMediaPicker(ConversationInputBase baseHost) {
|
|
super(baseHost, false);
|
|
}
|
|
|
|
private MediaPicker mMediaPicker;
|
|
|
|
@Override
|
|
public boolean show(boolean animate) {
|
|
if (mMediaPicker == null) {
|
|
mMediaPicker = getExistingOrCreateMediaPicker();
|
|
setConversationThemeColor(ConversationDrawables.get().getConversationThemeColor());
|
|
mMediaPicker.setSubscriptionDataProvider(mHost);
|
|
mMediaPicker.setDraftMessageDataModel(mDraftDataModel);
|
|
mMediaPicker.setListener(new MediaPickerListener() {
|
|
@Override
|
|
public void onOpened() {
|
|
handleStateChange();
|
|
}
|
|
|
|
@Override
|
|
public void onFullScreenChanged(boolean fullScreen) {
|
|
// When we're full screen, we want to disable accessibility on the
|
|
// ComposeMessageView controls (attach button, message input, sim chooser)
|
|
// that are hiding underneath the action bar.
|
|
mSink.setAccessibility(!fullScreen /*enabled*/);
|
|
handleStateChange();
|
|
}
|
|
|
|
@Override
|
|
public void onDismissed() {
|
|
// Re-enable accessibility on all controls now that the media picker is
|
|
// going away.
|
|
mSink.setAccessibility(true /*enabled*/);
|
|
handleStateChange();
|
|
}
|
|
|
|
private void handleStateChange() {
|
|
onVisibilityChanged(isOpen());
|
|
mHost.invalidateActionBar();
|
|
updateHostOptionsMenu();
|
|
}
|
|
|
|
@Override
|
|
public void onItemsSelected(final Collection<MessagePartData> items,
|
|
final boolean resumeCompose) {
|
|
mSink.onMediaItemsSelected(items);
|
|
mHost.invalidateActionBar();
|
|
if (resumeCompose) {
|
|
mSink.resumeComposeMessage();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onItemUnselected(final MessagePartData item) {
|
|
mSink.onMediaItemsUnselected(item);
|
|
mHost.invalidateActionBar();
|
|
}
|
|
|
|
@Override
|
|
public void onConfirmItemSelection() {
|
|
mSink.resumeComposeMessage();
|
|
}
|
|
|
|
@Override
|
|
public void onPendingItemAdded(final PendingAttachmentData pendingItem) {
|
|
mSink.onPendingAttachmentAdded(pendingItem);
|
|
}
|
|
|
|
@Override
|
|
public void onChooserSelected(final int chooserIndex) {
|
|
mHost.invalidateActionBar();
|
|
mHost.dismissActionMode();
|
|
}
|
|
});
|
|
}
|
|
|
|
mMediaPicker.open(MediaPicker.MEDIA_TYPE_DEFAULT, animate);
|
|
|
|
return isOpen();
|
|
}
|
|
|
|
@Override
|
|
public boolean hide(boolean animate) {
|
|
if (mMediaPicker != null) {
|
|
mMediaPicker.dismiss(animate);
|
|
}
|
|
return !isOpen();
|
|
}
|
|
|
|
public void resetViewHolderState() {
|
|
if (mMediaPicker != null) {
|
|
mMediaPicker.resetViewHolderState();
|
|
}
|
|
}
|
|
|
|
public void setConversationThemeColor(final int themeColor) {
|
|
if (mMediaPicker != null) {
|
|
mMediaPicker.setConversationThemeColor(themeColor);
|
|
}
|
|
}
|
|
|
|
private boolean isOpen() {
|
|
return (mMediaPicker != null && mMediaPicker.isOpen());
|
|
}
|
|
|
|
private MediaPicker getExistingOrCreateMediaPicker() {
|
|
if (mMediaPicker != null) {
|
|
return mMediaPicker;
|
|
}
|
|
MediaPicker mediaPicker = (MediaPicker)
|
|
mFragmentManager.findFragmentByTag(MediaPicker.FRAGMENT_TAG);
|
|
if (mediaPicker == null) {
|
|
mediaPicker = mHost.createMediaPicker();
|
|
if (mediaPicker == null) {
|
|
return null; // this use of ComposeMessageView doesn't support media picking
|
|
}
|
|
mFragmentManager.beginTransaction().replace(
|
|
R.id.mediapicker_container,
|
|
mediaPicker,
|
|
MediaPicker.FRAGMENT_TAG).commit();
|
|
}
|
|
return mediaPicker;
|
|
}
|
|
|
|
@Override
|
|
public boolean updateActionBar(ActionBar actionBar) {
|
|
if (isOpen()) {
|
|
mMediaPicker.updateActionBar(actionBar);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean onNavigationUpPressed() {
|
|
if (isOpen() && mMediaPicker.isFullScreen()) {
|
|
return onBackPressed();
|
|
}
|
|
return super.onNavigationUpPressed();
|
|
}
|
|
|
|
public boolean onBackPressed() {
|
|
if (mMediaPicker != null && mMediaPicker.onBackPressed()) {
|
|
return true;
|
|
}
|
|
return super.onBackPressed();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Manages showing/hiding the SIM selector in conversation.
|
|
*/
|
|
private class SimSelector extends ConversationSimSelector {
|
|
public SimSelector(ConversationInputBase baseHost) {
|
|
super(baseHost);
|
|
}
|
|
|
|
@Override
|
|
protected SimSelectorView getSimSelectorView() {
|
|
return mHost.getSimSelectorView();
|
|
}
|
|
|
|
@Override
|
|
public int getSimSelectorItemLayoutId() {
|
|
return mHost.getSimSelectorItemLayoutId();
|
|
}
|
|
|
|
@Override
|
|
protected void selectSim(SubscriptionListEntry item) {
|
|
mHost.selectSim(item);
|
|
}
|
|
|
|
@Override
|
|
public boolean show(boolean animate) {
|
|
final boolean result = super.show(animate);
|
|
mHost.showHideSimSelector(true /*show*/);
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public boolean hide(boolean animate) {
|
|
final boolean result = super.hide(animate);
|
|
mHost.showHideSimSelector(false /*show*/);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Manages showing/hiding the IME keyboard in conversation.
|
|
*/
|
|
private class ConversationImeKeyboard extends ConversationInput {
|
|
public ConversationImeKeyboard(ConversationInputBase baseHost, final boolean isShowing) {
|
|
super(baseHost, isShowing);
|
|
}
|
|
|
|
@Override
|
|
public boolean show(boolean animate) {
|
|
ImeUtil.get().showImeKeyboard(mContext, mSink.getComposeEditText());
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean hide(boolean animate) {
|
|
ImeUtil.get().hideImeKeyboard(mContext, mSink.getComposeEditText());
|
|
return true;
|
|
}
|
|
}
|
|
}
|