Home | History | Annotate | Download | only in activity
      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 
     17 package com.android.email.activity;
     18 
     19 import android.app.Activity;
     20 import android.app.AlertDialog;
     21 import android.app.Dialog;
     22 import android.app.DialogFragment;
     23 import android.app.Fragment;
     24 import android.app.LoaderManager;
     25 import android.content.AsyncTaskLoader;
     26 import android.content.Context;
     27 import android.content.DialogInterface;
     28 import android.content.Loader;
     29 import android.database.Cursor;
     30 import android.os.Bundle;
     31 import android.os.Handler;
     32 import android.util.Log;
     33 
     34 import com.android.email.Email;
     35 import com.android.email.R;
     36 import com.android.emailcommon.Logging;
     37 import com.android.emailcommon.provider.Account;
     38 import com.android.emailcommon.provider.EmailContent.Message;
     39 import com.android.emailcommon.provider.Mailbox;
     40 import 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  */
     50 public 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