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 
17 package com.android.messaging.ui.conversation;
18 
19 import android.app.FragmentManager;
20 import android.app.FragmentTransaction;
21 import android.content.Intent;
22 import android.graphics.Rect;
23 import android.net.Uri;
24 import android.os.Bundle;
25 import androidx.appcompat.app.ActionBar;
26 import android.text.TextUtils;
27 import android.view.MenuItem;
28 
29 import com.android.messaging.R;
30 import com.android.messaging.datamodel.MessagingContentProvider;
31 import com.android.messaging.datamodel.data.MessageData;
32 import com.android.messaging.ui.BugleActionBarActivity;
33 import com.android.messaging.ui.UIIntents;
34 import com.android.messaging.ui.contact.ContactPickerFragment;
35 import com.android.messaging.ui.contact.ContactPickerFragment.ContactPickerFragmentHost;
36 import com.android.messaging.ui.conversation.ConversationActivityUiState.ConversationActivityUiStateHost;
37 import com.android.messaging.ui.conversation.ConversationFragment.ConversationFragmentHost;
38 import com.android.messaging.ui.conversationlist.ConversationListActivity;
39 import com.android.messaging.util.Assert;
40 import com.android.messaging.util.ContentType;
41 import com.android.messaging.util.LogUtil;
42 import com.android.messaging.util.OsUtil;
43 import com.android.messaging.util.UiUtils;
44 
45 public 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
onCreate(final Bundle savedInstanceState)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
onSaveInstanceState(final Bundle outState)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
onResume()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
onPause()148     protected void onPause() {
149         super.onPause();
150         mIsPaused = true;
151     }
152 
153     @Override
onWindowFocusChanged(final boolean hasFocus)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
onDisplayHeightChanged(final int heightSpecification)165     public void onDisplayHeightChanged(final int heightSpecification) {
166         super.onDisplayHeightChanged(heightSpecification);
167         invalidateActionBar();
168     }
169 
170     @Override
onDestroy()171     protected void onDestroy() {
172         super.onDestroy();
173         if (mUiState != null) {
174             mUiState.setHost(null);
175         }
176     }
177 
178     @Override
updateActionBar(final ActionBar actionBar)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
onOptionsItemSelected(final MenuItem menuItem)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 
onNavigationUpPressed()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
onBackPressed()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 
getContactPicker()227     private ContactPickerFragment getContactPicker() {
228         return (ContactPickerFragment) getFragmentManager().findFragmentByTag(
229                 ContactPickerFragment.FRAGMENT_TAG);
230     }
231 
getConversationFragment()232     private ConversationFragment getConversationFragment() {
233         return (ConversationFragment) getFragmentManager().findFragmentByTag(
234                 ConversationFragment.FRAGMENT_TAG);
235     }
236 
237     @Override // From ContactPickerFragmentHost
onGetOrCreateNewConversation(final String conversationId)238     public void onGetOrCreateNewConversation(final String conversationId) {
239         Assert.isTrue(conversationId != null);
240         mUiState.onGetOrCreateConversation(conversationId);
241     }
242 
243     @Override // From ContactPickerFragmentHost
onBackButtonPressed()244     public void onBackButtonPressed() {
245         onBackPressed();
246     }
247 
248     @Override // From ContactPickerFragmentHost
onInitiateAddMoreParticipants()249     public void onInitiateAddMoreParticipants() {
250         mUiState.onAddMoreParticipants();
251     }
252 
253 
254     @Override
onParticipantCountChanged(final boolean canAddMoreParticipants)255     public void onParticipantCountChanged(final boolean canAddMoreParticipants) {
256         mUiState.onParticipantCountUpdated(canAddMoreParticipants);
257     }
258 
259     @Override // From ConversationFragmentHost
onStartComposeMessage()260     public void onStartComposeMessage() {
261         mUiState.onStartMessageCompose();
262     }
263 
264     @Override // From ConversationFragmentHost
onConversationMetadataUpdated()265     public void onConversationMetadataUpdated() {
266         invalidateActionBar();
267     }
268 
269     @Override // From ConversationFragmentHost
onConversationMessagesUpdated(final int numberOfMessages)270     public void onConversationMessagesUpdated(final int numberOfMessages) {
271     }
272 
273     @Override // From ConversationFragmentHost
onConversationParticipantDataLoaded(final int numberOfParticipants)274     public void onConversationParticipantDataLoaded(final int numberOfParticipants) {
275     }
276 
277     @Override // From ConversationFragmentHost
isActiveAndFocused()278     public boolean isActiveAndFocused() {
279         return !mIsPaused && hasWindowFocus();
280     }
281 
282     @Override // From ConversationActivityUiStateListener
onConversationContactPickerUiStateChanged(final int oldState, final int newState, final boolean animate)283     public void onConversationContactPickerUiStateChanged(final int oldState, final int newState,
284             final boolean animate) {
285         Assert.isTrue(oldState != newState);
286         updateUiState(animate);
287     }
288 
updateUiState(final boolean animate)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
onFinishCurrentConversation()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
shouldResumeComposeMessage()359     public boolean shouldResumeComposeMessage() {
360         return mUiState.shouldResumeComposeMessage();
361     }
362 
363     @Override
onActivityResult(final int requestCode, final int resultCode, final Intent data)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