1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.messaging.ui.conversation; 17 18 import android.app.FragmentManager; 19 import android.content.Context; 20 import android.os.Bundle; 21 import android.support.v7.app.ActionBar; 22 import android.widget.EditText; 23 24 import com.android.messaging.R; 25 import com.android.messaging.datamodel.binding.BindingBase; 26 import com.android.messaging.datamodel.binding.ImmutableBindingRef; 27 import com.android.messaging.datamodel.data.ConversationData; 28 import com.android.messaging.datamodel.data.ConversationData.ConversationDataListener; 29 import com.android.messaging.datamodel.data.ConversationData.SimpleConversationDataListener; 30 import com.android.messaging.datamodel.data.DraftMessageData; 31 import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageSubscriptionDataProvider; 32 import com.android.messaging.datamodel.data.MessagePartData; 33 import com.android.messaging.datamodel.data.PendingAttachmentData; 34 import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry; 35 import com.android.messaging.ui.ConversationDrawables; 36 import com.android.messaging.ui.mediapicker.MediaPicker; 37 import com.android.messaging.ui.mediapicker.MediaPicker.MediaPickerListener; 38 import com.android.messaging.util.Assert; 39 import com.android.messaging.util.ImeUtil; 40 import com.android.messaging.util.ImeUtil.ImeStateHost; 41 import com.google.common.annotations.VisibleForTesting; 42 43 import java.util.Collection; 44 45 /** 46 * Manages showing/hiding/persisting different mutually exclusive UI components nested in 47 * ConversationFragment that take user inputs, i.e. media picker, SIM selector and 48 * IME keyboard (the IME keyboard is not owned by Bugle, but we try to model it the same way 49 * as the other components). 50 */ 51 public class ConversationInputManager implements ConversationInput.ConversationInputBase { 52 /** 53 * The host component where all input components are contained. This is typically the 54 * conversation fragment but may be mocked in test code. 55 */ 56 public interface ConversationInputHost extends DraftMessageSubscriptionDataProvider { invalidateActionBar()57 void invalidateActionBar(); setOptionsMenuVisibility(boolean visible)58 void setOptionsMenuVisibility(boolean visible); dismissActionMode()59 void dismissActionMode(); selectSim(SubscriptionListEntry subscriptionData)60 void selectSim(SubscriptionListEntry subscriptionData); onStartComposeMessage()61 void onStartComposeMessage(); getSimSelectorView()62 SimSelectorView getSimSelectorView(); createMediaPicker()63 MediaPicker createMediaPicker(); showHideSimSelector(boolean show)64 void showHideSimSelector(boolean show); getSimSelectorItemLayoutId()65 int getSimSelectorItemLayoutId(); 66 } 67 68 /** 69 * The "sink" component where all inputs components will direct the user inputs to. This is 70 * typically the ComposeMessageView but may be mocked in test code. 71 */ 72 public interface ConversationInputSink { onMediaItemsSelected(Collection<MessagePartData> items)73 void onMediaItemsSelected(Collection<MessagePartData> items); onMediaItemsUnselected(MessagePartData item)74 void onMediaItemsUnselected(MessagePartData item); onPendingAttachmentAdded(PendingAttachmentData pendingItem)75 void onPendingAttachmentAdded(PendingAttachmentData pendingItem); resumeComposeMessage()76 void resumeComposeMessage(); getComposeEditText()77 EditText getComposeEditText(); setAccessibility(boolean enabled)78 void setAccessibility(boolean enabled); 79 } 80 81 private final ConversationInputHost mHost; 82 private final ConversationInputSink mSink; 83 84 /** Dependencies injected from the host during construction */ 85 private final FragmentManager mFragmentManager; 86 private final Context mContext; 87 private final ImeStateHost mImeStateHost; 88 private final ImmutableBindingRef<ConversationData> mConversationDataModel; 89 private final ImmutableBindingRef<DraftMessageData> mDraftDataModel; 90 91 private final ConversationInput[] mInputs; 92 private final ConversationMediaPicker mMediaInput; 93 private final ConversationSimSelector mSimInput; 94 private final ConversationImeKeyboard mImeInput; 95 private int mUpdateCount; 96 97 private final ImeUtil.ImeStateObserver mImeStateObserver = new ImeUtil.ImeStateObserver() { 98 @Override 99 public void onImeStateChanged(final boolean imeOpen) { 100 mImeInput.onVisibilityChanged(imeOpen); 101 } 102 }; 103 104 private final ConversationDataListener mDataListener = new SimpleConversationDataListener() { 105 @Override 106 public void onConversationParticipantDataLoaded(ConversationData data) { 107 mConversationDataModel.ensureBound(data); 108 } 109 110 @Override 111 public void onSubscriptionListDataLoaded(ConversationData data) { 112 mConversationDataModel.ensureBound(data); 113 mSimInput.onSubscriptionListDataLoaded(data.getSubscriptionListData()); 114 } 115 }; 116 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)117 public ConversationInputManager( 118 final Context context, 119 final ConversationInputHost host, 120 final ConversationInputSink sink, 121 final ImeStateHost imeStateHost, 122 final FragmentManager fm, 123 final BindingBase<ConversationData> conversationDataModel, 124 final BindingBase<DraftMessageData> draftDataModel, 125 final Bundle savedState) { 126 mHost = host; 127 mSink = sink; 128 mFragmentManager = fm; 129 mContext = context; 130 mImeStateHost = imeStateHost; 131 mConversationDataModel = BindingBase.createBindingReference(conversationDataModel); 132 mDraftDataModel = BindingBase.createBindingReference(draftDataModel); 133 134 // Register listeners on dependencies. 135 mImeStateHost.registerImeStateObserver(mImeStateObserver); 136 mConversationDataModel.getData().addConversationDataListener(mDataListener); 137 138 // Initialize the inputs 139 mMediaInput = new ConversationMediaPicker(this); 140 mSimInput = new SimSelector(this); 141 mImeInput = new ConversationImeKeyboard(this, mImeStateHost.isImeOpen()); 142 mInputs = new ConversationInput[] { mMediaInput, mSimInput, mImeInput }; 143 144 if (savedState != null) { 145 for (int i = 0; i < mInputs.length; i++) { 146 mInputs[i].restoreState(savedState); 147 } 148 } 149 updateHostOptionsMenu(); 150 } 151 onDetach()152 public void onDetach() { 153 mImeStateHost.unregisterImeStateObserver(mImeStateObserver); 154 // Don't need to explicitly unregister for data model events. It will unregister all 155 // listeners automagically on unbind. 156 } 157 onSaveInputState(final Bundle savedState)158 public void onSaveInputState(final Bundle savedState) { 159 for (int i = 0; i < mInputs.length; i++) { 160 mInputs[i].saveState(savedState); 161 } 162 } 163 164 @Override getInputStateKey(final ConversationInput input)165 public String getInputStateKey(final ConversationInput input) { 166 return input.getClass().getCanonicalName() + "_savedstate_"; 167 } 168 onBackPressed()169 public boolean onBackPressed() { 170 for (int i = 0; i < mInputs.length; i++) { 171 if (mInputs[i].onBackPressed()) { 172 return true; 173 } 174 } 175 return false; 176 } 177 onNavigationUpPressed()178 public boolean onNavigationUpPressed() { 179 for (int i = 0; i < mInputs.length; i++) { 180 if (mInputs[i].onNavigationUpPressed()) { 181 return true; 182 } 183 } 184 return false; 185 } 186 resetMediaPickerState()187 public void resetMediaPickerState() { 188 mMediaInput.resetViewHolderState(); 189 } 190 showHideMediaPicker(final boolean show, final boolean animate)191 public void showHideMediaPicker(final boolean show, final boolean animate) { 192 showHideInternal(mMediaInput, show, animate); 193 } 194 195 /** 196 * Show or hide the sim selector 197 * @param show visibility 198 * @param animate whether to animate the change in visibility 199 * @return true if the state of the visibility was changed 200 */ showHideSimSelector(final boolean show, final boolean animate)201 public boolean showHideSimSelector(final boolean show, final boolean animate) { 202 return showHideInternal(mSimInput, show, animate); 203 } 204 showHideImeKeyboard(final boolean show, final boolean animate)205 public void showHideImeKeyboard(final boolean show, final boolean animate) { 206 showHideInternal(mImeInput, show, animate); 207 } 208 hideAllInputs(final boolean animate)209 public void hideAllInputs(final boolean animate) { 210 beginUpdate(); 211 for (int i = 0; i < mInputs.length; i++) { 212 showHideInternal(mInputs[i], false, animate); 213 } 214 endUpdate(); 215 } 216 217 /** 218 * Toggle the visibility of the sim selector. 219 * @param animate 220 * @param subEntry 221 * @return true if the view is now shown, false if it now hidden 222 */ toggleSimSelector(final boolean animate, final SubscriptionListEntry subEntry)223 public boolean toggleSimSelector(final boolean animate, final SubscriptionListEntry subEntry) { 224 mSimInput.setSelected(subEntry); 225 return mSimInput.toggle(animate); 226 } 227 updateActionBar(final ActionBar actionBar)228 public boolean updateActionBar(final ActionBar actionBar) { 229 for (int i = 0; i < mInputs.length; i++) { 230 if (mInputs[i].mShowing) { 231 return mInputs[i].updateActionBar(actionBar); 232 } 233 } 234 return false; 235 } 236 237 @VisibleForTesting isMediaPickerVisible()238 boolean isMediaPickerVisible() { 239 return mMediaInput.mShowing; 240 } 241 242 @VisibleForTesting isSimSelectorVisible()243 boolean isSimSelectorVisible() { 244 return mSimInput.mShowing; 245 } 246 247 @VisibleForTesting isImeKeyboardVisible()248 boolean isImeKeyboardVisible() { 249 return mImeInput.mShowing; 250 } 251 252 @VisibleForTesting testNotifyImeStateChanged(final boolean imeOpen)253 void testNotifyImeStateChanged(final boolean imeOpen) { 254 mImeStateObserver.onImeStateChanged(imeOpen); 255 } 256 257 /** 258 * returns true if the state of the visibility was actually changed 259 */ 260 @Override showHideInternal(final ConversationInput target, final boolean show, final boolean animate)261 public boolean showHideInternal(final ConversationInput target, final boolean show, 262 final boolean animate) { 263 if (!mConversationDataModel.isBound()) { 264 return false; 265 } 266 267 if (target.mShowing == show) { 268 return false; 269 } 270 beginUpdate(); 271 boolean success; 272 if (!show) { 273 success = target.hide(animate); 274 } else { 275 success = target.show(animate); 276 } 277 278 if (success) { 279 target.onVisibilityChanged(show); 280 } 281 endUpdate(); 282 return true; 283 } 284 285 @Override handleOnShow(final ConversationInput target)286 public void handleOnShow(final ConversationInput target) { 287 if (!mConversationDataModel.isBound()) { 288 return; 289 } 290 beginUpdate(); 291 292 // All inputs are mutually exclusive. Showing one will hide everything else. 293 // The one exception, is that the keyboard and location media chooser can be open at the 294 // time to enable searching within that chooser 295 for (int i = 0; i < mInputs.length; i++) { 296 final ConversationInput currInput = mInputs[i]; 297 if (currInput != target) { 298 // TODO : If there's more exceptions we will want to make this more 299 // generic 300 if (currInput instanceof ConversationMediaPicker && 301 target instanceof ConversationImeKeyboard && 302 mMediaInput.getExistingOrCreateMediaPicker() != null && 303 mMediaInput.getExistingOrCreateMediaPicker().canShowIme()) { 304 // Allow the keyboard and location mediaPicker to be open at the same time, 305 // but ensure the media picker is full screen to allow enough room 306 mMediaInput.getExistingOrCreateMediaPicker().setFullScreen(true); 307 continue; 308 } 309 showHideInternal(currInput, false /* show */, false /* animate */); 310 } 311 } 312 // Always dismiss action mode on show. 313 mHost.dismissActionMode(); 314 // Invoking any non-keyboard input UI is treated as starting message compose. 315 if (target != mImeInput) { 316 mHost.onStartComposeMessage(); 317 } 318 endUpdate(); 319 } 320 321 @Override beginUpdate()322 public void beginUpdate() { 323 mUpdateCount++; 324 } 325 326 @Override endUpdate()327 public void endUpdate() { 328 Assert.isTrue(mUpdateCount > 0); 329 if (--mUpdateCount == 0) { 330 // Always try to update the host action bar after every update cycle. 331 mHost.invalidateActionBar(); 332 } 333 } 334 updateHostOptionsMenu()335 private void updateHostOptionsMenu() { 336 mHost.setOptionsMenuVisibility(!mMediaInput.isOpen()); 337 } 338 339 /** 340 * Manages showing/hiding the media picker in conversation. 341 */ 342 private class ConversationMediaPicker extends ConversationInput { ConversationMediaPicker(ConversationInputBase baseHost)343 public ConversationMediaPicker(ConversationInputBase baseHost) { 344 super(baseHost, false); 345 } 346 347 private MediaPicker mMediaPicker; 348 349 @Override show(boolean animate)350 public boolean show(boolean animate) { 351 if (mMediaPicker == null) { 352 mMediaPicker = getExistingOrCreateMediaPicker(); 353 setConversationThemeColor(ConversationDrawables.get().getConversationThemeColor()); 354 mMediaPicker.setSubscriptionDataProvider(mHost); 355 mMediaPicker.setDraftMessageDataModel(mDraftDataModel); 356 mMediaPicker.setListener(new MediaPickerListener() { 357 @Override 358 public void onOpened() { 359 handleStateChange(); 360 } 361 362 @Override 363 public void onFullScreenChanged(boolean fullScreen) { 364 // When we're full screen, we want to disable accessibility on the 365 // ComposeMessageView controls (attach button, message input, sim chooser) 366 // that are hiding underneath the action bar. 367 mSink.setAccessibility(!fullScreen /*enabled*/); 368 handleStateChange(); 369 } 370 371 @Override 372 public void onDismissed() { 373 // Re-enable accessibility on all controls now that the media picker is 374 // going away. 375 mSink.setAccessibility(true /*enabled*/); 376 handleStateChange(); 377 } 378 379 private void handleStateChange() { 380 onVisibilityChanged(isOpen()); 381 mHost.invalidateActionBar(); 382 updateHostOptionsMenu(); 383 } 384 385 @Override 386 public void onItemsSelected(final Collection<MessagePartData> items, 387 final boolean resumeCompose) { 388 mSink.onMediaItemsSelected(items); 389 mHost.invalidateActionBar(); 390 if (resumeCompose) { 391 mSink.resumeComposeMessage(); 392 } 393 } 394 395 @Override 396 public void onItemUnselected(final MessagePartData item) { 397 mSink.onMediaItemsUnselected(item); 398 mHost.invalidateActionBar(); 399 } 400 401 @Override 402 public void onConfirmItemSelection() { 403 mSink.resumeComposeMessage(); 404 } 405 406 @Override 407 public void onPendingItemAdded(final PendingAttachmentData pendingItem) { 408 mSink.onPendingAttachmentAdded(pendingItem); 409 } 410 411 @Override 412 public void onChooserSelected(final int chooserIndex) { 413 mHost.invalidateActionBar(); 414 mHost.dismissActionMode(); 415 } 416 }); 417 } 418 419 mMediaPicker.open(MediaPicker.MEDIA_TYPE_DEFAULT, animate); 420 421 return isOpen(); 422 } 423 424 @Override hide(boolean animate)425 public boolean hide(boolean animate) { 426 if (mMediaPicker != null) { 427 mMediaPicker.dismiss(animate); 428 } 429 return !isOpen(); 430 } 431 resetViewHolderState()432 public void resetViewHolderState() { 433 if (mMediaPicker != null) { 434 mMediaPicker.resetViewHolderState(); 435 } 436 } 437 setConversationThemeColor(final int themeColor)438 public void setConversationThemeColor(final int themeColor) { 439 if (mMediaPicker != null) { 440 mMediaPicker.setConversationThemeColor(themeColor); 441 } 442 } 443 isOpen()444 private boolean isOpen() { 445 return (mMediaPicker != null && mMediaPicker.isOpen()); 446 } 447 getExistingOrCreateMediaPicker()448 private MediaPicker getExistingOrCreateMediaPicker() { 449 if (mMediaPicker != null) { 450 return mMediaPicker; 451 } 452 MediaPicker mediaPicker = (MediaPicker) 453 mFragmentManager.findFragmentByTag(MediaPicker.FRAGMENT_TAG); 454 if (mediaPicker == null) { 455 mediaPicker = mHost.createMediaPicker(); 456 if (mediaPicker == null) { 457 return null; // this use of ComposeMessageView doesn't support media picking 458 } 459 mFragmentManager.beginTransaction().replace( 460 R.id.mediapicker_container, 461 mediaPicker, 462 MediaPicker.FRAGMENT_TAG).commit(); 463 } 464 return mediaPicker; 465 } 466 467 @Override updateActionBar(ActionBar actionBar)468 public boolean updateActionBar(ActionBar actionBar) { 469 if (isOpen()) { 470 mMediaPicker.updateActionBar(actionBar); 471 return true; 472 } 473 return false; 474 } 475 476 @Override onNavigationUpPressed()477 public boolean onNavigationUpPressed() { 478 if (isOpen() && mMediaPicker.isFullScreen()) { 479 return onBackPressed(); 480 } 481 return super.onNavigationUpPressed(); 482 } 483 onBackPressed()484 public boolean onBackPressed() { 485 if (mMediaPicker != null && mMediaPicker.onBackPressed()) { 486 return true; 487 } 488 return super.onBackPressed(); 489 } 490 } 491 492 /** 493 * Manages showing/hiding the SIM selector in conversation. 494 */ 495 private class SimSelector extends ConversationSimSelector { SimSelector(ConversationInputBase baseHost)496 public SimSelector(ConversationInputBase baseHost) { 497 super(baseHost); 498 } 499 500 @Override getSimSelectorView()501 protected SimSelectorView getSimSelectorView() { 502 return mHost.getSimSelectorView(); 503 } 504 505 @Override getSimSelectorItemLayoutId()506 public int getSimSelectorItemLayoutId() { 507 return mHost.getSimSelectorItemLayoutId(); 508 } 509 510 @Override selectSim(SubscriptionListEntry item)511 protected void selectSim(SubscriptionListEntry item) { 512 mHost.selectSim(item); 513 } 514 515 @Override show(boolean animate)516 public boolean show(boolean animate) { 517 final boolean result = super.show(animate); 518 mHost.showHideSimSelector(true /*show*/); 519 return result; 520 } 521 522 @Override hide(boolean animate)523 public boolean hide(boolean animate) { 524 final boolean result = super.hide(animate); 525 mHost.showHideSimSelector(false /*show*/); 526 return result; 527 } 528 } 529 530 /** 531 * Manages showing/hiding the IME keyboard in conversation. 532 */ 533 private class ConversationImeKeyboard extends ConversationInput { ConversationImeKeyboard(ConversationInputBase baseHost, final boolean isShowing)534 public ConversationImeKeyboard(ConversationInputBase baseHost, final boolean isShowing) { 535 super(baseHost, isShowing); 536 } 537 538 @Override show(boolean animate)539 public boolean show(boolean animate) { 540 ImeUtil.get().showImeKeyboard(mContext, mSink.getComposeEditText()); 541 return true; 542 } 543 544 @Override hide(boolean animate)545 public boolean hide(boolean animate) { 546 ImeUtil.get().hideImeKeyboard(mContext, mSink.getComposeEditText()); 547 return true; 548 } 549 } 550 } 551