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.AlertDialog; 21 import android.app.Dialog; 22 import android.app.DialogFragment; 23 import android.app.Fragment; 24 import android.app.LoaderManager; 25 import android.content.AsyncTaskLoader; 26 import android.content.Context; 27 import android.content.DialogInterface; 28 import android.content.Loader; 29 import android.database.Cursor; 30 import android.os.Bundle; 31 import android.os.Handler; 32 import android.util.Log; 33 34 import com.android.email.Email; 35 import com.android.email.R; 36 import com.android.emailcommon.Logging; 37 import com.android.emailcommon.provider.Account; 38 import com.android.emailcommon.provider.EmailContent.Message; 39 import com.android.emailcommon.provider.Mailbox; 40 import com.android.emailcommon.utility.Utility; 41 42 /** 43 * "Move (messages) to" dialog. This is a modal dialog and the design is such so that only one is 44 * active. If a new instance is created while an existing one is active, the existing one is 45 * dismissed. 46 * 47 * TODO The check logic in MessageCheckerCallback is not efficient. It shouldn't restore full 48 * Message objects. 49 */ 50 public class MoveMessageToDialog extends DialogFragment implements DialogInterface.OnClickListener { 51 private static final String BUNDLE_MESSAGE_IDS = "message_ids"; 52 53 private static final int LOADER_ID_MOVE_TO_DIALOG_MAILBOX_LOADER = 1; 54 private static final int LOADER_ID_MOVE_TO_DIALOG_MESSAGE_CHECKER = 2; 55 56 /** Message IDs passed to {@link #newInstance} */ 57 private long[] mMessageIds; 58 private MailboxMoveToAdapter mAdapter; 59 60 /** ID of the account that contains all of the messages to move */ 61 private long mAccountId; 62 /** ID of the mailbox that contains all of the messages to move */ 63 private long mMailboxId; 64 65 private boolean mDestroyed; 66 67 /** 68 * Callback that target fragments, or the owner activity should implement. 69 */ 70 public interface Callback { onMoveToMailboxSelected(long newMailboxId, long[] messageIds)71 public void onMoveToMailboxSelected(long newMailboxId, long[] messageIds); 72 } 73 74 /** 75 * Create and return a new instance. 76 * 77 * @param messageIds IDs of the messages to be moved. 78 * @param callbackFragment Fragment that gets a callback. The fragment must implement 79 * {@link Callback}. 80 */ newInstance(long[] messageIds, T callbackFragment)81 public static <T extends Fragment & Callback> MoveMessageToDialog newInstance(long[] messageIds, 82 T callbackFragment) { 83 if (messageIds.length == 0) { 84 throw new IllegalArgumentException(); 85 } 86 if (callbackFragment == null) { 87 throw new IllegalArgumentException(); // fail fast 88 } 89 MoveMessageToDialog dialog = new MoveMessageToDialog(); 90 Bundle args = new Bundle(); 91 args.putLongArray(BUNDLE_MESSAGE_IDS, messageIds); 92 dialog.setArguments(args); 93 dialog.setTargetFragment(callbackFragment, 0); 94 return dialog; 95 } 96 97 @Override onCreate(Bundle savedInstanceState)98 public void onCreate(Bundle savedInstanceState) { 99 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 100 Log.d(Logging.LOG_TAG, "" + this + " onCreate target=" + getTargetFragment()); 101 } 102 super.onCreate(savedInstanceState); 103 mMessageIds = getArguments().getLongArray(BUNDLE_MESSAGE_IDS); 104 setStyle(STYLE_NORMAL, android.R.style.Theme_Holo_Light); 105 } 106 107 @Override onDestroy()108 public void onDestroy() { 109 mDestroyed = true; 110 super.onDestroy(); 111 } 112 113 @Override onCreateDialog(Bundle savedInstanceState)114 public Dialog onCreateDialog(Bundle savedInstanceState) { 115 final Activity activity = getActivity(); 116 117 // Build adapter & dialog 118 // Make sure to pass Builder's context to the adapter, so that it'll get the correct theme. 119 AlertDialog.Builder builder = new AlertDialog.Builder(activity) 120 .setTitle(activity.getResources().getString(R.string.move_to_folder_dialog_title)); 121 122 mAdapter = new MailboxMoveToAdapter(builder.getContext()); 123 builder.setSingleChoiceItems(mAdapter, -1, this); 124 125 getLoaderManager().initLoader( 126 LOADER_ID_MOVE_TO_DIALOG_MESSAGE_CHECKER, 127 null, new MessageCheckerCallback()); 128 129 return builder.create(); 130 } 131 132 @Override onStart()133 public void onStart() { 134 super.onStart(); 135 136 if (mAdapter.getCursor() == null) { 137 // Data isn't ready - don't show yet. 138 getDialog().hide(); 139 } 140 } 141 142 /** 143 * The active move message dialog. This dialog is fairly modal so it only makes sense to have 144 * one instance active, and for debounce purposes, we dismiss any existing ones. 145 * 146 * Only touched on the UI thread so doesn't require synchronization. 147 */ 148 static MoveMessageToDialog sActiveDialog; 149 150 @Override onAttach(Activity activity)151 public void onAttach(Activity activity) { 152 super.onAttach(activity); 153 if (sActiveDialog != null) { 154 // Something is already attached. Dismiss it! 155 sActiveDialog.dismissAsync(); 156 } 157 158 sActiveDialog = this; 159 } 160 161 @Override onDetach()162 public void onDetach() { 163 super.onDetach(); 164 165 if (sActiveDialog == this) { 166 sActiveDialog = null; 167 } 168 } 169 170 @Override onClick(DialogInterface dialog, int position)171 public void onClick(DialogInterface dialog, int position) { 172 final long mailboxId = mAdapter.getItemId(position); 173 174 ((Callback) getTargetFragment()).onMoveToMailboxSelected(mailboxId, mMessageIds); 175 dismiss(); 176 } 177 178 /** 179 * Delay-call {@link #dismissAllowingStateLoss()} using a {@link Handler}. Calling 180 * {@link #dismissAllowingStateLoss()} from {@link LoaderManager.LoaderCallbacks#onLoadFinished} 181 * is not allowed, so we use it instead. 182 */ dismissAsync()183 private void dismissAsync() { 184 Utility.getMainThreadHandler().post(new Runnable() { 185 @Override 186 public void run() { 187 if (!mDestroyed) { 188 dismissAllowingStateLoss(); 189 } 190 } 191 }); 192 } 193 194 /** 195 * Loader callback for {@link MessageChecker} 196 */ 197 private class MessageCheckerCallback implements LoaderManager.LoaderCallbacks<IdContainer> { 198 @Override onCreateLoader(int id, Bundle args)199 public Loader<IdContainer> onCreateLoader(int id, Bundle args) { 200 return new MessageChecker(getActivity(), mMessageIds); 201 } 202 203 @Override onLoadFinished(Loader<IdContainer> loader, IdContainer idSet)204 public void onLoadFinished(Loader<IdContainer> loader, IdContainer idSet) { 205 if (mDestroyed) { 206 return; 207 } 208 // accountId shouldn't be null, but I'm paranoia. 209 if (idSet == null || idSet.mAccountId == Account.NO_ACCOUNT 210 || idSet.mMailboxId == Mailbox.NO_MAILBOX) { 211 // Some of the messages can't be moved. Close the dialog. 212 dismissAsync(); 213 return; 214 } 215 mAccountId = idSet.mAccountId; 216 mMailboxId = idSet.mMailboxId; 217 getLoaderManager().initLoader( 218 LOADER_ID_MOVE_TO_DIALOG_MAILBOX_LOADER, 219 null, new MailboxesLoaderCallbacks()); 220 } 221 222 @Override onLoaderReset(Loader<IdContainer> loader)223 public void onLoaderReset(Loader<IdContainer> loader) { 224 } 225 } 226 227 /** 228 * Loader callback for destination mailbox list. 229 */ 230 private class MailboxesLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> { 231 @Override onCreateLoader(int id, Bundle args)232 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 233 return MailboxMoveToAdapter.createLoader(getActivity().getApplicationContext(), 234 mAccountId, mMailboxId); 235 } 236 237 @Override onLoadFinished(Loader<Cursor> loader, Cursor data)238 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 239 if (mDestroyed) { 240 return; 241 } 242 boolean needsShowing = (mAdapter.getCursor() == null); 243 mAdapter.swapCursor(data); 244 245 // The first time data is loaded, we need to show the dialog. 246 if (needsShowing && isAdded()) { 247 getDialog().show(); 248 } 249 } 250 251 @Override onLoaderReset(Loader<Cursor> loader)252 public void onLoaderReset(Loader<Cursor> loader) { 253 mAdapter.swapCursor(null); 254 } 255 } 256 257 /** 258 * A loader that checks if the messages can be moved. If messages can be moved, it returns 259 * the account and mailbox IDs where the messages are currently located. If any the messages 260 * cannot be moved (such as the messages belong to different accounts), the IDs returned 261 * will be {@link Account#NO_ACCOUNT} and {@link Mailbox#NO_MAILBOX}. 262 */ 263 private static class MessageChecker extends AsyncTaskLoader<IdContainer> { 264 private final Activity mActivity; 265 private final long[] mMessageIds; 266 MessageChecker(Activity activity, long[] messageIds)267 public MessageChecker(Activity activity, long[] messageIds) { 268 super(activity); 269 mActivity = activity; 270 mMessageIds = messageIds; 271 } 272 273 @Override loadInBackground()274 public IdContainer loadInBackground() { 275 final Context c = getContext(); 276 277 long accountId = Account.NO_ACCOUNT; 278 long mailboxId = Mailbox.NO_MAILBOX; 279 280 for (long messageId : mMessageIds) { 281 // TODO This shouln't restore a full Message object. 282 final Message message = Message.restoreMessageWithId(c, messageId); 283 if (message == null) { 284 continue; // Skip removed messages. 285 } 286 287 // First, check account. 288 if (accountId == Account.NO_ACCOUNT) { 289 // First, check if the account supports move 290 accountId = message.mAccountKey; 291 if (!Account.restoreAccountWithId(c, accountId).supportsMoveMessages(c)) { 292 Utility.showToast( 293 mActivity, R.string.cannot_move_protocol_not_supported_toast); 294 accountId = Account.NO_ACCOUNT; 295 break; 296 } 297 mailboxId = message.mMailboxKey; 298 // Second, check if the mailbox supports move 299 if (!Mailbox.restoreMailboxWithId(c, mailboxId).canHaveMessagesMoved()) { 300 Utility.showToast(mActivity, R.string.cannot_move_special_mailboxes_toast); 301 accountId = Account.NO_ACCOUNT; 302 mailboxId = Mailbox.NO_MAILBOX; 303 break; 304 } 305 } else { 306 // Subsequent messages; all messages must to belong to the same mailbox 307 if (message.mAccountKey != accountId || message.mMailboxKey != mailboxId) { 308 Utility.showToast(mActivity, R.string.cannot_move_multiple_accounts_toast); 309 accountId = Account.NO_ACCOUNT; 310 mailboxId = Mailbox.NO_MAILBOX; 311 break; 312 } 313 } 314 } 315 return new IdContainer(accountId, mailboxId); 316 } 317 318 @Override onStartLoading()319 protected void onStartLoading() { 320 cancelLoad(); 321 forceLoad(); 322 } 323 324 @Override onStopLoading()325 protected void onStopLoading() { 326 cancelLoad(); 327 } 328 329 @Override onReset()330 protected void onReset() { 331 stopLoading(); 332 } 333 } 334 335 /** Container for multiple types of IDs */ 336 private static class IdContainer { 337 private final long mAccountId; 338 private final long mMailboxId; 339 IdContainer(long accountId, long mailboxId)340 private IdContainer(long accountId, long mailboxId) { 341 mAccountId = accountId; 342 mMailboxId = mailboxId; 343 } 344 } 345 } 346