1 /* 2 * Copyright (C) 2013 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.mail.ui; 18 19 import android.animation.Animator; 20 import android.animation.ObjectAnimator; 21 import android.animation.Animator.AnimatorListener; 22 import android.app.LoaderManager; 23 import android.app.LoaderManager.LoaderCallbacks; 24 import android.content.Context; 25 import android.content.Loader; 26 import android.content.res.Resources; 27 import android.os.Bundle; 28 import android.text.SpannableString; 29 import android.text.style.TextAppearanceSpan; 30 import android.util.AttributeSet; 31 import android.view.View; 32 import android.view.animation.DecelerateInterpolator; 33 import android.widget.FrameLayout; 34 import android.widget.TextView; 35 36 import com.android.mail.R; 37 import com.android.mail.browse.ConversationCursor; 38 import com.android.mail.content.ObjectCursor; 39 import com.android.mail.content.ObjectCursorLoader; 40 import com.android.mail.preferences.AccountPreferences; 41 import com.android.mail.providers.Account; 42 import com.android.mail.providers.Folder; 43 import com.android.mail.providers.UIProvider; 44 import com.android.mail.utils.Utils; 45 46 /** 47 * Tip that is displayed in conversation list of 'Sent' folder whenever there are 48 * one or more messages in the Outbox. 49 */ 50 public class ConversationsInOutboxTipView extends FrameLayout 51 implements ConversationSpecialItemView, SwipeableItemView { 52 53 private static int sScrollSlop = 0; 54 private static int sShrinkAnimationDuration; 55 56 private Account mAccount = null; 57 private AccountPreferences mAccountPreferences; 58 private AnimatedAdapter mAdapter; 59 private LoaderManager mLoaderManager; 60 private FolderSelector mFolderSelector; 61 private Folder mOutbox; 62 private int mOutboxCount = -1; 63 64 private View mSwipeableContent; 65 private TextView mText; 66 67 private int mAnimatedHeight = -1; 68 69 private View mTeaserRightEdge; 70 /** Whether we are on a tablet device or not */ 71 private final boolean mTabletDevice; 72 /** When in conversation mode, true if the list is hidden */ 73 private final boolean mListCollapsible; 74 75 private static final int LOADER_FOLDER_LIST = 76 AbstractActivityController.LAST_FRAGMENT_LOADER_ID + 100; 77 78 public ConversationsInOutboxTipView(final Context context) { 79 this(context, null); 80 } 81 82 public ConversationsInOutboxTipView(final Context context, final AttributeSet attrs) { 83 this(context, attrs, -1); 84 } 85 86 public ConversationsInOutboxTipView( 87 final Context context, final AttributeSet attrs, final int defStyle) { 88 super(context, attrs, defStyle); 89 90 final Resources resources = context.getResources(); 91 92 if (sScrollSlop == 0) { 93 sScrollSlop = resources.getInteger(R.integer.swipeScrollSlop); 94 sShrinkAnimationDuration = resources.getInteger( 95 R.integer.shrink_animation_duration); 96 } 97 98 mTabletDevice = Utils.useTabletUI(resources); 99 mListCollapsible = resources.getBoolean(R.bool.list_collapsible); 100 } 101 102 public void bind(final Account account, final FolderSelector folderSelector) { 103 mAccount = account; 104 mAccountPreferences = AccountPreferences.get(getContext(), account.getEmailAddress()); 105 mFolderSelector = folderSelector; 106 } 107 108 @Override 109 public void onGetView() { 110 // Do nothing 111 } 112 113 @Override 114 protected void onFinishInflate() { 115 mSwipeableContent = findViewById(R.id.swipeable_content); 116 117 mText = (TextView) findViewById(R.id.outbox); 118 119 findViewById(R.id.outbox).setOnClickListener(new View.OnClickListener() { 120 @Override 121 public void onClick(View v) { 122 goToOutbox(); 123 } 124 }); 125 126 findViewById(R.id.dismiss_button).setOnClickListener(new View.OnClickListener() { 127 @Override 128 public void onClick(View v) { 129 dismiss(); 130 } 131 }); 132 133 mTeaserRightEdge = findViewById(R.id.teaser_right_edge); 134 } 135 136 private void goToOutbox() { 137 if (mOutbox != null) { 138 mFolderSelector.onFolderSelected(mOutbox); 139 } 140 } 141 142 @Override 143 public void onUpdate(Folder folder, ConversationCursor cursor) { 144 if (mLoaderManager != null && folder != null) { 145 if ((folder.type & UIProvider.FolderType.SENT) > 0) { 146 // Only display this tip if user is viewing the Sent folder 147 mLoaderManager.initLoader(LOADER_FOLDER_LIST, null, mFolderListLoaderCallbacks); 148 } 149 } 150 } 151 152 private final LoaderCallbacks<ObjectCursor<Folder>> mFolderListLoaderCallbacks = 153 new LoaderManager.LoaderCallbacks<ObjectCursor<Folder>>() { 154 @Override 155 public void onLoaderReset(final Loader<ObjectCursor<Folder>> loader) { 156 // Do nothing 157 } 158 159 @Override 160 public void onLoadFinished(final Loader<ObjectCursor<Folder>> loader, 161 final ObjectCursor<Folder> data) { 162 if (data != null && data.moveToFirst()) { 163 do { 164 final Folder folder = data.getModel(); 165 if ((folder.type & UIProvider.FolderType.OUTBOX) > 0) { 166 mOutbox = folder; 167 onOutboxTotalCount(folder.totalCount); 168 } 169 } while (data.moveToNext()); 170 } 171 } 172 173 @Override 174 public Loader<ObjectCursor<Folder>> onCreateLoader(final int id, final Bundle args) { 175 // This loads all folders in order to find 'Outbox'. We could consider adding a new 176 // query to load folders of a given type to make this more efficient, but should be 177 // okay for now since this is triggered infrequently (only when user visits the 178 // 'Sent' folder). 179 final ObjectCursorLoader<Folder> loader = new ObjectCursorLoader<Folder>(getContext(), 180 mAccount.folderListUri, UIProvider.FOLDERS_PROJECTION, Folder.FACTORY); 181 return loader; 182 } 183 }; 184 185 private void onOutboxTotalCount(int outboxCount) { 186 if (mOutboxCount != outboxCount) { 187 mOutboxCount = outboxCount; 188 if (outboxCount > 0) { 189 if (mText != null) { 190 updateText(); 191 } 192 } 193 } 194 if (outboxCount == 0) { 195 // Clear the last seen count, so that new messages in Outbox will always cause this 196 // tip to appear again. 197 mAccountPreferences.setLastSeenOutboxCount(0); 198 } 199 } 200 201 private void updateText() { 202 // Update the display text to reflect current mOutboxCount 203 final Resources resources = getContext().getResources(); 204 final String subString = mOutbox.name; 205 final String entireString = resources.getString( 206 R.string.unsent_messages_in_outbox, 207 String.valueOf(mOutboxCount), subString); 208 final SpannableString text = new SpannableString(entireString); 209 final int index = entireString.indexOf(subString); 210 text.setSpan( 211 new TextAppearanceSpan(getContext(), R.style.LinksInTipTextAppearance), 212 index, 213 index + subString.length(), 214 0); 215 mText.setText(text); 216 } 217 218 @Override 219 public boolean getShouldDisplayInList() { 220 return (mOutboxCount > 0 && mOutboxCount != mAccountPreferences.getLastSeenOutboxCount()); 221 } 222 223 @Override 224 public int getPosition() { 225 // We want this teaser to go before the first real conversation 226 return 0; 227 } 228 229 @Override 230 public void setAdapter(AnimatedAdapter adapter) { 231 mAdapter = adapter; 232 } 233 234 @Override 235 public void bindFragment(final LoaderManager loaderManager, final Bundle savedInstanceState) { 236 mLoaderManager = loaderManager; 237 } 238 239 @Override 240 public void cleanup() { 241 } 242 243 @Override 244 public void onConversationSelected() { 245 // DO NOTHING 246 } 247 248 @Override 249 public void onCabModeEntered() { 250 } 251 252 @Override 253 public void onCabModeExited() { 254 } 255 256 @Override 257 public void onConversationListVisibilityChanged(final boolean visible) { 258 // Do nothing 259 } 260 261 @Override 262 public void saveInstanceState(final Bundle outState) { 263 // Do nothing 264 } 265 266 @Override 267 public boolean acceptsUserTaps() { 268 return true; 269 } 270 271 @Override 272 public void dismiss() { 273 // Do not show this tip again until we have a new count. Note this is not quite 274 // ideal behavior since after a user dismisses an "1 unsent in outbox" tip, 275 // the message stuck in Outbox could get sent, and a new one gets stuck. 276 // If the user checks back on on Sent folder then, we don't reshow the message since count 277 // itself hasn't changed, but ideally we should since it's a different message than before. 278 // However if user checks the Sent folder in between (when there were 0 messages 279 // in Outbox), the preference is cleared (see {@link onOutboxTotalCount}). 280 mAccountPreferences.setLastSeenOutboxCount(mOutboxCount); 281 282 startDestroyAnimation(); 283 } 284 285 @Override 286 public SwipeableView getSwipeableView() { 287 return SwipeableView.from(mSwipeableContent); 288 } 289 290 @Override 291 public boolean canChildBeDismissed() { 292 return true; 293 } 294 295 @Override 296 public float getMinAllowScrollDistance() { 297 return sScrollSlop; 298 } 299 300 private void startDestroyAnimation() { 301 final int start = getHeight(); 302 final int end = 0; 303 mAnimatedHeight = start; 304 final ObjectAnimator heightAnimator = 305 ObjectAnimator.ofInt(this, "animatedHeight", start, end); 306 heightAnimator.setInterpolator(new DecelerateInterpolator(2.0f)); 307 heightAnimator.setDuration(sShrinkAnimationDuration); 308 heightAnimator.addListener(new AnimatorListener() { 309 @Override 310 public void onAnimationStart(final Animator animation) { 311 // Do nothing 312 } 313 314 @Override 315 public void onAnimationRepeat(final Animator animation) { 316 // Do nothing 317 } 318 319 @Override 320 public void onAnimationEnd(final Animator animation) { 321 // We should no longer exist, so notify the adapter 322 mAdapter.notifyDataSetChanged(); 323 } 324 325 @Override 326 public void onAnimationCancel(final Animator animation) { 327 // Do nothing 328 } 329 }); 330 heightAnimator.start(); 331 } 332 333 /** 334 * This method is used by the animator. It is explicitly kept in proguard.flags to prevent it 335 * from being removed, inlined, or obfuscated. 336 * Edit ./vendor/unbundled/packages/apps/UnifiedGmail/proguard.flags 337 * In the future, we want to use @Keep 338 */ 339 public void setAnimatedHeight(final int height) { 340 mAnimatedHeight = height; 341 requestLayout(); 342 } 343 344 @Override 345 protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { 346 if (Utils.getDisplayListRightEdgeEffect(mTabletDevice, mListCollapsible, 347 mAdapter.getViewMode())) { 348 mTeaserRightEdge.setVisibility(VISIBLE); 349 } else { 350 mTeaserRightEdge.setVisibility(GONE); 351 } 352 353 if (mAnimatedHeight == -1) { 354 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 355 } else { 356 setMeasuredDimension(View.MeasureSpec.getSize(widthMeasureSpec), mAnimatedHeight); 357 } 358 } 359 } 360