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
17package com.android.messaging.ui.conversation;
18
19import android.app.FragmentManager;
20import android.app.FragmentTransaction;
21import android.content.Intent;
22import android.graphics.Rect;
23import android.net.Uri;
24import android.os.Bundle;
25import android.support.v7.app.ActionBar;
26import android.text.TextUtils;
27import android.view.MenuItem;
28
29import com.android.messaging.R;
30import com.android.messaging.datamodel.MessagingContentProvider;
31import com.android.messaging.datamodel.data.MessageData;
32import com.android.messaging.ui.BugleActionBarActivity;
33import com.android.messaging.ui.UIIntents;
34import com.android.messaging.ui.contact.ContactPickerFragment;
35import com.android.messaging.ui.contact.ContactPickerFragment.ContactPickerFragmentHost;
36import com.android.messaging.ui.conversation.ConversationActivityUiState.ConversationActivityUiStateHost;
37import com.android.messaging.ui.conversation.ConversationFragment.ConversationFragmentHost;
38import com.android.messaging.ui.conversationlist.ConversationListActivity;
39import com.android.messaging.util.Assert;
40import com.android.messaging.util.ContentType;
41import com.android.messaging.util.LogUtil;
42import com.android.messaging.util.OsUtil;
43import com.android.messaging.util.UiUtils;
44
45public class ConversationActivity extends BugleActionBarActivity
46        implements ContactPickerFragmentHost, ConversationFragmentHost,
47        ConversationActivityUiStateHost {
48    public static final int FINISH_RESULT_CODE = 1;
49    private static final String SAVED_INSTANCE_STATE_UI_STATE_KEY = "uistate";
50
51    private ConversationActivityUiState mUiState;
52
53    // Fragment transactions cannot be performed after onSaveInstanceState() has been called since
54    // it will cause state loss. We don't want to call commitAllowingStateLoss() since it's
55    // dangerous. Therefore, we note when instance state is saved and avoid performing UI state
56    // updates concerning fragments past that point.
57    private boolean mInstanceStateSaved;
58
59    // Tracks whether onPause is called.
60    private boolean mIsPaused;
61
62    @Override
63    protected void onCreate(final Bundle savedInstanceState) {
64        super.onCreate(savedInstanceState);
65
66        setContentView(R.layout.conversation_activity);
67
68        final Intent intent = getIntent();
69
70        // Do our best to restore UI state from saved instance state.
71        if (savedInstanceState != null) {
72            mUiState = savedInstanceState.getParcelable(SAVED_INSTANCE_STATE_UI_STATE_KEY);
73        } else {
74            if (intent.
75                    getBooleanExtra(UIIntents.UI_INTENT_EXTRA_GOTO_CONVERSATION_LIST, false)) {
76                // See the comment in BugleWidgetService.getViewMoreConversationsView() why this
77                // is unfortunately necessary. The Bugle desktop widget can display a list of
78                // conversations. When there are more conversations that can be displayed in
79                // the widget, the last item is a "More conversations" item. The way widgets
80                // are built, the list items can only go to a single fill-in intent which points
81                // to this ConversationActivity. When the user taps on "More conversations", we
82                // really want to go to the ConversationList. This code makes that possible.
83                finish();
84                final Intent convListIntent = new Intent(this, ConversationListActivity.class);
85                convListIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
86                startActivity(convListIntent);
87                return;
88            }
89        }
90
91        // If saved instance state doesn't offer a clue, get the info from the intent.
92        if (mUiState == null) {
93            final String conversationId = intent.getStringExtra(
94                    UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID);
95            mUiState = new ConversationActivityUiState(conversationId);
96        }
97        mUiState.setHost(this);
98        mInstanceStateSaved = false;
99
100        // Don't animate UI state change for initial setup.
101        updateUiState(false /* animate */);
102
103        // See if we're getting called from a widget to directly display an image or video
104        final String extraToDisplay =
105                intent.getStringExtra(UIIntents.UI_INTENT_EXTRA_ATTACHMENT_URI);
106        if (!TextUtils.isEmpty(extraToDisplay)) {
107            final String contentType =
108                    intent.getStringExtra(UIIntents.UI_INTENT_EXTRA_ATTACHMENT_TYPE);
109            final Rect bounds = UiUtils.getMeasuredBoundsOnScreen(
110                    findViewById(R.id.conversation_and_compose_container));
111            if (ContentType.isImageType(contentType)) {
112                final Uri imagesUri = MessagingContentProvider.buildConversationImagesUri(
113                        mUiState.getConversationId());
114                UIIntents.get().launchFullScreenPhotoViewer(
115                        this, Uri.parse(extraToDisplay), bounds, imagesUri);
116            } else if (ContentType.isVideoType(contentType)) {
117                UIIntents.get().launchFullScreenVideoViewer(this, Uri.parse(extraToDisplay));
118            }
119        }
120    }
121
122    @Override
123    protected void onSaveInstanceState(final Bundle outState) {
124        super.onSaveInstanceState(outState);
125        // After onSaveInstanceState() is called, future changes to mUiState won't update the UI
126        // anymore, because fragment transactions are not allowed past this point.
127        // For an activity recreation due to orientation change, the saved instance state keeps
128        // using the in-memory copy of the UI state instead of writing it to parcel as an
129        // optimization, so the UI state values may still change in response to, for example,
130        // focus change from the framework, making mUiState and actual UI inconsistent.
131        // Therefore, save an exact "snapshot" (clone) of the UI state object to make sure the
132        // restored UI state ALWAYS matches the actual restored UI components.
133        outState.putParcelable(SAVED_INSTANCE_STATE_UI_STATE_KEY, mUiState.clone());
134        mInstanceStateSaved = true;
135    }
136
137    @Override
138    protected void onResume() {
139        super.onResume();
140
141        // we need to reset the mInstanceStateSaved flag since we may have just been restored from
142        // a previous onStop() instead of an onDestroy().
143        mInstanceStateSaved = false;
144        mIsPaused = false;
145    }
146
147    @Override
148    protected void onPause() {
149        super.onPause();
150        mIsPaused = true;
151    }
152
153    @Override
154    public void onWindowFocusChanged(final boolean hasFocus) {
155        super.onWindowFocusChanged(hasFocus);
156        final ConversationFragment conversationFragment = getConversationFragment();
157        // When the screen is turned on, the last used activity gets resumed, but it gets
158        // window focus only after the lock screen is unlocked.
159        if (hasFocus && conversationFragment != null) {
160            conversationFragment.setConversationFocus();
161        }
162    }
163
164    @Override
165    public void onDisplayHeightChanged(final int heightSpecification) {
166        super.onDisplayHeightChanged(heightSpecification);
167        invalidateActionBar();
168    }
169
170    @Override
171    protected void onDestroy() {
172        super.onDestroy();
173        if (mUiState != null) {
174            mUiState.setHost(null);
175        }
176    }
177
178    @Override
179    public void updateActionBar(final ActionBar actionBar) {
180        super.updateActionBar(actionBar);
181        final ConversationFragment conversation = getConversationFragment();
182        final ContactPickerFragment contactPicker = getContactPicker();
183        if (contactPicker != null && mUiState.shouldShowContactPickerFragment()) {
184            contactPicker.updateActionBar(actionBar);
185        } else if (conversation != null && mUiState.shouldShowConversationFragment()) {
186            conversation.updateActionBar(actionBar);
187        }
188    }
189
190    @Override
191    public boolean onOptionsItemSelected(final MenuItem menuItem) {
192        if (super.onOptionsItemSelected(menuItem)) {
193            return true;
194        }
195        if (menuItem.getItemId() == android.R.id.home) {
196            onNavigationUpPressed();
197            return true;
198        }
199        return false;
200    }
201
202    public void onNavigationUpPressed() {
203        // Let the conversation fragment handle the navigation up press.
204        final ConversationFragment conversationFragment = getConversationFragment();
205        if (conversationFragment != null && conversationFragment.onNavigationUpPressed()) {
206            return;
207        }
208        onFinishCurrentConversation();
209    }
210
211    @Override
212    public void onBackPressed() {
213        // If action mode is active dismiss it
214        if (getActionMode() != null) {
215            dismissActionMode();
216            return;
217        }
218
219        // Let the conversation fragment handle the back press.
220        final ConversationFragment conversationFragment = getConversationFragment();
221        if (conversationFragment != null && conversationFragment.onBackPressed()) {
222            return;
223        }
224        super.onBackPressed();
225    }
226
227    private ContactPickerFragment getContactPicker() {
228        return (ContactPickerFragment) getFragmentManager().findFragmentByTag(
229                ContactPickerFragment.FRAGMENT_TAG);
230    }
231
232    private ConversationFragment getConversationFragment() {
233        return (ConversationFragment) getFragmentManager().findFragmentByTag(
234                ConversationFragment.FRAGMENT_TAG);
235    }
236
237    @Override // From ContactPickerFragmentHost
238    public void onGetOrCreateNewConversation(final String conversationId) {
239        Assert.isTrue(conversationId != null);
240        mUiState.onGetOrCreateConversation(conversationId);
241    }
242
243    @Override // From ContactPickerFragmentHost
244    public void onBackButtonPressed() {
245        onBackPressed();
246    }
247
248    @Override // From ContactPickerFragmentHost
249    public void onInitiateAddMoreParticipants() {
250        mUiState.onAddMoreParticipants();
251    }
252
253
254    @Override
255    public void onParticipantCountChanged(final boolean canAddMoreParticipants) {
256        mUiState.onParticipantCountUpdated(canAddMoreParticipants);
257    }
258
259    @Override // From ConversationFragmentHost
260    public void onStartComposeMessage() {
261        mUiState.onStartMessageCompose();
262    }
263
264    @Override // From ConversationFragmentHost
265    public void onConversationMetadataUpdated() {
266        invalidateActionBar();
267    }
268
269    @Override // From ConversationFragmentHost
270    public void onConversationMessagesUpdated(final int numberOfMessages) {
271    }
272
273    @Override // From ConversationFragmentHost
274    public void onConversationParticipantDataLoaded(final int numberOfParticipants) {
275    }
276
277    @Override // From ConversationFragmentHost
278    public boolean isActiveAndFocused() {
279        return !mIsPaused && hasWindowFocus();
280    }
281
282    @Override // From ConversationActivityUiStateListener
283    public void onConversationContactPickerUiStateChanged(final int oldState, final int newState,
284            final boolean animate) {
285        Assert.isTrue(oldState != newState);
286        updateUiState(animate);
287    }
288
289    private void updateUiState(final boolean animate) {
290        if (mInstanceStateSaved || mIsPaused) {
291            return;
292        }
293        Assert.notNull(mUiState);
294        final Intent intent = getIntent();
295        final String conversationId = mUiState.getConversationId();
296
297        final FragmentManager fragmentManager = getFragmentManager();
298        final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
299
300        final boolean needConversationFragment = mUiState.shouldShowConversationFragment();
301        final boolean needContactPickerFragment = mUiState.shouldShowContactPickerFragment();
302        ConversationFragment conversationFragment = getConversationFragment();
303
304        // Set up the conversation fragment.
305        if (needConversationFragment) {
306            Assert.notNull(conversationId);
307            if (conversationFragment == null) {
308                conversationFragment = new ConversationFragment();
309                fragmentTransaction.add(R.id.conversation_fragment_container,
310                        conversationFragment, ConversationFragment.FRAGMENT_TAG);
311            }
312            final MessageData draftData = intent.getParcelableExtra(
313                    UIIntents.UI_INTENT_EXTRA_DRAFT_DATA);
314            if (!needContactPickerFragment) {
315                // Once the user has committed the audience,remove the draft data from the
316                // intent to prevent reuse
317                intent.removeExtra(UIIntents.UI_INTENT_EXTRA_DRAFT_DATA);
318            }
319            conversationFragment.setHost(this);
320            conversationFragment.setConversationInfo(this, conversationId, draftData);
321        } else if (conversationFragment != null) {
322            // Don't save draft to DB when removing conversation fragment and switching to
323            // contact picking mode.  The draft is intended for the new group.
324            conversationFragment.suppressWriteDraft();
325            fragmentTransaction.remove(conversationFragment);
326        }
327
328        // Set up the contact picker fragment.
329        ContactPickerFragment contactPickerFragment = getContactPicker();
330        if (needContactPickerFragment) {
331            if (contactPickerFragment == null) {
332                contactPickerFragment = new ContactPickerFragment();
333                fragmentTransaction.add(R.id.contact_picker_fragment_container,
334                        contactPickerFragment, ContactPickerFragment.FRAGMENT_TAG);
335            }
336            contactPickerFragment.setHost(this);
337            contactPickerFragment.setContactPickingMode(mUiState.getDesiredContactPickingMode(),
338                    animate);
339        } else if (contactPickerFragment != null) {
340            fragmentTransaction.remove(contactPickerFragment);
341        }
342
343        fragmentTransaction.commit();
344        invalidateActionBar();
345    }
346
347    @Override
348    public void onFinishCurrentConversation() {
349        // Simply finish the current activity. The current design is to leave any empty
350        // conversations as is.
351        if (OsUtil.isAtLeastL()) {
352            finishAfterTransition();
353        } else {
354            finish();
355        }
356    }
357
358    @Override
359    public boolean shouldResumeComposeMessage() {
360        return mUiState.shouldResumeComposeMessage();
361    }
362
363    @Override
364    protected void onActivityResult(final int requestCode, final int resultCode,
365            final Intent data) {
366        if (requestCode == ConversationFragment.REQUEST_CHOOSE_ATTACHMENTS &&
367                resultCode == RESULT_OK) {
368            final ConversationFragment conversationFragment = getConversationFragment();
369            if (conversationFragment != null) {
370                conversationFragment.onAttachmentChoosen();
371            } else {
372                LogUtil.e(LogUtil.BUGLE_TAG, "ConversationFragment is missing after launching " +
373                        "AttachmentChooserActivity!");
374            }
375        } else if (resultCode == FINISH_RESULT_CODE) {
376            finish();
377        }
378    }
379}
380