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