MoveMessageToDialog.java revision 8b9f2a7284337bfb23d5e2f8de9f1c70cb9532a1
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
17package com.android.email.activity;
18
19import com.android.email.R;
20import com.android.email.Utility;
21import com.android.email.provider.EmailContent.Account;
22import com.android.email.provider.EmailContent.Mailbox;
23import com.android.email.provider.EmailContent.Message;
24
25import android.app.Activity;
26import android.app.AlertDialog;
27import android.app.Dialog;
28import android.app.DialogFragment;
29import android.app.Fragment;
30import android.app.LoaderManager;
31import android.content.AsyncTaskLoader;
32import android.content.Context;
33import android.content.DialogInterface;
34import android.content.Loader;
35import android.database.Cursor;
36import android.os.Bundle;
37import android.os.Handler;
38
39import java.security.InvalidParameterException;
40
41/**
42 * "Move (messages) to" dialog.
43 *
44 * TODO The check logic in MessageCheckerCallback is not efficient.  It shouldn't restore full
45 * Message objects.  But we don't bother at this point as the UI is still temporary.
46 */
47public class MoveMessageToDialog extends DialogFragment implements DialogInterface.OnClickListener {
48    private static final String BUNDLE_MESSAGE_IDS = "message_ids";
49
50    /** Message IDs passed to {@link #newInstance} */
51    private long[] mMessageIds;
52    private MailboxesAdapter mAdapter;
53
54    /** Account ID is restored by {@link MailboxesLoaderCallbacks} */
55    private long mAccountId;
56
57    private boolean mDestroyed;
58
59    /**
60     * Callback that target fragments, or the owner activity should implement.
61     */
62    public interface Callback {
63        public void onMoveToMailboxSelected(long newMailboxId, long[] messageIds);
64    }
65
66    /**
67     * Create and return a new instance.
68     *
69     * @param parent owner activity.
70     * @param messageIds IDs of the messages to be moved.
71     * @param callbackFragment Fragment that gets a callback.  The fragment must implement
72     * {@link Callback}.  If null is passed, then the owner activity is used instead, in which case
73     * it must implement {@link Callback} instead.
74     */
75    public static MoveMessageToDialog newInstance(Activity parent,
76            long[] messageIds, Fragment callbackFragment) {
77        if (messageIds.length == 0) {
78            throw new InvalidParameterException();
79        }
80        MoveMessageToDialog dialog = new MoveMessageToDialog();
81        Bundle args = new Bundle();
82        args.putLongArray(BUNDLE_MESSAGE_IDS, messageIds);
83        dialog.setArguments(args);
84        if (callbackFragment != null) {
85            dialog.setTargetFragment(callbackFragment, 0);
86        }
87        return dialog;
88    }
89
90    @Override
91    public void onCreate(Bundle savedInstanceState) {
92        super.onCreate(savedInstanceState);
93        mMessageIds = getArguments().getLongArray(BUNDLE_MESSAGE_IDS);
94        setStyle(STYLE_NORMAL, android.R.style.Theme_Light_Holo);
95    }
96
97    @Override
98    public void onDestroy() {
99        LoaderManager lm = getActivity().getLoaderManager();
100        lm.destroyLoader(ActivityHelper.GLOBAL_LOADER_ID_MOVE_TO_DIALOG_MESSAGE_CHECKER);
101        lm.destroyLoader(ActivityHelper.GLOBAL_LOADER_ID_MOVE_TO_DIALOG_MAILBOX_LOADER);
102        mDestroyed = true;
103        super.onDestroy();
104    }
105
106    @Override
107    public Dialog onCreateDialog(Bundle savedInstanceState) {
108        final Activity activity = getActivity();
109
110        // Build adapter & dialog
111        // Make sure to pass Builder's context to the adapter, so that it'll get the correct theme.
112        AlertDialog.Builder builder = new AlertDialog.Builder(activity)
113                .setTitle(activity.getResources().getString(R.string.move_to_folder_dialog_title));
114
115        mAdapter =
116            new MailboxesAdapter(builder.getContext(), MailboxesAdapter.MODE_MOVE_TO_TARGET,
117                    new MailboxesAdapter.EmptyCallback());
118        builder.setSingleChoiceItems(mAdapter, -1, this);
119
120        activity.getLoaderManager().initLoader(
121                ActivityHelper.GLOBAL_LOADER_ID_MOVE_TO_DIALOG_MESSAGE_CHECKER,
122                null, new MessageCheckerCallback());
123
124        return builder.show();
125    }
126
127    @Override
128    public void onClick(DialogInterface dialog, int position) {
129        final long mailboxId = mAdapter.getItemId(position);
130
131        getCallback().onMoveToMailboxSelected(mailboxId, mMessageIds);
132        dismiss();
133    }
134
135    private Callback getCallback() {
136        Fragment targetFragment = getTargetFragment();
137        if (targetFragment != null) {
138            // If a target is set, it MUST implement Callback.
139            return (Callback) targetFragment;
140        }
141        // If not the parent activity MUST implement Callback.
142        return (Callback) getActivity();
143    }
144
145    /**
146     * Delay-call {@link #dismiss()} using a {@link Handler}.  Calling {@link #dismiss()} from
147     * {@link LoaderManager.LoaderCallbacks#onLoadFinished} is not allowed, so we use it instead.
148     */
149    private void dismissAsync() {
150        new Handler().post(new Runnable() {
151            @Override
152            public void run() {
153                if (!mDestroyed) {
154                    dismiss();
155                }
156            }
157        });
158    }
159
160    /**
161     * Loader callback for {@link MessageChecker}
162     */
163    private class MessageCheckerCallback implements LoaderManager.LoaderCallbacks<Long> {
164        @Override
165        public Loader<Long> onCreateLoader(int id, Bundle args) {
166            return new MessageChecker(getActivity(), mMessageIds);
167        }
168
169        @Override
170        public void onLoadFinished(Loader<Long> loader, Long accountId) {
171            if (mDestroyed) {
172                return;
173            }
174            // accountId shouldn't be null, but I'm paranoia.
175            if ((accountId == null) || (accountId == -1)) {
176                // Some of the messages can't be moved.  Close the dialog.
177                dismissAsync();
178                return;
179            }
180            mAccountId = accountId;
181            getActivity().getLoaderManager().initLoader(
182                    ActivityHelper.GLOBAL_LOADER_ID_MOVE_TO_DIALOG_MAILBOX_LOADER,
183                    null, new MailboxesLoaderCallbacks());
184        }
185
186        @Override
187        public void onLoaderReset(Loader<Long> loader) {
188        }
189    }
190
191    /**
192     * Loader callback for destination mailbox list.
193     */
194    private class MailboxesLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
195        @Override
196        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
197            return MailboxesAdapter.createLoader(getActivity().getApplicationContext(), mAccountId,
198                    MailboxesAdapter.MODE_MOVE_TO_TARGET);
199        }
200
201        @Override
202        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
203            if (mDestroyed) {
204                return;
205            }
206            mAdapter.swapCursor(data);
207        }
208
209        @Override
210        public void onLoaderReset(Loader<Cursor> loader) {
211            mAdapter.swapCursor(null);
212        }
213    }
214
215    /**
216     * A loader that checks if the messages can be moved, and return the Id of the account that owns
217     * the messages.  (If any of the messages can't be moved, return -1.)
218     */
219    private static class MessageChecker extends AsyncTaskLoader<Long> {
220        private final Activity mActivity;
221        private final long[] mMessageIds;
222
223        public MessageChecker(Activity activity, long[] messageIds) {
224            super(activity);
225            mActivity = activity;
226            mMessageIds = messageIds;
227        }
228
229        @Override
230        public Long loadInBackground() {
231            final Context c = getContext();
232
233            long accountId = -1; // -1 == account not found yet.
234
235            for (long messageId : mMessageIds) {
236                // TODO This shouln't restore a full Message object.
237                final Message message = Message.restoreMessageWithId(c, messageId);
238                if (message == null) {
239                    continue; // Skip removed messages.
240                }
241
242                // First, check account.
243                if (accountId == -1) {
244                    // First message -- see if the account supports move.
245                    accountId = message.mAccountKey;
246                    if (!Account.supportsMoveMessages(c, accountId)) {
247                        Utility.showToast(
248                                mActivity, R.string.cannot_move_protocol_not_supported_toast);
249                        return -1L;
250                    }
251                } else {
252                    // Following messages -- have to belong to the same account
253                    if (message.mAccountKey != accountId) {
254                        Utility.showToast(mActivity, R.string.cannot_move_multiple_accounts_toast);
255                        return -1L;
256                    }
257                }
258                // Second, check mailbox.
259                if (!Mailbox.canMoveFrom(c, message.mMailboxKey)) {
260                    Utility.showToast(mActivity, R.string.cannot_move_special_messages);
261                    return -1L;
262                }
263            }
264
265            // If all messages have been removed, accountId remains -1, which is what we should
266            // return here.
267            return accountId;
268        }
269
270        @Override
271        protected void onStartLoading() {
272            cancelLoad();
273            forceLoad();
274        }
275
276        @Override
277        protected void onStopLoading() {
278            cancelLoad();
279        }
280
281        @Override
282        protected void onReset() {
283            stopLoading();
284        }
285    }
286}
287