summaryrefslogtreecommitdiffstats
path: root/src/com/android/messaging/ui/conversation/ConversationActivityUiState.java
blob: 1469c9393ec372882b26347558293a7f450b5e41 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
/*
 * 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.conversation;

import android.os.Parcel;
import android.os.Parcelable;

import com.android.messaging.ui.contact.ContactPickerFragment;
import com.android.messaging.util.Assert;
import com.google.common.annotations.VisibleForTesting;

/**
 * Keeps track of the different UI states that the ConversationActivity may be in. This acts as
 * a state machine which, based on different actions (e.g. onAddMoreParticipants), notifies the
 * ConversationActivity about any state UI change so it can update the visuals. This class
 * implements Parcelable and it's persisted across activity tear down and relaunch.
 */
public class ConversationActivityUiState implements Parcelable, Cloneable {
    interface ConversationActivityUiStateHost {
        void onConversationContactPickerUiStateChanged(int oldState, int newState, boolean animate);
    }

    /*------ Overall UI states (conversation & contact picker) ------*/

    /** Only a full screen conversation is showing. */
    public static final int STATE_CONVERSATION_ONLY = 1;
    /** Only a full screen contact picker is showing asking user to pick the initial contact. */
    public static final int STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT = 2;
    /**
     * Only a full screen contact picker is showing asking user to pick more participants. This
     * happens after the user picked the initial contact, and then decide to go back and add more.
     */
    public static final int STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS = 3;
    /**
     * Only a full screen contact picker is showing asking user to pick more participants. However
     * user has reached max number of conversation participants and can add no more.
     */
    public static final int STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS = 4;
    /**
     * A hybrid mode where the conversation view + contact chips view are showing. This happens
     * right after the user picked the initial contact for which a 1-1 conversation is fetched or
     * created.
     */
    public static final int STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW = 5;

    // The overall UI state of the ConversationActivity.
    private int mConversationContactUiState;

    // The currently displayed conversation (if any).
    private String mConversationId;

    // Indicates whether we should put focus in the compose message view when the
    // ConversationFragment is attached. This is a transient state that's not persisted as
    // part of the parcelable.
    private boolean mPendingResumeComposeMessage = false;

    // The owner ConversationActivity. This is not parceled since the instance always change upon
    // object reuse.
    private ConversationActivityUiStateHost mHost;

    // Indicates the owning ConverastionActivity is in the process of updating its UI presentation
    // to be in sync with the UI states. Outside of the UI updates, the UI states here should
    // ALWAYS be consistent with the actual states of the activity.
    private int mUiUpdateCount;

    /**
     * Create a new instance with an initial conversation id.
     */
    ConversationActivityUiState(final String conversationId) {
        // The conversation activity may be initialized with only one of two states:
        // Conversation-only (when there's a conversation id) or picking initial contact
        // (when no conversation id is given).
        mConversationId = conversationId;
        mConversationContactUiState = conversationId == null ?
                STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT : STATE_CONVERSATION_ONLY;
    }

    public void setHost(final ConversationActivityUiStateHost host) {
        mHost = host;
    }

    public boolean shouldShowConversationFragment() {
        return mConversationContactUiState == STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW ||
                mConversationContactUiState == STATE_CONVERSATION_ONLY;
    }

    public boolean shouldShowContactPickerFragment() {
        return mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS ||
                mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS ||
                mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT ||
                mConversationContactUiState == STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW;
    }

    /**
     * Returns whether there's a pending request to resume message compose (i.e. set focus to
     * the compose message view and show the soft keyboard). If so, this request will be served
     * when the conversation fragment get created and resumed. This happens when the user commits
     * participant selection for a group conversation and goes back to the conversation fragment.
     * Since conversation fragment creation happens asynchronously, we issue and track this
     * pending request for it to be eventually fulfilled.
     */
    public boolean shouldResumeComposeMessage() {
        if (mPendingResumeComposeMessage) {
            // This is a one-shot operation that just keeps track of the pending resume compose
            // state. This is also a non-critical operation so we don't care about failure case.
            mPendingResumeComposeMessage = false;
            return true;
        }
        return false;
    }

    public int getDesiredContactPickingMode() {
        switch (mConversationContactUiState) {
            case STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS:
                return ContactPickerFragment.MODE_PICK_MORE_CONTACTS;
            case STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS:
                return ContactPickerFragment.MODE_PICK_MAX_PARTICIPANTS;
            case STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT:
                return ContactPickerFragment.MODE_PICK_INITIAL_CONTACT;
            case STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW:
                return ContactPickerFragment.MODE_CHIPS_ONLY;
            default:
                Assert.fail("Invalid contact picking mode for ConversationActivity!");
                return ContactPickerFragment.MODE_UNDEFINED;
        }
    }

    public String getConversationId() {
        return mConversationId;
    }

