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