Home | History | Annotate | Download | only in conversation
      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.os.Parcel;
     19 import android.os.Parcelable;
     20 
     21 import com.android.messaging.ui.contact.ContactPickerFragment;
     22 import com.android.messaging.util.Assert;
     23 import com.google.common.annotations.VisibleForTesting;
     24 
     25 /**
     26  * Keeps track of the different UI states that the ConversationActivity may be in. This acts as
     27  * a state machine which, based on different actions (e.g. onAddMoreParticipants), notifies the
     28  * ConversationActivity about any state UI change so it can update the visuals. This class
     29  * implements Parcelable and it's persisted across activity tear down and relaunch.
     30  */
     31 public class ConversationActivityUiState implements Parcelable, Cloneable {
     32     interface ConversationActivityUiStateHost {
     33         void onConversationContactPickerUiStateChanged(int oldState, int newState, boolean animate);
     34     }
     35 
     36     /*------ Overall UI states (conversation & contact picker) ------*/
     37 
     38     /** Only a full screen conversation is showing. */
     39     public static final int STATE_CONVERSATION_ONLY = 1;
     40     /** Only a full screen contact picker is showing asking user to pick the initial contact. */
     41     public static final int STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT = 2;
     42     /**
     43      * Only a full screen contact picker is showing asking user to pick more participants. This
     44      * happens after the user picked the initial contact, and then decide to go back and add more.
     45      */
     46     public static final int STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS = 3;
     47     /**
     48      * Only a full screen contact picker is showing asking user to pick more participants. However
     49      * user has reached max number of conversation participants and can add no more.
     50      */
     51     public static final int STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS = 4;
     52     /**
     53      * A hybrid mode where the conversation view + contact chips view are showing. This happens
     54      * right after the user picked the initial contact for which a 1-1 conversation is fetched or
     55      * created.
     56      */
     57     public static final int STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW = 5;
     58 
     59     // The overall UI state of the ConversationActivity.
     60     private int mConversationContactUiState;
     61 
     62     // The currently displayed conversation (if any).
     63     private String mConversationId;
     64 
     65     // Indicates whether we should put focus in the compose message view when the
     66     // ConversationFragment is attached. This is a transient state that's not persisted as
     67     // part of the parcelable.
     68     private boolean mPendingResumeComposeMessage = false;
     69 
     70     // The owner ConversationActivity. This is not parceled since the instance always change upon
     71     // object reuse.
     72     private ConversationActivityUiStateHost mHost;
     73 
     74     // Indicates the owning ConverastionActivity is in the process of updating its UI presentation
     75     // to be in sync with the UI states. Outside of the UI updates, the UI states here should
     76     // ALWAYS be consistent with the actual states of the activity.
     77     private int mUiUpdateCount;
     78 
     79     /**
     80      * Create a new instance with an initial conversation id.
     81      */
     82     ConversationActivityUiState(final String conversationId) {
     83         // The conversation activity may be initialized with only one of two states:
     84         // Conversation-only (when there's a conversation id) or picking initial contact
     85         // (when no conversation id is given).
     86         mConversationId = conversationId;
     87         mConversationContactUiState = conversationId == null ?
     88                 STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT : STATE_CONVERSATION_ONLY;
     89     }
     90 
     91     public void setHost(final ConversationActivityUiStateHost host) {
     92         mHost = host;
     93     }
     94 
     95     public boolean shouldShowConversationFragment() {
     96         return mConversationContactUiState == STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW ||
     97                 mConversationContactUiState == STATE_CONVERSATION_ONLY;
     98     }
     99 
    100     public boolean shouldShowContactPickerFragment() {
    101         return mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS ||
    102                 mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS ||
    103                 mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT ||
    104                 mConversationContactUiState == STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW;
    105     }
    106 
    107     /**
    108      * Returns whether there's a pending request to resume message compose (i.e. set focus to
    109      * the compose message view and show the soft keyboard). If so, this request will be served
    110      * when the conversation fragment get created and resumed. This happens when the user commits
    111      * participant selection for a group conversation and goes back to the conversation fragment.
    112      * Since conversation fragment creation happens asynchronously, we issue and track this
    113      * pending request for it to be eventually fulfilled.
    114      */
    115     public boolean shouldResumeComposeMessage() {
    116         if (mPendingResumeComposeMessage) {
    117             // This is a one-shot operation that just keeps track of the pending resume compose
    118             // state. This is also a non-critical operation so we don't care about failure case.
    119             mPendingResumeComposeMessage = false;
    120             return true;
    121         }
    122         return false;
    123     }
    124 
    125     public int getDesiredContactPickingMode() {
    126         switch (mConversationContactUiState) {
    127             case STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS:
    128                 return ContactPickerFragment.MODE_PICK_MORE_CONTACTS;
    129             case STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS:
    130                 return ContactPickerFragment.MODE_PICK_MAX_PARTICIPANTS;
    131             case STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT:
    132                 return ContactPickerFragment.MODE_PICK_INITIAL_CONTACT;
    133             case STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW:
    134                 return ContactPickerFragment.MODE_CHIPS_ONLY;
    135             default:
    136                 Assert.fail("Invalid contact picking mode for ConversationActivity!");
    137                 return ContactPickerFragment.MODE_UNDEFINED;
    138         }
    139     }
    140 
    141     public String getConversationId() {
    142         return mConversationId;
    143     }
    144 
    145     /**
    146      * Called whenever the contact picker fragment successfully fetched or created a conversation.
    147      */
    148     public void onGetOrCreateConversation(final String conversationId) {
    149         int newState = STATE_CONVERSATION_ONLY;
    150         if (mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT) {
    151             newState = STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW;
    152         } else if (mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS ||
    153                 mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS) {
    154             newState = STATE_CONVERSATION_ONLY;
    155         } else {
    156             // New conversation should only be created when we are in one of the contact picking
    157             // modes.
    158             Assert.fail("Invalid conversation activity state: can't create conversation!");
    159         }
    160         mConversationId = conversationId;
    161         performUiStateUpdate(newState, true);
    162     }
    163 
    164     /**
    165      * Called when the user started composing message. If we are in the hybrid chips state, we
    166      * should commit to enter the conversation only state.
    167      */
    168     public void onStartMessageCompose() {
    169         // This cannot happen when we are in one of the full-screen contact picking states.
    170         Assert.isTrue(mConversationContactUiState != STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT &&
    171                 mConversationContactUiState != STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS &&
    172                 mConversationContactUiState != STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS);
    173         if (mConversationContactUiState == STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW) {
    174             performUiStateUpdate(STATE_CONVERSATION_ONLY, true);
    175         }
    176     }
    177 
    178     /**
    179      * Called when the user initiated an action to add more participants in the hybrid state,
    180      * namely clicking on the "add more participants" button or entered a new contact chip via
    181      * auto-complete.
    182      */
    183     public void onAddMoreParticipants() {
    184         if (mConversationContactUiState == STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW) {
    185             mPendingResumeComposeMessage = true;
    186             performUiStateUpdate(STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS, true);
    187         } else {
    188             // This is only possible in the hybrid state.
    189             Assert.fail("Invalid conversation activity state: can't add more participants!");
    190         }
    191     }
    192 
    193     /**
    194      * Called each time the number of participants is updated to check against the limit and
    195      * update the ui state accordingly.
    196      */
    197     public void onParticipantCountUpdated(final boolean canAddMoreParticipants) {
    198         if (mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS
    199                 && !canAddMoreParticipants) {
    200             performUiStateUpdate(STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS, false);
    201         } else if (mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS
    202                 && canAddMoreParticipants) {
    203             performUiStateUpdate(STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS, false);
    204         }
    205     }
    206 
    207     private void performUiStateUpdate(final int conversationContactState, final boolean animate) {
    208         // This starts one UI update cycle, during which we allow the conversation activity's
    209         // UI presentation to be temporarily out of sync with the states here.
    210         beginUiUpdate();
    211 
    212         if (conversationContactState != mConversationContactUiState) {
    213             final int oldState = mConversationContactUiState;
    214             mConversationContactUiState = conversationContactState;
    215             notifyOnOverallUiStateChanged(oldState, mConversationContactUiState, animate);
    216         }
    217         endUiUpdate();
    218     }
    219 
    220     private void notifyOnOverallUiStateChanged(
    221             final int oldState, final int newState, final boolean animate) {
    222         // Always verify state validity whenever we have a state change.
    223         assertValidState();
    224         Assert.isTrue(isUiUpdateInProgress());
    225 
    226         // Only do this if we are still attached to the host. mHost can be null if the host
    227         // activity is already destroyed, but due to timing the contained UI components may still
    228         // receive events such as focus change and trigger a callback to the Ui state. We'd like
    229         // to guard against those cases.
    230         if (mHost != null) {
    231             mHost.onConversationContactPickerUiStateChanged(oldState, newState, animate);
    232         }
    233     }
    234 
    235     private void assertValidState() {
    236         // Conversation id may be null IF AND ONLY IF the user is picking the initial contact to
    237         // start a conversation.
    238         Assert.isTrue((mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT) ==
    239                 (mConversationId == null));
    240     }
    241 
    242     private void beginUiUpdate() {
    243         mUiUpdateCount++;
    244     }
    245 
    246     private void endUiUpdate() {
    247         if (--mUiUpdateCount < 0) {
    248             Assert.fail("Unbalanced Ui updates!");
    249         }
    250     }
    251 
    252     private boolean isUiUpdateInProgress() {
    253         return mUiUpdateCount > 0;
    254     }
    255 
    256     @Override
    257     public int describeContents() {
    258         return 0;
    259     }
    260 
    261     @Override
    262     public void writeToParcel(final Parcel dest, final int flags) {
    263         dest.writeInt(mConversationContactUiState);
    264         dest.writeString(mConversationId);
    265     }
    266 
    267     private ConversationActivityUiState(final Parcel in) {
    268         mConversationContactUiState = in.readInt();
    269         mConversationId = in.readString();
    270 
    271         // Always verify state validity whenever we initialize states.
    272         assertValidState();
    273     }
    274 
    275     public static final Parcelable.Creator<ConversationActivityUiState> CREATOR
    276         = new Parcelable.Creator<ConversationActivityUiState>() {
    277         @Override
    278         public ConversationActivityUiState createFromParcel(final Parcel in) {
    279             return new ConversationActivityUiState(in);
    280         }
    281 
    282         @Override
    283         public ConversationActivityUiState[] newArray(final int size) {
    284             return new ConversationActivityUiState[size];
    285         }
    286     };
    287 
    288     @Override
    289     protected ConversationActivityUiState clone() {
    290         try {
    291             return (ConversationActivityUiState) super.clone();
    292         } catch (CloneNotSupportedException e) {
    293             Assert.fail("ConversationActivityUiState: failed to clone(). Is there a mutable " +
    294                     "reference?");
    295         }
    296         return null;
    297     }
    298 
    299     /**
    300      * allows for overridding the internal UI state. Should never be called except by test code.
    301      */
    302     @VisibleForTesting
    303     void testSetUiState(final int uiState) {
    304         mConversationContactUiState = uiState;
    305     }
    306 }
    307