1 /* 2 * Copyright (C) 2012 Google Inc. 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package com.android.mail.browse; 19 20 import android.app.FragmentManager; 21 import android.app.LoaderManager; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.Loader; 25 import android.database.Cursor; 26 import android.net.Uri; 27 import android.os.Bundle; 28 import androidx.core.text.BidiFormatter; 29 import android.text.TextUtils; 30 import android.util.AttributeSet; 31 import android.view.LayoutInflater; 32 import android.view.View; 33 import android.widget.LinearLayout; 34 35 import com.android.mail.R; 36 import com.android.mail.analytics.Analytics; 37 import com.android.mail.browse.AttachmentLoader.AttachmentCursor; 38 import com.android.mail.browse.ConversationContainer.DetachListener; 39 import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem; 40 import com.android.mail.providers.Account; 41 import com.android.mail.providers.Attachment; 42 import com.android.mail.providers.Message; 43 import com.android.mail.ui.AccountFeedbackActivity; 44 import com.android.mail.ui.AttachmentTile; 45 import com.android.mail.ui.AttachmentTileGrid; 46 import com.android.mail.utils.LogTag; 47 import com.android.mail.utils.LogUtils; 48 import com.google.common.base.Objects; 49 import com.google.common.collect.Lists; 50 51 import java.util.ArrayList; 52 import java.util.List; 53 54 public class MessageFooterView extends LinearLayout implements DetachListener, 55 LoaderManager.LoaderCallbacks<Cursor>, View.OnClickListener { 56 57 private MessageHeaderItem mMessageHeaderItem; 58 private LoaderManager mLoaderManager; 59 private FragmentManager mFragmentManager; 60 private AttachmentCursor mAttachmentsCursor; 61 private View mViewEntireMessagePrompt; 62 private AttachmentTileGrid mAttachmentGrid; 63 private LinearLayout mAttachmentBarList; 64 65 private final LayoutInflater mInflater; 66 67 private static final String LOG_TAG = LogTag.getLogTag(); 68 69 private ConversationAccountController mAccountController; 70 71 private BidiFormatter mBidiFormatter; 72 73 private MessageFooterCallbacks mCallbacks; 74 75 private Integer mOldAttachmentLoaderId; 76 77 /** 78 * Callbacks for the MessageFooterView to enable resizing the height. 79 */ 80 public interface MessageFooterCallbacks { 81 /** 82 * @return <tt>true</tt> if this footer is contained within a SecureConversationViewFragment 83 * and cannot assume the content is <strong>not</strong> malicious 84 */ isSecure()85 boolean isSecure(); 86 } 87 MessageFooterView(Context context)88 public MessageFooterView(Context context) { 89 this(context, null); 90 } 91 MessageFooterView(Context context, AttributeSet attrs)92 public MessageFooterView(Context context, AttributeSet attrs) { 93 super(context, attrs); 94 95 mInflater = LayoutInflater.from(context); 96 } 97 98 @Override onFinishInflate()99 protected void onFinishInflate() { 100 super.onFinishInflate(); 101 102 mViewEntireMessagePrompt = findViewById(R.id.view_entire_message_prompt); 103 mAttachmentGrid = (AttachmentTileGrid) findViewById(R.id.attachment_tile_grid); 104 mAttachmentBarList = (LinearLayout) findViewById(R.id.attachment_bar_list); 105 106 mViewEntireMessagePrompt.setOnClickListener(this); 107 } 108 initialize(LoaderManager loaderManager, FragmentManager fragmentManager, ConversationAccountController accountController, MessageFooterCallbacks callbacks)109 public void initialize(LoaderManager loaderManager, FragmentManager fragmentManager, 110 ConversationAccountController accountController, MessageFooterCallbacks callbacks) { 111 mLoaderManager = loaderManager; 112 mFragmentManager = fragmentManager; 113 mAccountController = accountController; 114 mCallbacks = callbacks; 115 } 116 bind( MessageHeaderItem headerItem, boolean measureOnly)117 public void bind( 118 MessageHeaderItem headerItem, boolean measureOnly) { 119 mMessageHeaderItem = headerItem; 120 121 final Integer attachmentLoaderId = getAttachmentLoaderId(); 122 123 // Destroy the loader if we are attempting to load a different attachment 124 if (mOldAttachmentLoaderId != null && 125 !Objects.equal(mOldAttachmentLoaderId, attachmentLoaderId)) { 126 mLoaderManager.destroyLoader(mOldAttachmentLoaderId); 127 128 // Resets the footer view. This step is only done if the 129 // attachmentsListUri changes so that we don't 130 // repeat the work of layout and measure when 131 // we're only updating the attachments. 132 mAttachmentGrid.removeAllViewsInLayout(); 133 mAttachmentBarList.removeAllViewsInLayout(); 134 mViewEntireMessagePrompt.setVisibility(View.GONE); 135 mAttachmentGrid.setVisibility(View.GONE); 136 mAttachmentBarList.setVisibility(View.GONE); 137 } 138 mOldAttachmentLoaderId = attachmentLoaderId; 139 140 // kick off load of Attachment objects in background thread 141 // but don't do any Loader work if we're only measuring 142 if (!measureOnly && attachmentLoaderId != null) { 143 LogUtils.i(LOG_TAG, "binding footer view, calling initLoader for message %d", 144 attachmentLoaderId); 145 mLoaderManager.initLoader(attachmentLoaderId, Bundle.EMPTY, this); 146 } 147 148 // Do an initial render if initLoader didn't already do one 149 if (mAttachmentGrid.getChildCount() == 0 && 150 mAttachmentBarList.getChildCount() == 0) { 151 renderAttachments(false); 152 } 153 154 final ConversationMessage message = mMessageHeaderItem.getMessage(); 155 mViewEntireMessagePrompt.setVisibility( 156 message.clipped && !TextUtils.isEmpty(message.permalink) ? VISIBLE : GONE); 157 setVisibility(mMessageHeaderItem.isExpanded() ? VISIBLE : GONE); 158 } 159 renderAttachments(boolean loaderResult)160 private void renderAttachments(boolean loaderResult) { 161 final List<Attachment> attachments; 162 if (mAttachmentsCursor != null && !mAttachmentsCursor.isClosed()) { 163 int i = -1; 164 attachments = Lists.newArrayList(); 165 while (mAttachmentsCursor.moveToPosition(++i)) { 166 attachments.add(mAttachmentsCursor.get()); 167 } 168 } else { 169 // before the attachment loader results are in, we can still render immediately using 170 // the basic info in the message's attachmentsJSON 171 attachments = mMessageHeaderItem.getMessage().getAttachments(); 172 } 173 renderAttachments(attachments, loaderResult); 174 } 175 renderAttachments(List<Attachment> attachments, boolean loaderResult)176 private void renderAttachments(List<Attachment> attachments, boolean loaderResult) { 177 if (attachments == null || attachments.isEmpty()) { 178 return; 179 } 180 181 // filter the attachments into tiled and non-tiled 182 final int maxSize = attachments.size(); 183 final List<Attachment> tiledAttachments = new ArrayList<Attachment>(maxSize); 184 final List<Attachment> barAttachments = new ArrayList<Attachment>(maxSize); 185 186 for (Attachment attachment : attachments) { 187 // attachments in secure views are displayed in the footer so the user may interact with 188 // them; for normal views there is no need to show inline attachments in the footer 189 // since users can interact with them in place 190 if (!attachment.isInlineAttachment() || mCallbacks.isSecure()) { 191 if (AttachmentTile.isTiledAttachment(attachment)) { 192 tiledAttachments.add(attachment); 193 } else { 194 barAttachments.add(attachment); 195 } 196 } 197 } 198 199 mMessageHeaderItem.getMessage().attachmentsJson = Attachment.toJSONArray(attachments); 200 201 // All attachments are inline, don't display anything. 202 if (tiledAttachments.isEmpty() && barAttachments.isEmpty()) { 203 return; 204 } 205 206 if (!tiledAttachments.isEmpty()) { 207 renderTiledAttachments(tiledAttachments, loaderResult); 208 } 209 if (!barAttachments.isEmpty()) { 210 renderBarAttachments(barAttachments, loaderResult); 211 } 212 } 213 renderTiledAttachments(List<Attachment> tiledAttachments, boolean loaderResult)214 private void renderTiledAttachments(List<Attachment> tiledAttachments, boolean loaderResult) { 215 mAttachmentGrid.setVisibility(View.VISIBLE); 216 217 // Setup the tiles. 218 mAttachmentGrid.configureGrid(mFragmentManager, getAccount(), 219 mMessageHeaderItem.getMessage(), tiledAttachments, loaderResult); 220 } 221 renderBarAttachments(List<Attachment> barAttachments, boolean loaderResult)222 private void renderBarAttachments(List<Attachment> barAttachments, boolean loaderResult) { 223 mAttachmentBarList.setVisibility(View.VISIBLE); 224 225 final Account account = getAccount(); 226 for (Attachment attachment : barAttachments) { 227 final Uri id = attachment.getIdentifierUri(); 228 MessageAttachmentBar barAttachmentView = 229 (MessageAttachmentBar) mAttachmentBarList.findViewWithTag(id); 230 231 if (barAttachmentView == null) { 232 barAttachmentView = MessageAttachmentBar.inflate(mInflater, this); 233 barAttachmentView.setTag(id); 234 barAttachmentView.initialize(mFragmentManager); 235 mAttachmentBarList.addView(barAttachmentView); 236 } 237 238 barAttachmentView.render(attachment, account, mMessageHeaderItem.getMessage(), 239 loaderResult, getBidiFormatter()); 240 } 241 } 242 getAttachmentLoaderId()243 private Integer getAttachmentLoaderId() { 244 Integer id = null; 245 final Message msg = mMessageHeaderItem == null ? null : mMessageHeaderItem.getMessage(); 246 if (msg != null && msg.hasAttachments && msg.attachmentListUri != null) { 247 id = msg.attachmentListUri.hashCode(); 248 } 249 return id; 250 } 251 252 @Override onDetachedFromParent()253 public void onDetachedFromParent() { 254 // Do nothing. 255 } 256 257 @Override onCreateLoader(int id, Bundle args)258 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 259 return new AttachmentLoader(getContext(), 260 mMessageHeaderItem.getMessage().attachmentListUri); 261 } 262 263 @Override onLoadFinished(Loader<Cursor> loader, Cursor data)264 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 265 mAttachmentsCursor = (AttachmentCursor) data; 266 267 if (mAttachmentsCursor == null || mAttachmentsCursor.isClosed()) { 268 return; 269 } 270 271 renderAttachments(true); 272 } 273 274 @Override onLoaderReset(Loader<Cursor> loader)275 public void onLoaderReset(Loader<Cursor> loader) { 276 mAttachmentsCursor = null; 277 } 278 getBidiFormatter()279 private BidiFormatter getBidiFormatter() { 280 if (mBidiFormatter == null) { 281 final ConversationViewAdapter adapter = mMessageHeaderItem != null 282 ? mMessageHeaderItem.getAdapter() : null; 283 if (adapter == null) { 284 mBidiFormatter = BidiFormatter.getInstance(); 285 } else { 286 mBidiFormatter = adapter.getBidiFormatter(); 287 } 288 } 289 return mBidiFormatter; 290 } 291 292 @Override onClick(View v)293 public void onClick(View v) { 294 viewEntireMessage(); 295 } 296 viewEntireMessage()297 private void viewEntireMessage() { 298 Analytics.getInstance().sendEvent("view_entire_message", "clicked", null, 0); 299 300 final Context context = getContext(); 301 final Intent intent = new Intent(); 302 final String activityName = 303 context.getResources().getString(R.string.full_message_activity); 304 if (TextUtils.isEmpty(activityName)) { 305 LogUtils.wtf(LOG_TAG, "Trying to open clipped message with no activity defined"); 306 return; 307 } 308 intent.setClassName(context, activityName); 309 final Account account = getAccount(); 310 final ConversationMessage message = mMessageHeaderItem.getMessage(); 311 if (account != null && !TextUtils.isEmpty(message.permalink)) { 312 intent.putExtra(AccountFeedbackActivity.EXTRA_ACCOUNT_URI, account.uri); 313 intent.putExtra(FullMessageContract.EXTRA_PERMALINK, message.permalink); 314 intent.putExtra(FullMessageContract.EXTRA_ACCOUNT_NAME, account.getEmailAddress()); 315 intent.putExtra(FullMessageContract.EXTRA_SERVER_MESSAGE_ID, message.serverId); 316 context.startActivity(intent); 317 } 318 } 319 getAccount()320 private Account getAccount() { 321 return mAccountController != null ? mAccountController.getAccount() : null; 322 } 323 } 324