• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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