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