1 /* 2 * Copyright (C) 2010 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.email.activity; 18 19 import android.app.Activity; 20 import android.app.ListFragment; 21 import android.app.LoaderManager; 22 import android.app.LoaderManager.LoaderCallbacks; 23 import android.content.ClipData; 24 import android.content.ClipDescription; 25 import android.content.Context; 26 import android.content.Loader; 27 import android.database.Cursor; 28 import android.graphics.Rect; 29 import android.net.Uri; 30 import android.os.Bundle; 31 import android.os.Parcelable; 32 import android.util.Log; 33 import android.view.DragEvent; 34 import android.view.LayoutInflater; 35 import android.view.View; 36 import android.view.View.OnDragListener; 37 import android.view.ViewGroup; 38 import android.widget.AdapterView; 39 import android.widget.AdapterView.OnItemClickListener; 40 import android.widget.ListView; 41 42 import com.android.email.Controller; 43 import com.android.email.Email; 44 import com.android.email.R; 45 import com.android.email.RefreshManager; 46 import com.android.email.provider.EmailProvider; 47 import com.android.emailcommon.Logging; 48 import com.android.emailcommon.provider.Account; 49 import com.android.emailcommon.provider.Mailbox; 50 import com.android.emailcommon.utility.EmailAsyncTask; 51 import com.android.emailcommon.utility.Utility; 52 import com.google.common.annotations.VisibleForTesting; 53 54 import java.util.Timer; 55 56 /** 57 * This fragment presents a list of mailboxes for a given account or the combined mailboxes. 58 * 59 * This fragment has several parameters that determine the current view. 60 * 61 * <pre> 62 * Parameters: 63 * - Account ID. 64 * - Set via {@link #newInstance}. 65 * - Can be obtained with {@link #getAccountId()}. 66 * - Will not change throughout fragment lifecycle. 67 * - Either an actual account ID, or {@link Account#ACCOUNT_ID_COMBINED_VIEW}. 68 * 69 * - "Highlight enabled?" flag 70 * - Set via {@link #newInstance}. 71 * - Can be obtained with {@link #getEnableHighlight()}. 72 * - Will not change throughout fragment lifecycle. 73 * - If {@code true}, we highlight the "selected" mailbox (used only on 2-pane). 74 * - Note even if it's {@code true}, there may be no highlighted mailbox. 75 * (This usually happens on 2-pane before the UI controller finds the Inbox to highlight.) 76 * 77 * - "Parent" mailbox ID 78 * - Stored in {@link #mParentMailboxId} 79 * - Changes as the user navigates through nested mailboxes. 80 * - Initialized using the {@code mailboxId} parameter for {@link #newInstance} 81 * in {@link #setInitialParentAndHighlight()}. 82 * 83 * - "Highlighted" mailbox 84 * - Only used when highlighting is enabled. (Otherwise always {@link Mailbox#NO_MAILBOX}.) 85 * i.e. used only on two-pane. 86 * - Stored in {@link #mHighlightedMailboxId} 87 * - Initialized using the {@code mailboxId} parameter for {@link #newInstance} 88 * in {@link #setInitialParentAndHighlight()}. 89 * 90 * - Can be changed any time, using {@link #setHighlightedMailbox(long)}. 91 * 92 * - If set, it's considered "selected", and we highlight the list item. 93 * 94 * - (It should always be the ID of the list item selected in the list view, but we store it in 95 * a member for efficiency.) 96 * 97 * - Sometimes, we need to set the highlighted mailbox while we're still loading data. 98 * In this case, we can't update {@link #mHighlightedMailboxId} right away, but need to do so 99 * in when the next data set arrives, in 100 * {@link MailboxListFragment.MailboxListLoaderCallbacks#onLoadFinished}. For this, we use 101 * we store the mailbox ID in {@link #mNextHighlightedMailboxId} and update 102 * {@link #mHighlightedMailboxId} in onLoadFinished. 103 * 104 * 105 * The "selected" is defined using the "parent" and "highlighted" mailboxes. 106 * - "Selected" mailbox (also sometimes called "current".) 107 * - This is what the user thinks it's now selected. 108 * 109 * - Can be obtained with {@link #getSelectedMailboxId()} 110 * - If the "highlighted" mailbox exists, it's the "selected." Otherwise, the "parent" 111 * is considered "selected." 112 * - This is what is passed to {@link Callback#onMailboxSelected}. 113 * </pre> 114 * 115 * 116 * This fragment shows the content in one of the three following views, depending on the 117 * parameters above. 118 * 119 * <pre> 120 * 1. Combined view 121 * - Used if the account ID == {@link Account#ACCOUNT_ID_COMBINED_VIEW}. 122 * - Parent mailbox is always {@link Mailbox#NO_MAILBOX}. 123 * - List contains: 124 * - combined mailboxes 125 * - all accounts 126 * 127 * 2. Root view for an account 128 * - Used if the account ID != {@link Account#ACCOUNT_ID_COMBINED_VIEW} and 129 * Parent mailbox == {@link Mailbox#NO_MAILBOX} 130 * - List contains 131 * - all the top level mailboxes for the selected account. 132 * 133 * 3. Root view for a mailbox. (nested view) 134 * - Used if the account ID != {@link Account#ACCOUNT_ID_COMBINED_VIEW} and 135 * Parent mailbox != {@link Mailbox#NO_MAILBOX} 136 * - List contains: 137 * - parent mailbox (determined by "parent" mailbox ID) 138 * - all child mailboxes of the parent mailbox. 139 * </pre> 140 * 141 * 142 * Note that when a fragment is put in the back stack, it'll lose the content view but the fragment 143 * itself is not destroyed. If you call {@link #getListView()} in this state it'll throw 144 * an {@link IllegalStateException}. So, 145 * - If code is supposed to be executed only when the fragment has the content view, use 146 * {@link #getListView()} directly to make sure it doesn't accidentally get executed when there's 147 * no views. 148 * - Otherwise, make sure to check if the fragment has views with {@link #isViewCreated()} 149 * before touching any views. 150 */ 151 public class MailboxListFragment extends ListFragment implements OnItemClickListener, 152 OnDragListener { 153 private static final String TAG = "MailboxListFragment"; 154 155 private static final String BUNDLE_KEY_PARENT_MAILBOX_ID 156 = "MailboxListFragment.state.parent_mailbox_id"; 157 private static final String BUNDLE_KEY_HIGHLIGHTED_MAILBOX_ID 158 = "MailboxListFragment.state.selected_mailbox_id"; 159 private static final String BUNDLE_LIST_STATE = "MailboxListFragment.state.listState"; 160 private static final boolean DEBUG_DRAG_DROP = false; // MUST NOT SUBMIT SET TO TRUE 161 162 /** No drop target is available where the user is currently hovering over */ 163 private static final int NO_DROP_TARGET = -1; 164 // Total height of the top and bottom scroll zones, in pixels 165 private static final int SCROLL_ZONE_SIZE = 64; 166 // The amount of time to scroll by one pixel, in ms 167 private static final int SCROLL_SPEED = 4; 168 169 /** Arbitrary number for use with the loader manager */ 170 private static final int MAILBOX_LOADER_ID = 1; 171 172 /** Argument name(s) */ 173 private static final String ARG_ACCOUNT_ID = "accountId"; 174 private static final String ARG_ENABLE_HIGHLIGHT = "enablehighlight"; 175 private static final String ARG_INITIAL_CURRENT_MAILBOX_ID = "initialParentMailboxId"; 176 177 private final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker(); 178 179 /** Rectangle used for hit testing children */ 180 private static final Rect sTouchFrame = new Rect(); 181 182 private RefreshManager mRefreshManager; 183 184 // UI Support 185 private Activity mActivity; 186 private MailboxFragmentAdapter mListAdapter; 187 private Callback mCallback = EmptyCallback.INSTANCE; 188 189 // See the class javadoc 190 private long mParentMailboxId; 191 private long mHighlightedMailboxId; 192 193 /** 194 * Becomes {@code true} once we determine which mailbox to use as the parent. 195 */ 196 private boolean mParentDetermined; 197 198 /** 199 * ID of the mailbox that should be highlighted when the next cursor is loaded. 200 */ 201 private long mNextHighlightedMailboxId = Mailbox.NO_MAILBOX; 202 203 // True if a drag is currently in progress 204 private boolean mDragInProgress; 205 /** Mailbox ID of the item being dragged. Used to determine valid drop targets. */ 206 private long mDragItemMailboxId = -1; 207 /** A unique identifier for the drop target. May be {@link #NO_DROP_TARGET}. */ 208 private int mDropTargetId = NO_DROP_TARGET; 209 // The mailbox list item view that the user's finger is hovering over 210 private MailboxListItem mDropTargetView; 211 // Lazily instantiated height of a mailbox list item (-1 is a sentinel for 'not initialized') 212 private int mDragItemHeight = -1; 213 /** {@code true} if we are currently scrolling under the drag item */ 214 private boolean mTargetScrolling; 215 216 private Parcelable mSavedListState; 217 218 private final MailboxFragmentAdapter.Callback mMailboxesAdapterCallback = 219 new MailboxFragmentAdapter.Callback() { 220 @Override 221 public void onBind(MailboxListItem listItem) { 222 listItem.setDropTargetBackground(mDragInProgress, mDragItemMailboxId); 223 } 224 }; 225 226 /** 227 * Callback interface that owning activities must implement 228 */ 229 public interface Callback { 230 /** 231 * Called when any mailbox (even a combined mailbox) is selected. 232 * 233 * @param accountId 234 * The ID of the owner account of the selected mailbox. 235 * Or {@link Account#ACCOUNT_ID_COMBINED_VIEW} if it's a combined mailbox. 236 * @param mailboxId 237 * The ID of the selected mailbox. This may be real mailbox ID [e.g. a number > 0], 238 * or a combined mailbox ID [e.g. {@link Mailbox#QUERY_ALL_INBOXES}]. 239 * @param nestedNavigation {@code true} if the event is caused by nested mailbox navigation, 240 * that is, going up or drilling-in to a child mailbox. 241 */ onMailboxSelected(long accountId, long mailboxId, boolean nestedNavigation)242 public void onMailboxSelected(long accountId, long mailboxId, boolean nestedNavigation); 243 244 /** Called when an account is selected on the combined view. */ onAccountSelected(long accountId)245 public void onAccountSelected(long accountId); 246 247 /** 248 * Called when the parent mailbox is changing. 249 */ onParentMailboxChanged()250 public void onParentMailboxChanged(); 251 } 252 253 private static class EmptyCallback implements Callback { 254 public static final Callback INSTANCE = new EmptyCallback(); onMailboxSelected(long accountId, long mailboxId, boolean nestedNavigation)255 @Override public void onMailboxSelected(long accountId, long mailboxId, 256 boolean nestedNavigation) { } onAccountSelected(long accountId)257 @Override public void onAccountSelected(long accountId) { } 258 @Override onParentMailboxChanged()259 public void onParentMailboxChanged() { } 260 } 261 262 /** 263 * Returns the index of the view located at the specified coordinates in the given list. 264 * If the coordinates are outside of the list, {@code NO_DROP_TARGET} is returned. 265 */ pointToIndex(ListView list, int x, int y)266 private static int pointToIndex(ListView list, int x, int y) { 267 final int count = list.getChildCount(); 268 for (int i = count - 1; i >= 0; i--) { 269 final View child = list.getChildAt(i); 270 if (child.getVisibility() == View.VISIBLE) { 271 child.getHitRect(sTouchFrame); 272 if (sTouchFrame.contains(x, y)) { 273 return i; 274 } 275 } 276 } 277 return NO_DROP_TARGET; 278 } 279 280 /** 281 * Create a new instance with initialization parameters. 282 * 283 * This fragment should be created only with this method. (Arguments should always be set.) 284 * 285 * @param accountId The ID of the account we want to view 286 * @param initialCurrentMailboxId ID of the mailbox of interest. 287 * Pass {@link Mailbox#NO_MAILBOX} to show top-level mailboxes. 288 * @param enableHighlight {@code true} if highlighting is enabled on the current screen 289 * configuration. (We don't highlight mailboxes on one-pane.) 290 */ newInstance(long accountId, long initialCurrentMailboxId, boolean enableHighlight)291 public static MailboxListFragment newInstance(long accountId, long initialCurrentMailboxId, 292 boolean enableHighlight) { 293 final MailboxListFragment instance = new MailboxListFragment(); 294 final Bundle args = new Bundle(); 295 args.putLong(ARG_ACCOUNT_ID, accountId); 296 args.putLong(ARG_INITIAL_CURRENT_MAILBOX_ID, initialCurrentMailboxId); 297 args.putBoolean(ARG_ENABLE_HIGHLIGHT, enableHighlight); 298 instance.setArguments(args); 299 return instance; 300 } 301 302 /** 303 * The account ID the mailbox is associated with. Do not use directly; instead, use 304 * {@link #getAccountId()}. 305 * <p><em>NOTE:</em> Although we cannot force these to be immutable using Java language 306 * constructs, this <em>must</em> be considered immutable. 307 */ 308 private Long mImmutableAccountId; 309 310 /** 311 * {@code initialCurrentMailboxId} passed to {@link #newInstance}. 312 * Do not use directly; instead, use {@link #getInitialCurrentMailboxId()}. 313 * <p><em>NOTE:</em> Although we cannot force these to be immutable using Java language 314 * constructs, this <em>must</em> be considered immutable. 315 */ 316 private long mImmutableInitialCurrentMailboxId; 317 318 /** 319 * {@code enableHighlight} passed to {@link #newInstance}. 320 * Do not use directly; instead, use {@link #getEnableHighlight()}. 321 * <p><em>NOTE:</em> Although we cannot force these to be immutable using Java language 322 * constructs, this <em>must</em> be considered immutable. 323 */ 324 private boolean mImmutableEnableHighlight; 325 initializeArgCache()326 private void initializeArgCache() { 327 if (mImmutableAccountId != null) return; 328 mImmutableAccountId = getArguments().getLong(ARG_ACCOUNT_ID); 329 mImmutableInitialCurrentMailboxId = getArguments().getLong(ARG_INITIAL_CURRENT_MAILBOX_ID); 330 mImmutableEnableHighlight = getArguments().getBoolean(ARG_ENABLE_HIGHLIGHT); 331 } 332 333 /** 334 * @return {@code accountId} passed to {@link #newInstance}. Safe to call even before onCreate. 335 */ getAccountId()336 public long getAccountId() { 337 initializeArgCache(); 338 return mImmutableAccountId; 339 } 340 341 /** 342 * @return {@code initialCurrentMailboxId} passed to {@link #newInstance}. 343 * Safe to call even before onCreate. 344 */ getInitialCurrentMailboxId()345 public long getInitialCurrentMailboxId() { 346 initializeArgCache(); 347 return mImmutableInitialCurrentMailboxId; 348 } 349 350 /** 351 * @return {@code enableHighlight} passed to {@link #newInstance}. 352 * Safe to call even before onCreate. 353 */ getEnableHighlight()354 public boolean getEnableHighlight() { 355 initializeArgCache(); 356 return mImmutableEnableHighlight; 357 } 358 359 @Override onAttach(Activity activity)360 public void onAttach(Activity activity) { 361 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 362 Log.d(Logging.LOG_TAG, this + " onAttach"); 363 } 364 super.onAttach(activity); 365 } 366 367 /** 368 * Called to do initial creation of a fragment. This is called after 369 * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}. 370 */ 371 @Override onCreate(Bundle savedInstanceState)372 public void onCreate(Bundle savedInstanceState) { 373 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 374 Log.d(Logging.LOG_TAG, this + " onCreate"); 375 } 376 super.onCreate(savedInstanceState); 377 378 mActivity = getActivity(); 379 mRefreshManager = RefreshManager.getInstance(mActivity); 380 mListAdapter = new MailboxFragmentAdapter(mActivity, mMailboxesAdapterCallback); 381 setListAdapter(mListAdapter); // It's safe to do even before the list view is created. 382 383 if (savedInstanceState == null) { 384 setInitialParentAndHighlight(); 385 } else { 386 restoreInstanceState(savedInstanceState); 387 } 388 } 389 390 /** 391 * Set {@link #mParentMailboxId} and {@link #mHighlightedMailboxId} from the fragment arguments. 392 */ setInitialParentAndHighlight()393 private void setInitialParentAndHighlight() { 394 final long initialMailboxId = getInitialCurrentMailboxId(); 395 if (getAccountId() == Account.ACCOUNT_ID_COMBINED_VIEW) { 396 // For the combined view, always show the top-level, but highlight the "current". 397 mParentMailboxId = Mailbox.NO_MAILBOX; 398 } else { 399 // Inbox needs special care. 400 // Note we can't get the mailbox type on the UI thread but this method *can* be used... 401 final long inboxId = Mailbox.findMailboxOfType(getActivity(), getAccountId(), 402 Mailbox.TYPE_INBOX); 403 if (initialMailboxId == inboxId) { 404 // If Inbox is set as the initial current, we show the top level mailboxes 405 // with inbox highlighted. 406 mParentMailboxId = Mailbox.NO_MAILBOX; 407 } else { 408 // Otherwise, try using the "current" as the "parent" (and also highlight it). 409 // If it has no children, we go up in onLoadFinished(). 410 mParentMailboxId = initialMailboxId; 411 } 412 } 413 // Highlight the mailbox of interest 414 if (getEnableHighlight()) { 415 mHighlightedMailboxId = initialMailboxId; 416 } 417 } 418 419 @Override onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)420 public View onCreateView( 421 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 422 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 423 Log.d(Logging.LOG_TAG, this + " onCreateView"); 424 } 425 return inflater.inflate(R.layout.mailbox_list_fragment, container, false); 426 } 427 428 /** 429 * @return true if the content view is created and not destroyed yet. (i.e. between 430 * {@link #onCreateView} and {@link #onDestroyView}. 431 */ isViewCreated()432 private boolean isViewCreated() { 433 return getView() != null; 434 } 435 436 @Override onActivityCreated(Bundle savedInstanceState)437 public void onActivityCreated(Bundle savedInstanceState) { 438 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 439 Log.d(Logging.LOG_TAG, this + " onActivityCreated"); 440 } 441 super.onActivityCreated(savedInstanceState); 442 443 // Note we can't do this in onCreateView. 444 // getListView() is only usable after onCreateView(). 445 final ListView lv = getListView(); 446 lv.setOnItemClickListener(this); 447 lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE); 448 lv.setOnDragListener(this); 449 450 startLoading(mParentMailboxId, mHighlightedMailboxId); 451 452 UiUtilities.installFragment(this); 453 } 454 setCallback(Callback callback)455 public void setCallback(Callback callback) { 456 mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback; 457 } 458 459 /** 460 * Called when the Fragment is visible to the user. 461 */ 462 @Override onStart()463 public void onStart() { 464 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 465 Log.d(Logging.LOG_TAG, this + " onStart"); 466 } 467 super.onStart(); 468 } 469 470 /** 471 * Called when the fragment is visible to the user and actively running. 472 */ 473 @Override onResume()474 public void onResume() { 475 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 476 Log.d(Logging.LOG_TAG, this + " onResume"); 477 } 478 super.onResume(); 479 480 // Fetch the latest mailbox list from the server here if stale so that the user always 481 // sees the (reasonably) up-to-date mailbox list, without pressing "refresh". 482 final long accountId = getAccountId(); 483 if (mRefreshManager.isMailboxListStale(accountId)) { 484 mRefreshManager.refreshMailboxList(accountId); 485 } 486 } 487 488 @Override onPause()489 public void onPause() { 490 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 491 Log.d(Logging.LOG_TAG, this + " onPause"); 492 } 493 mSavedListState = getListView().onSaveInstanceState(); 494 super.onPause(); 495 } 496 497 /** 498 * Called when the Fragment is no longer started. 499 */ 500 @Override onStop()501 public void onStop() { 502 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 503 Log.d(Logging.LOG_TAG, this + " onStop"); 504 } 505 super.onStop(); 506 } 507 508 @Override onDestroyView()509 public void onDestroyView() { 510 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 511 Log.d(Logging.LOG_TAG, this + " onDestroyView"); 512 } 513 UiUtilities.uninstallFragment(this); 514 super.onDestroyView(); 515 } 516 517 /** 518 * Called when the fragment is no longer in use. 519 */ 520 @Override onDestroy()521 public void onDestroy() { 522 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 523 Log.d(Logging.LOG_TAG, this + " onDestroy"); 524 } 525 mTaskTracker.cancellAllInterrupt(); 526 super.onDestroy(); 527 } 528 529 @Override onDetach()530 public void onDetach() { 531 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 532 Log.d(Logging.LOG_TAG, this + " onDetach"); 533 } 534 super.onDetach(); 535 } 536 537 @Override onSaveInstanceState(Bundle outState)538 public void onSaveInstanceState(Bundle outState) { 539 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 540 Log.d(Logging.LOG_TAG, this + " onSaveInstanceState"); 541 } 542 super.onSaveInstanceState(outState); 543 outState.putLong(BUNDLE_KEY_PARENT_MAILBOX_ID, mParentMailboxId); 544 outState.putLong(BUNDLE_KEY_HIGHLIGHTED_MAILBOX_ID, mHighlightedMailboxId); 545 if (isViewCreated()) { 546 outState.putParcelable(BUNDLE_LIST_STATE, getListView().onSaveInstanceState()); 547 } 548 } 549 restoreInstanceState(Bundle savedInstanceState)550 private void restoreInstanceState(Bundle savedInstanceState) { 551 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 552 Log.d(Logging.LOG_TAG, this + " restoreInstanceState"); 553 } 554 mParentMailboxId = savedInstanceState.getLong(BUNDLE_KEY_PARENT_MAILBOX_ID); 555 mNextHighlightedMailboxId = savedInstanceState.getLong(BUNDLE_KEY_HIGHLIGHTED_MAILBOX_ID); 556 mSavedListState = savedInstanceState.getParcelable(BUNDLE_LIST_STATE); 557 } 558 559 /** 560 * @return "Selected" mailbox ID. 561 */ getSelectedMailboxId()562 public long getSelectedMailboxId() { 563 return (mHighlightedMailboxId != Mailbox.NO_MAILBOX) ? mHighlightedMailboxId 564 : mParentMailboxId; 565 } 566 567 /** 568 * @return {@code true} if top-level mailboxes are shown. {@code false} otherwise. 569 */ isRoot()570 private boolean isRoot() { 571 return mParentMailboxId == Mailbox.NO_MAILBOX; 572 } 573 574 /** 575 * Navigate one level up in the mailbox hierarchy. Does nothing if at the root account view. 576 */ navigateUp()577 public boolean navigateUp() { 578 if (isRoot()) { 579 return false; 580 } 581 FindParentMailboxTask.ResultCallback callback = new FindParentMailboxTask.ResultCallback() { 582 @Override public void onResult(long nextParentMailboxId, 583 long nextHighlightedMailboxId, long nextSelectedMailboxId) { 584 585 startLoading(nextParentMailboxId, nextHighlightedMailboxId); 586 } 587 }; 588 new FindParentMailboxTask( 589 getActivity().getApplicationContext(), mTaskTracker, getAccountId(), 590 getEnableHighlight(), mParentMailboxId, mHighlightedMailboxId, callback 591 ).cancelPreviousAndExecuteParallel((Void[]) null); 592 return true; 593 } 594 595 /** 596 * @return {@code true} if the fragment is showing nested mailboxes and we can go one level up. 597 * {@code false} otherwise, meaning we're showing the top level mailboxes *OR* 598 * we're still loading initial data and we can't determine if we're going to show 599 * top-level or not. 600 */ canNavigateUp()601 public boolean canNavigateUp() { 602 if (!mParentDetermined) { 603 return false; // We can't determine yet... 604 } 605 return !isRoot(); 606 } 607 608 /** 609 * A task to determine what parent mailbox ID/highlighted mailbox ID to use for the "UP" 610 * navigation, given the current parent mailbox ID, the highlighted mailbox ID, and {@link 611 * #mEnableHighlight}. 612 */ 613 @VisibleForTesting 614 static class FindParentMailboxTask extends EmailAsyncTask<Void, Void, Long[]> { 615 public interface ResultCallback { 616 /** 617 * Callback to get the result. 618 * 619 * @param nextParentMailboxId ID of the mailbox to use 620 * @param nextHighlightedMailboxId ID of the mailbox to highlight 621 * @param nextSelectedMailboxId ID of the mailbox to notify with 622 * {@link Callback#onMailboxSelected}. 623 */ onResult(long nextParentMailboxId, long nextHighlightedMailboxId, long nextSelectedMailboxId)624 public void onResult(long nextParentMailboxId, long nextHighlightedMailboxId, 625 long nextSelectedMailboxId); 626 } 627 628 private final Context mContext; 629 private final long mAccountId; 630 private final boolean mEnableHighlight; 631 private final long mParentMailboxId; 632 private final long mHighlightedMailboxId; 633 private final ResultCallback mCallback; 634 FindParentMailboxTask(Context context, EmailAsyncTask.Tracker taskTracker, long accountId, boolean enableHighlight, long parentMailboxId, long highlightedMailboxId, ResultCallback callback)635 public FindParentMailboxTask(Context context, EmailAsyncTask.Tracker taskTracker, 636 long accountId, boolean enableHighlight, long parentMailboxId, 637 long highlightedMailboxId, ResultCallback callback) { 638 super(taskTracker); 639 mContext = context; 640 mAccountId = accountId; 641 mEnableHighlight = enableHighlight; 642 mParentMailboxId = parentMailboxId; 643 mHighlightedMailboxId = highlightedMailboxId; 644 mCallback = callback; 645 } 646 647 @Override doInBackground(Void... params)648 protected Long[] doInBackground(Void... params) { 649 Mailbox parentMailbox = Mailbox.restoreMailboxWithId(mContext, mParentMailboxId); 650 final long nextParentId = (parentMailbox == null) ? Mailbox.NO_MAILBOX 651 : parentMailbox.mParentKey; 652 final long nextHighlightedId; 653 final long nextSelectedId; 654 if (mEnableHighlight) { 655 // If the "parent" is highlighted before the transition, it should still be 656 // highlighted after the upper level view. 657 if (mParentMailboxId == mHighlightedMailboxId) { 658 nextHighlightedId = mParentMailboxId; 659 } else { 660 // Otherwise, the next parent will be highlighted, unless we're going up to 661 // the root, in which case Inbox should be highlighted. 662 if (nextParentId == Mailbox.NO_MAILBOX) { 663 nextHighlightedId = Mailbox.findMailboxOfType(mContext, mAccountId, 664 Mailbox.TYPE_INBOX); 665 } else { 666 nextHighlightedId = nextParentId; 667 } 668 } 669 670 // Highlighted one will be "selected". 671 nextSelectedId = nextHighlightedId; 672 673 } else { // !mEnableHighlight 674 nextHighlightedId = Mailbox.NO_MAILBOX; 675 676 // Parent will be selected. 677 nextSelectedId = nextParentId; 678 } 679 return new Long[]{nextParentId, nextHighlightedId, nextSelectedId}; 680 } 681 682 @Override onSuccess(Long[] result)683 protected void onSuccess(Long[] result) { 684 mCallback.onResult(result[0], result[1], result[2]); 685 } 686 } 687 688 /** 689 * Starts the loader. 690 * 691 * @param parentMailboxId Mailbox ID to be used as the "parent" mailbox 692 * @param highlightedMailboxId Mailbox ID that should be highlighted when the data is loaded. 693 */ startLoading(long parentMailboxId, long highlightedMailboxId)694 private void startLoading(long parentMailboxId, long highlightedMailboxId) { 695 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 696 Log.d(Logging.LOG_TAG, this + " startLoading parent=" + parentMailboxId 697 + " highlighted=" + highlightedMailboxId); 698 } 699 final LoaderManager lm = getLoaderManager(); 700 boolean parentMailboxChanging = false; 701 702 // Parent mailbox changing -- destroy the current loader to force reload. 703 if (mParentMailboxId != parentMailboxId) { 704 lm.destroyLoader(MAILBOX_LOADER_ID); 705 setListShown(false); 706 parentMailboxChanging = true; 707 } 708 mParentMailboxId = parentMailboxId; 709 if (getEnableHighlight()) { 710 mNextHighlightedMailboxId = highlightedMailboxId; 711 } 712 713 lm.initLoader(MAILBOX_LOADER_ID, null, new MailboxListLoaderCallbacks()); 714 715 if (parentMailboxChanging) { 716 mCallback.onParentMailboxChanged(); 717 } 718 } 719 720 /** 721 * Highlight the given mailbox. 722 * 723 * If data is already loaded, it just sets {@link #mHighlightedMailboxId} and highlight the 724 * corresponding list item. (And if the corresponding list item is not found, 725 * {@link #mHighlightedMailboxId} is set to {@link Mailbox#NO_MAILBOX}) 726 * 727 * If we're still loading data, it sets {@link #mNextHighlightedMailboxId} instead, and then 728 * it'll be set to {@link #mHighlightedMailboxId} in 729 * {@link MailboxListLoaderCallbacks#onLoadFinished}. 730 * 731 * @param mailboxId The ID of the mailbox to highlight. 732 */ setHighlightedMailbox(long mailboxId)733 public void setHighlightedMailbox(long mailboxId) { 734 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 735 Log.d(Logging.LOG_TAG, this + " setHighlightedMailbox mailbox=" + mailboxId); 736 } 737 if (!getEnableHighlight()) { 738 return; 739 } 740 if (mHighlightedMailboxId == mailboxId) { 741 return; // already highlighted. 742 } 743 if (mListAdapter.getCursor() == null) { 744 // List not loaded yet. Just remember the ID here and let onLoadFinished() update 745 // mHighlightedMailboxId. 746 mNextHighlightedMailboxId = mailboxId; 747 return; 748 } 749 mHighlightedMailboxId = mailboxId; 750 updateHighlightedMailbox(true); 751 } 752 753 // TODO This class probably should be made static. There are many calls into the enclosing 754 // class and we need to be cautious about what we call while in these callbacks 755 private class MailboxListLoaderCallbacks implements LoaderCallbacks<Cursor> { 756 private boolean mIsFirstLoad; 757 758 @Override onCreateLoader(int id, Bundle args)759 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 760 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 761 Log.d(Logging.LOG_TAG, MailboxListFragment.this + " onCreateLoader"); 762 } 763 mIsFirstLoad = true; 764 if (getAccountId() == Account.ACCOUNT_ID_COMBINED_VIEW) { 765 return MailboxFragmentAdapter.createCombinedViewLoader(getActivity()); 766 } else { 767 return MailboxFragmentAdapter.createMailboxesLoader(getActivity(), getAccountId(), 768 mParentMailboxId); 769 } 770 } 771 772 @Override onLoadFinished(Loader<Cursor> loader, Cursor cursor)773 public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { 774 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 775 Log.d(Logging.LOG_TAG, MailboxListFragment.this + " onLoadFinished count=" 776 + cursor.getCount()); 777 } 778 // Note in onLoadFinished we can assume the view is created. 779 // The loader manager doesn't deliver results when a fragment is stopped. 780 781 // If we're showing a nested mailboxes, and the current parent mailbox has no children, 782 // go up. 783 if (getAccountId() != Account.ACCOUNT_ID_COMBINED_VIEW) { 784 MailboxFragmentAdapter.CursorWithExtras c = 785 (MailboxFragmentAdapter.CursorWithExtras) cursor; 786 if ((c.mChildCount == 0) && !isRoot()) { 787 // Always swap out the cursor so we don't hold a reference to a stale one. 788 mListAdapter.swapCursor(cursor); 789 navigateUp(); 790 return; 791 } 792 } 793 794 // Save list view state (primarily scroll position) 795 final ListView lv = getListView(); 796 final Parcelable listState; 797 if (mSavedListState != null) { 798 listState = mSavedListState; 799 mSavedListState = null; 800 } else { 801 listState = lv.onSaveInstanceState(); 802 } 803 804 if (cursor.getCount() == 0) { 805 // There's no row -- call setListShown(false) to make ListFragment show progress 806 // icon. 807 mListAdapter.swapCursor(null); 808 setListShown(false); 809 810 } else { 811 mParentDetermined = true; // Okay now we're sure which mailbox is the parent. 812 813 mListAdapter.swapCursor(cursor); 814 setListShown(true); 815 816 // Restore the list state, so scroll position is restored - this has to happen 817 // prior to setting the checked/highlighted mailbox below. 818 lv.onRestoreInstanceState(listState); 819 820 // Update the highlighted mailbox 821 if (mNextHighlightedMailboxId != Mailbox.NO_MAILBOX) { 822 setHighlightedMailbox(mNextHighlightedMailboxId); 823 mNextHighlightedMailboxId = Mailbox.NO_MAILBOX; 824 } 825 826 // We want to make visible the selection only for the first load. 827 // Re-load caused by content changed events shouldn't scroll the list. 828 if (!updateHighlightedMailbox(mIsFirstLoad)) { 829 // This may happen if the mailbox to be selected is not actually in the list 830 // that was loaded. Let the user just pick one manually if needed. 831 return; 832 } 833 } 834 835 // List has been reloaded; clear any drop target information 836 mDropTargetId = NO_DROP_TARGET; 837 mDropTargetView = null; 838 839 mIsFirstLoad = false; 840 } 841 842 @Override onLoaderReset(Loader<Cursor> loader)843 public void onLoaderReset(Loader<Cursor> loader) { 844 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 845 Log.d(Logging.LOG_TAG, MailboxListFragment.this + " onLoaderReset"); 846 } 847 mListAdapter.swapCursor(null); 848 } 849 } 850 851 /** 852 * {@inheritDoc} 853 * <p> 854 * @param doNotUse <em>IMPORTANT</em>: Do not use this parameter. The ID in the list widget 855 * must be a positive value. However, we rely on negative IDs for special mailboxes. Instead, 856 * we use the ID returned by {@link MailboxFragmentAdapter#getId(int)}. 857 */ 858 @Override onItemClick(AdapterView<?> parent, View view, int position, long doNotUse)859 public void onItemClick(AdapterView<?> parent, View view, int position, long doNotUse) { 860 final long id = mListAdapter.getId(position); 861 if (mListAdapter.isAccountRow(position)) { 862 mCallback.onAccountSelected(id); 863 } else if (mListAdapter.isMailboxRow(position)) { 864 // Save account-id. (Need to do this before startLoading() below, which will destroy 865 // the current loader and make the mListAdapter lose the cursor. 866 // Note, don't just use getAccountId(). A mailbox may tied to a different account ID 867 // from getAccountId(). (Currently "Starred" does so.) 868 long accountId = mListAdapter.getAccountId(position); 869 boolean nestedNavigation = false; 870 if (((MailboxListItem) view).isNavigable() && (id != mParentMailboxId)) { 871 // Drill-in. Selected one will be the next parent, and it'll also be highlighted. 872 startLoading(id, id); 873 nestedNavigation = true; 874 } 875 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { 876 // Virtual mailboxes, such as "Starred", will have a "combined view" ID. However, 877 // we really want to relay the current active account, so that 878 // things like per-account starred mailboxes work as expected. 879 accountId = getAccountId(); 880 } 881 mCallback.onMailboxSelected(accountId, id, nestedNavigation); 882 } 883 } 884 885 /** 886 * Really highlight the mailbox for {@link #mHighlightedMailboxId} on the list view. 887 * 888 * Note if a list item for {@link #mHighlightedMailboxId} is not found, 889 * {@link #mHighlightedMailboxId} will be set to {@link Mailbox#NO_MAILBOX}. 890 * 891 * @return false when the highlighted mailbox seems to be gone; i.e. if 892 * {@link #mHighlightedMailboxId} is set but not found in the list. 893 */ updateHighlightedMailbox(boolean ensureSelectionVisible)894 private boolean updateHighlightedMailbox(boolean ensureSelectionVisible) { 895 if (!getEnableHighlight() || !isViewCreated()) { 896 return true; // Nothing to highlight 897 } 898 final ListView lv = getListView(); 899 boolean found = false; 900 if (mHighlightedMailboxId == Mailbox.NO_MAILBOX) { 901 // No mailbox selected 902 lv.clearChoices(); 903 found = true; 904 } else { 905 // TODO Don't mix list view & list adapter indices. This is a recipe for disaster. 906 final int count = lv.getCount(); 907 for (int i = 0; i < count; i++) { 908 if (mListAdapter.getId(i) != mHighlightedMailboxId) { 909 continue; 910 } 911 found = true; 912 lv.setItemChecked(i, true); 913 if (ensureSelectionVisible) { 914 Utility.listViewSmoothScrollToPosition(getActivity(), lv, i); 915 } 916 break; 917 } 918 } 919 if (!found) { 920 mHighlightedMailboxId = Mailbox.NO_MAILBOX; 921 } 922 return found; 923 } 924 925 // Drag & Drop handling 926 927 /** 928 * Update all of the list's child views with the proper target background (for now, orange if 929 * a valid target, except red if the trash; standard background otherwise) 930 */ updateChildViews()931 private void updateChildViews() { 932 final ListView lv = getListView(); 933 int itemCount = lv.getChildCount(); 934 // Lazily initialize the height of our list items 935 if (itemCount > 0 && mDragItemHeight < 0) { 936 mDragItemHeight = lv.getChildAt(0).getHeight(); 937 } 938 for (int i = 0; i < itemCount; i++) { 939 final View child = lv.getChildAt(i); 940 if (!(child instanceof MailboxListItem)) { 941 continue; 942 } 943 MailboxListItem item = (MailboxListItem) child; 944 item.setDropTargetBackground(mDragInProgress, mDragItemMailboxId); 945 } 946 } 947 948 /** 949 * Called when the user has dragged outside of the mailbox list area. 950 */ onDragExited()951 private void onDragExited() { 952 // Reset the background of the current target 953 if (mDropTargetView != null) { 954 mDropTargetView.setDropTargetBackground(mDragInProgress, mDragItemMailboxId); 955 mDropTargetView = null; 956 } 957 mDropTargetId = NO_DROP_TARGET; 958 stopScrolling(); 959 } 960 961 /** 962 * Called while dragging; highlight possible drop targets, and auto scroll the list. 963 */ onDragLocation(DragEvent event)964 private void onDragLocation(DragEvent event) { 965 final ListView lv = getListView(); 966 // TODO The list may be changing while in drag-n-drop; temporarily suspend drag-n-drop 967 // if the list is being updated [i.e. navigated to another mailbox] 968 if (mDragItemHeight <= 0) { 969 // This shouldn't be possible, but avoid NPE 970 Log.w(TAG, "drag item height is not set"); 971 return; 972 } 973 // Find out which item we're in and highlight as appropriate 974 final int rawTouchX = (int) event.getX(); 975 final int rawTouchY = (int) event.getY(); 976 final int viewIndex = pointToIndex(lv, rawTouchX, rawTouchY); 977 int targetId = viewIndex; 978 if (targetId != mDropTargetId) { 979 if (DEBUG_DRAG_DROP) { 980 Log.d(TAG, "=== Target changed; oldId: " + mDropTargetId + ", newId: " + targetId); 981 } 982 // Remove highlight the current target; if there was one 983 if (mDropTargetView != null) { 984 mDropTargetView.setDropTargetBackground(true, mDragItemMailboxId); 985 mDropTargetView = null; 986 } 987 // Get the new target mailbox view 988 final View childView = lv.getChildAt(viewIndex); 989 final MailboxListItem newTarget; 990 if (childView == null) { 991 // In any event, we're no longer dragging in the list view if newTarget is null 992 if (DEBUG_DRAG_DROP) { 993 Log.d(TAG, "=== Drag off the list"); 994 } 995 newTarget = null; 996 final int childCount = lv.getChildCount(); 997 if (viewIndex >= childCount) { 998 // Touching beyond the end of the list; may happen for small lists 999 onDragExited(); 1000 return; 1001 } else { 1002 // We should never get here 1003 Log.w(TAG, "null view; idx: " + viewIndex + ", cnt: " + childCount); 1004 } 1005 } else if (!(childView instanceof MailboxListItem)) { 1006 // We're over a header suchas "Recent folders". We shouldn't finish DnD, but 1007 // drop should be disabled. 1008 newTarget = null; 1009 targetId = NO_DROP_TARGET; 1010 } else { 1011 newTarget = (MailboxListItem) childView; 1012 if (newTarget.mMailboxType == Mailbox.TYPE_TRASH) { 1013 if (DEBUG_DRAG_DROP) { 1014 Log.d(TAG, "=== Trash mailbox; id: " + newTarget.mMailboxId); 1015 } 1016 newTarget.setDropTrashBackground(); 1017 } else if (newTarget.isDropTarget(mDragItemMailboxId)) { 1018 if (DEBUG_DRAG_DROP) { 1019 Log.d(TAG, "=== Target mailbox; id: " + newTarget.mMailboxId); 1020 } 1021 newTarget.setDropActiveBackground(); 1022 } else { 1023 if (DEBUG_DRAG_DROP) { 1024 Log.d(TAG, "=== Non-droppable mailbox; id: " + newTarget.mMailboxId); 1025 } 1026 newTarget.setDropTargetBackground(true, mDragItemMailboxId); 1027 targetId = NO_DROP_TARGET; 1028 } 1029 } 1030 // Save away our current position and view 1031 mDropTargetId = targetId; 1032 mDropTargetView = newTarget; 1033 } 1034 1035 // This is a quick-and-dirty implementation of drag-under-scroll; something like this 1036 // should eventually find its way into the framework 1037 int scrollDiff = rawTouchY - (lv.getHeight() - SCROLL_ZONE_SIZE); 1038 boolean scrollDown = (scrollDiff > 0); 1039 boolean scrollUp = (SCROLL_ZONE_SIZE > rawTouchY); 1040 if (!mTargetScrolling && scrollDown) { 1041 int itemsToScroll = lv.getCount() - lv.getLastVisiblePosition(); 1042 int pixelsToScroll = (itemsToScroll + 1) * mDragItemHeight; 1043 lv.smoothScrollBy(pixelsToScroll, pixelsToScroll * SCROLL_SPEED); 1044 if (DEBUG_DRAG_DROP) { 1045 Log.d(TAG, "=== Start scrolling list down"); 1046 } 1047 mTargetScrolling = true; 1048 } else if (!mTargetScrolling && scrollUp) { 1049 int pixelsToScroll = (lv.getFirstVisiblePosition() + 1) * mDragItemHeight; 1050 lv.smoothScrollBy(-pixelsToScroll, pixelsToScroll * SCROLL_SPEED); 1051 if (DEBUG_DRAG_DROP) { 1052 Log.d(TAG, "=== Start scrolling list up"); 1053 } 1054 mTargetScrolling = true; 1055 } else if (!scrollUp && !scrollDown) { 1056 stopScrolling(); 1057 } 1058 } 1059 1060 /** 1061 * Indicate that scrolling has stopped 1062 */ stopScrolling()1063 private void stopScrolling() { 1064 final ListView lv = getListView(); 1065 if (mTargetScrolling) { 1066 mTargetScrolling = false; 1067 if (DEBUG_DRAG_DROP) { 1068 Log.d(TAG, "=== Stop scrolling list"); 1069 } 1070 // Stop the scrolling 1071 lv.smoothScrollBy(0, 0); 1072 } 1073 } 1074 onDragEnded()1075 private void onDragEnded() { 1076 if (mDragInProgress) { 1077 mDragInProgress = false; 1078 // Reenable updates to the view and redraw (in case it changed) 1079 MailboxFragmentAdapter.enableUpdates(true); 1080 mListAdapter.notifyDataSetChanged(); 1081 // Stop highlighting targets 1082 updateChildViews(); 1083 // Stop any scrolling that was going on 1084 stopScrolling(); 1085 } 1086 } 1087 onDragStarted(DragEvent event)1088 private boolean onDragStarted(DragEvent event) { 1089 // We handle dropping of items with our email mime type 1090 // If the mime type has a mailbox id appended, that is the mailbox of the item 1091 // being draged 1092 ClipDescription description = event.getClipDescription(); 1093 int mimeTypeCount = description.getMimeTypeCount(); 1094 for (int i = 0; i < mimeTypeCount; i++) { 1095 String mimeType = description.getMimeType(i); 1096 if (mimeType.startsWith(EmailProvider.EMAIL_MESSAGE_MIME_TYPE)) { 1097 if (DEBUG_DRAG_DROP) { 1098 Log.d(TAG, "=== Drag started"); 1099 } 1100 mDragItemMailboxId = -1; 1101 // See if we find a mailbox id here 1102 int dash = mimeType.lastIndexOf('-'); 1103 if (dash > 0) { 1104 try { 1105 mDragItemMailboxId = Long.parseLong(mimeType.substring(dash + 1)); 1106 } catch (NumberFormatException e) { 1107 // Ignore; we just won't know the mailbox 1108 } 1109 } 1110 mDragInProgress = true; 1111 // Stop the list from updating 1112 MailboxFragmentAdapter.enableUpdates(false); 1113 // Update the backgrounds of our child views to highlight drop targets 1114 updateChildViews(); 1115 return true; 1116 } 1117 } 1118 return false; 1119 } 1120 1121 /** 1122 * Perform a "drop" action. If the user is not on top of a valid drop target, no action 1123 * is performed. 1124 * @return {@code true} if the drop action was performed. Otherwise {@code false}. 1125 */ onDrop(DragEvent event)1126 private boolean onDrop(DragEvent event) { 1127 stopScrolling(); 1128 // If we're not on a target, we're done 1129 if (mDropTargetId == NO_DROP_TARGET) { 1130 return false; 1131 } 1132 final Controller controller = Controller.getInstance(mActivity); 1133 ClipData clipData = event.getClipData(); 1134 int count = clipData.getItemCount(); 1135 if (DEBUG_DRAG_DROP) { 1136 Log.d(TAG, "=== Dropping " + count + " items."); 1137 } 1138 // Extract the messageId's to move from the ClipData (set up in MessageListItem) 1139 final long[] messageIds = new long[count]; 1140 for (int i = 0; i < count; i++) { 1141 Uri uri = clipData.getItemAt(i).getUri(); 1142 String msgNum = uri.getPathSegments().get(1); 1143 long id = Long.parseLong(msgNum); 1144 messageIds[i] = id; 1145 } 1146 // Call either deleteMessage or moveMessage, depending on the target 1147 if (mDropTargetView.mMailboxType == Mailbox.TYPE_TRASH) { 1148 controller.deleteMessages(messageIds); 1149 } else { 1150 controller.moveMessages(messageIds, mDropTargetView.mMailboxId); 1151 } 1152 return true; 1153 } 1154 1155 @Override onDrag(View view, DragEvent event)1156 public boolean onDrag(View view, DragEvent event) { 1157 boolean result = false; 1158 switch (event.getAction()) { 1159 case DragEvent.ACTION_DRAG_STARTED: 1160 result = onDragStarted(event); 1161 break; 1162 case DragEvent.ACTION_DRAG_ENTERED: 1163 // The drag has entered the ListView window 1164 if (DEBUG_DRAG_DROP) { 1165 Log.d(TAG, "=== Drag entered; targetId: " + mDropTargetId); 1166 } 1167 break; 1168 case DragEvent.ACTION_DRAG_EXITED: 1169 // The drag has left the building 1170 if (DEBUG_DRAG_DROP) { 1171 Log.d(TAG, "=== Drag exited; targetId: " + mDropTargetId); 1172 } 1173 onDragExited(); 1174 break; 1175 case DragEvent.ACTION_DRAG_ENDED: 1176 // The drag is over 1177 if (DEBUG_DRAG_DROP) { 1178 Log.d(TAG, "=== Drag ended"); 1179 } 1180 onDragEnded(); 1181 break; 1182 case DragEvent.ACTION_DRAG_LOCATION: 1183 // We're moving around within our window; handle scroll, if necessary 1184 onDragLocation(event); 1185 break; 1186 case DragEvent.ACTION_DROP: 1187 // The drag item was dropped 1188 if (DEBUG_DRAG_DROP) { 1189 Log.d(TAG, "=== Drop"); 1190 } 1191 result = onDrop(event); 1192 break; 1193 default: 1194 break; 1195 } 1196 return result; 1197 } 1198 } 1199