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