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 android.app.Activity;
20import android.app.AlertDialog;
21import android.app.Dialog;
22import android.app.DialogFragment;
23import android.app.Fragment;
24import android.app.LoaderManager;
25import android.content.AsyncTaskLoader;
26import android.content.Context;
27import android.content.DialogInterface;
28import android.content.Loader;
29import android.database.Cursor;
30import android.os.Bundle;
31import android.os.Handler;
32import android.util.Log;
33
34import com.android.email.Email;
35import com.android.email.R;
36import com.android.emailcommon.Logging;
37import com.android.emailcommon.provider.Account;
38import com.android.emailcommon.provider.EmailContent.Message;
39import com.android.emailcommon.provider.Mailbox;
40import 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 */
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    /** 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 {
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     */
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
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
108    public void onDestroy() {
109        mDestroyed = true;
110        super.onDestroy();
111    }
112
113    @Override
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
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
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
162    public void onDetach() {
163        super.onDetach();
164
165        if (sActiveDialog == this) {
166            sActiveDialog = null;
167        }
168    }
169
170    @Override
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     */
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
199        public Loader<IdContainer> onCreateLoader(int id, Bundle args) {
200            return new MessageChecker(getActivity(), mMessageIds);
201        }
202
203        @Override
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
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
232        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
233            return MailboxMoveToAdapter.createLoader(getActivity().getApplicationContext(),
234                    mAccountId, mMailboxId);
235        }
236
237        @Override
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
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
267        public MessageChecker(Activity activity, long[] messageIds) {
268            super(activity);
269            mActivity = activity;
270            mMessageIds = messageIds;
271        }
272
273        @Override
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
319        protected void onStartLoading() {
320            cancelLoad();
321            forceLoad();
322        }
323
324        @Override
325        protected void onStopLoading() {
326            cancelLoad();
327        }
328
329        @Override
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
340        private IdContainer(long accountId, long mailboxId) {
341            mAccountId = accountId;
342            mMailboxId = mailboxId;
343        }
344    }
345}
346