    /**
     * Called whenever the contact picker fragment successfully fetched or created a conversation.
     */
    public void onGetOrCreateConversation(final String conversationId) {
        int newState = STATE_CONVERSATION_ONLY;
        if (mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT) {
            newState = STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW;
        } else if (mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS ||
                mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS) {
            newState = STATE_CONVERSATION_ONLY;
        } else {
            // New conversation should only be created when we are in one of the contact picking
            // modes.
            Assert.fail("Invalid conversation activity state: can't create conversation!");
        }
        mConversationId = conversationId;
        performUiStateUpdate(newState, true);
    }

    /**
     * Called when the user started composing message. If we are in the hybrid chips state, we
     * should commit to enter the conversation only state.
     */
    public void onStartMessageCompose() {
        // This cannot happen when we are in one of the full-screen contact picking states.
        Assert.isTrue(mConversationContactUiState != STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT &&
                mConversationContactUiState != STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS &&
                mConversationContactUiState != STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS);
        if (mConversationContactUiState == STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW) {
            performUiStateUpdate(STATE_CONVERSATION_ONLY, true);
        }
    }

    /**
     * Called when the user initiated an action to add more participants in the hybrid state,
     * namely clicking on the "add more participants" button or entered a new contact chip via
     * auto-complete.
     */
    public void onAddMoreParticipants() {
        if (mConversationContactUiState == STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW) {
            mPendingResumeComposeMessage = true;
            performUiStateUpdate(STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS, true);
        } else {
            // This is only possible in the hybrid state.
            Assert.fail("Invalid conversation activity state: can't add more participants!");
        }
    }

    /**
     * Called each time the number of participants is updated to check against the limit and
     * update the ui state accordingly.
     */
    public void onParticipantCountUpdated(final boolean canAddMoreParticipants) {
        if (mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS
                && !canAddMoreParticipants) {
            performUiStateUpdate(STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS, false);
        } else if (mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS
                && canAddMoreParticipants) {
            performUiStateUpdate(STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS, false);
        }
    }

    private void performUiStateUpdate(final int conversationContactState, final boolean animate) {
        // This starts one UI update cycle, during which we allow the conversation activity's
        // UI presentation to be temporarily out of sync with the states here.
        beginUiUpdate();

        if (conversationContactState != mConversationContactUiState) {
            final int oldState = mConversationContactUiState;
            mConversationContactUiState = conversationContactState;
            notifyOnOverallUiStateChanged(oldState, mConversationContactUiState, animate);
        }
        endUiUpdate();
    }

    private void notifyOnOverallUiStateChanged(
            final int oldState, final int newState, final boolean animate) {
        // Always verify state validity whenever we have a state change.
        assertValidState();
        Assert.isTrue(isUiUpdateInProgress());

        // Only do this if we are still attached to the host. mHost can be null if the host
        // activity is already destroyed, but due to timing the contained UI components may still
        // receive events such as focus change and trigger a callback to the Ui state. We'd like
        // to guard against those cases.
        if (mHost != null) {
            mHost.onConversationContactPickerUiStateChanged(oldState, newState, animate);
        }
    }

    private void assertValidState() {
        // Conversation id may be null IF AND ONLY IF the user is picking the initial contact to
        // start a conversation.
        Assert.isTrue((mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT) ==
                (mConversationId == null));
    }

    private void beginUiUpdate() {
        mUiUpdateCount++;
    }

    private void endUiUpdate() {
        if (--mUiUpdateCount < 0) {
            Assert.fail("Unbalanced Ui updates!");
        }
    }

    private boolean isUiUpdateInProgress() {
        return mUiUpdateCount > 0;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(final Parcel dest, final int flags) {
        dest.writeInt(mConversationContactUiState);
        dest.writeString(mConversationId);
    }

    private ConversationActivityUiState(final Parcel in) {
        mConversationContactUiState = in.readInt();
        mConversationId = in.readString();

        // Always verify state validity whenever we initialize states.
        assertValidState();
    }

    public static final Parcelable.Creator<ConversationActivityUiState> CREATOR
        = new Parcelable.Creator<ConversationActivityUiState>() {
        @Override
        public ConversationActivityUiState createFromParcel(final Parcel in) {
            return new ConversationActivityUiState(in);
        }

        @Override
        public ConversationActivityUiState[] newArray(final int size) {
            return new ConversationActivityUiState[size];
        }
    };

    @Override
    protected ConversationActivityUiState clone() {
        try {
            return (ConversationActivityUiState) super.clone();
        } catch (CloneNotSupportedException e) {
            Assert.fail("ConversationActivityUiState: failed to clone(). Is there a mutable " +
                    "reference?");
        }
        return null;
    }

    /**
     * allows for overridding the internal UI state. Should never be called except by test code.
     */
    @VisibleForTesting
    void testSetUiState(final int uiState) {
        mConversationContactUiState = uiState;
    }
}