Add a bunch of stuff missed earlier

Change-Id: I7f707446a963912fe5786dacb5569e68db572d1c
This commit is contained in:
Marc Blank 2012-06-28 12:16:59 -07:00
parent f419287f22
commit c5afb16430
39 changed files with 8625 additions and 0 deletions

30
emailsync/Android.mk Normal file
View File

@ -0,0 +1,30 @@
# Copyright 2012, The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
LOCAL_PATH := $(call my-dir)
# Build the com.android.emailcommon static library. At the moment, this includes
# the emailcommon files themselves plus everything under src/org (apache code). All of our
# AIDL files are also compiled into the static library
include $(CLEAR_VARS)
LOCAL_MODULE := com.android.emailsync
LOCAL_SRC_FILES := $(call all-java-files-under, src/com/android/emailsync)
LOCAL_STATIC_JAVA_LIBRARIES := com.android.emailcommon2
LOCAL_SDK_VERSION := 14
include $(BUILD_STATIC_JAVA_LIBRARY)

View File

@ -0,0 +1,305 @@
/*
* Copyright (C) 2008-2009 Marc Blank
* Licensed to The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.emailsync;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.Mailbox;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.util.Log;
import java.util.concurrent.LinkedBlockingQueue;
/**
* Base class for all protocol services SyncManager (extends Service, implements
* Runnable) instantiates subclasses to run a sync (either timed, or push, or
* mail placed in outbox, etc.) EasSyncService is currently implemented; my goal
* would be to move IMAP to this structure when it comes time to introduce push
* functionality.
*/
public abstract class AbstractSyncService implements Runnable {
public String TAG = "AbstractSyncService";
public static final int SECONDS = 1000;
public static final int MINUTES = 60*SECONDS;
public static final int HOURS = 60*MINUTES;
public static final int DAYS = 24*HOURS;
public static final int CONNECT_TIMEOUT = 30*SECONDS;
public static final int NETWORK_WAIT = 15*SECONDS;
public static final String EAS_PROTOCOL = "eas";
public static final int EXIT_DONE = 0;
public static final int EXIT_IO_ERROR = 1;
public static final int EXIT_LOGIN_FAILURE = 2;
public static final int EXIT_EXCEPTION = 3;
public static final int EXIT_SECURITY_FAILURE = 4;
public static final int EXIT_ACCESS_DENIED = 5;
public Mailbox mMailbox;
protected long mMailboxId;
protected int mExitStatus = EXIT_EXCEPTION;
protected String mMailboxName;
public Account mAccount;
public Context mContext;
public int mChangeCount = 0;
public volatile int mSyncReason = 0;
protected volatile boolean mStop = false;
public volatile Thread mThread;
protected final Object mSynchronizer = new Object();
// Whether or not the sync service is valid (usable)
public boolean mIsValid = true;
public boolean mUserLog = true; // STOPSHIP
public boolean mFileLog = false;
protected volatile long mRequestTime = 0;
protected LinkedBlockingQueue<Request> mRequestQueue = new LinkedBlockingQueue<Request>();
/**
* Sent by SyncManager to request that the service stop itself cleanly
*/
public abstract void stop();
/**
* Sent by SyncManager to indicate that an alarm has fired for this service, and that its
* pending (network) operation has timed out. The service is NOT automatically stopped,
* although the behavior is service dependent.
*
* @return true if the operation was stopped normally; false if the thread needed to be
* interrupted.
*/
public abstract boolean alarm();
/**
* Sent by SyncManager to request that the service reset itself cleanly; the meaning of this
* operation is service dependent.
*/
public abstract void reset();
/**
* Called to validate an account; abstract to allow each protocol to do what
* is necessary. For consistency with the Email app's original
* functionality, success is indicated by a failure to throw an Exception
* (ugh). Parameters are self-explanatory
*
* @param hostAuth
* @return a Bundle containing a result code and, depending on the result, a PolicySet or an
* error message
*/
public abstract Bundle validateAccount(HostAuth hostAuth, Context context);
/**
* Called to clear the syncKey for the calendar associated with this service; this is necessary
* because changes to calendar sync state cause a reset of data.
*/
public abstract void resetCalendarSyncKey();
public AbstractSyncService(Context _context, Mailbox _mailbox) {
mContext = _context;
mMailbox = _mailbox;
mMailboxId = _mailbox.mId;
mMailboxName = _mailbox.mServerId;
mAccount = Account.restoreAccountWithId(_context, _mailbox.mAccountKey);
}
// Will be required when subclasses are instantiated by name
public AbstractSyncService(String prefix) {
}
/**
* The UI can call this static method to perform account validation. This method wraps each
* protocol's validateAccount method. Arguments are self-explanatory, except where noted.
*
* @param klass the protocol class (EasSyncService.class for example)
* @param hostAuth
* @param context
* @return a Bundle containing a result code and, depending on the result, a PolicySet or an
* error message
*/
public static Bundle validate(Class<? extends AbstractSyncService> klass,
HostAuth hostAuth, Context context) {
AbstractSyncService svc;
try {
svc = klass.newInstance();
return svc.validateAccount(hostAuth, context);
} catch (IllegalAccessException e) {
} catch (InstantiationException e) {
}
return null;
}
public static class ValidationResult {
static final int NO_FAILURE = 0;
static final int CONNECTION_FAILURE = 1;
static final int VALIDATION_FAILURE = 2;
static final int EXCEPTION = 3;
static final ValidationResult succeeded = new ValidationResult(true, NO_FAILURE, null);
boolean success;
int failure = NO_FAILURE;
String reason = null;
Exception exception = null;
ValidationResult(boolean _success, int _failure, String _reason) {
success = _success;
failure = _failure;
reason = _reason;
}
ValidationResult(boolean _success) {
success = _success;
}
ValidationResult(Exception e) {
success = false;
failure = EXCEPTION;
exception = e;
}
public boolean isSuccess() {
return success;
}
public String getReason() {
return reason;
}
}
public boolean isStopped() {
return mStop;
}
public Object getSynchronizer() {
return mSynchronizer;
}
/**
* Convenience methods to do user logging (i.e. connection activity). Saves a bunch of
* repetitive code.
*/
public void userLog(String string, int code, String string2) {
if (mUserLog) {
userLog(string + code + string2);
}
}
public void userLog(String string, int code) {
if (mUserLog) {
userLog(string + code);
}
}
public void userLog(String str, Exception e) {
if (mUserLog) {
Log.e(TAG, str, e);
} else {
Log.e(TAG, str + e);
}
if (mFileLog) {
FileLogger.log(e);
}
}
/**
* Standard logging for EAS.
* If user logging is active, we concatenate any arguments and log them using Log.d
* We also check for file logging, and log appropriately
* @param strings strings to concatenate and log
*/
public void userLog(String ...strings) {
if (mUserLog) {
String logText;
if (strings.length == 1) {
logText = strings[0];
} else {
StringBuilder sb = new StringBuilder(64);
for (String string: strings) {
sb.append(string);
}
logText = sb.toString();
}
Log.d(TAG, logText);
if (mFileLog) {
FileLogger.log(TAG, logText);
}
}
}
/**
* Error log is used for serious issues that should always be logged
* @param str the string to log
*/
public void errorLog(String str) {
Log.e(TAG, str);
if (mFileLog) {
FileLogger.log(TAG, str);
}
}
/**
* Waits for up to 10 seconds for network connectivity; returns whether or not there is
* network connectivity.
*
* @return whether there is network connectivity
*/
public boolean hasConnectivity() {
ConnectivityManager cm =
(ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
int tries = 0;
while (tries++ < 1) {
// Use the same test as in ExchangeService#waitForConnectivity
// TODO: Create common code for this test in emailcommon
NetworkInfo info = cm.getActiveNetworkInfo();
if (info != null) {
return true;
}
try {
Thread.sleep(10*SECONDS);
} catch (InterruptedException e) {
}
}
return false;
}
/**
* Request handling (common functionality)
* Can be overridden if desired
*/
public void addRequest(Request req) {
mRequestQueue.offer(req);
}
public void removeRequest(Request req) {
mRequestQueue.remove(req);
}
public boolean hasPendingRequests() {
return !mRequestQueue.isEmpty();
}
public void clearRequests() {
mRequestQueue.clear();
}
}

View File

@ -0,0 +1,111 @@
/*
* Copyright (C) 2008-2009 Marc Blank
* Licensed to The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.emailsync;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.util.Log;
import com.android.emailcommon.provider.EmailContent.Message;
import com.android.emailcommon.provider.EmailContent.MessageColumns;
import com.android.emailcommon.provider.ProviderUnavailableException;
import java.util.ArrayList;
/**
* EmailSyncAlarmReceiver (USAR) is used by the SyncManager to start up-syncs of user-modified data
* back to the Exchange server.
*
* Here's how this works for Email, for example:
*
* 1) User modifies or deletes an email from the UI.
* 2) SyncManager, which has a ContentObserver watching the Message class, is alerted to a change
* 3) SyncManager sets an alarm (to be received by USAR) for a few seconds in the
* future (currently 15), the delay preventing excess syncing (think of it as a debounce mechanism).
* 4) ESAR Receiver's onReceive method is called
* 5) ESAR goes through all change and deletion records and compiles a list of mailboxes which have
* changes to be uploaded.
* 6) ESAR calls SyncManager to start syncs of those mailboxes
*
* If EmailProvider isn't available, the upsyncs will happen the next time ExchangeService starts
*
*/
public class EmailSyncAlarmReceiver extends BroadcastReceiver {
final String[] MAILBOX_DATA_PROJECTION = {MessageColumns.MAILBOX_KEY};
@Override
public void onReceive(final Context context, Intent intent) {
new Thread(new Runnable() {
public void run() {
handleReceive(context);
}
}).start();
}
private void handleReceive(Context context) {
ArrayList<Long> mailboxesToNotify = new ArrayList<Long>();
ContentResolver cr = context.getContentResolver();
// Get a selector for EAS accounts (we don't want to sync on changes to POP/IMAP messages)
String selector = SyncServiceManager.getAccountSelector();
try {
// Find all of the deletions
Cursor c = cr.query(Message.DELETED_CONTENT_URI, MAILBOX_DATA_PROJECTION, selector,
null, null);
if (c == null) throw new ProviderUnavailableException();
try {
// Keep track of which mailboxes to notify; we'll only notify each one once
while (c.moveToNext()) {
long mailboxId = c.getLong(0);
if (!mailboxesToNotify.contains(mailboxId)) {
mailboxesToNotify.add(mailboxId);
}
}
} finally {
c.close();
}
// Now, find changed messages
c = cr.query(Message.UPDATED_CONTENT_URI, MAILBOX_DATA_PROJECTION, selector,
null, null);
if (c == null) throw new ProviderUnavailableException();
try {
// Keep track of which mailboxes to notify; we'll only notify each one once
while (c.moveToNext()) {
long mailboxId = c.getLong(0);
if (!mailboxesToNotify.contains(mailboxId)) {
mailboxesToNotify.add(mailboxId);
}
}
} finally {
c.close();
}
// Request service from the mailbox
for (Long mailboxId: mailboxesToNotify) {
SyncServiceManager.serviceRequest(mailboxId, SyncServiceManager.SYNC_UPSYNC);
}
} catch (ProviderUnavailableException e) {
Log.e("EmailSyncAlarmReceiver", "EmailProvider unavailable; aborting alarm receiver");
}
}
}

View File

@ -0,0 +1,120 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.emailsync;
import android.content.Context;
import android.os.Environment;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
public class FileLogger {
private static FileLogger LOGGER = null;
private static FileWriter sLogWriter = null;
public static String LOG_FILE_NAME =
Environment.getExternalStorageDirectory() + "/emaillog.txt";
public synchronized static FileLogger getLogger (Context c) {
LOGGER = new FileLogger();
return LOGGER;
}
private FileLogger() {
try {
sLogWriter = new FileWriter(LOG_FILE_NAME, true);
} catch (IOException e) {
// Doesn't matter
}
}
static public synchronized void close() {
if (sLogWriter != null) {
try {
sLogWriter.close();
} catch (IOException e) {
// Doesn't matter
}
sLogWriter = null;
}
}
static public synchronized void log(Exception e) {
if (sLogWriter != null) {
log("Exception", "Stack trace follows...");
PrintWriter pw = new PrintWriter(sLogWriter);
e.printStackTrace(pw);
pw.flush();
}
}
@SuppressWarnings("deprecation")
static public synchronized void log(String prefix, String str) {
if (LOGGER == null) {
LOGGER = new FileLogger();
log("Logger", "\r\n\r\n --- New Log ---");
}
Date d = new Date();
int hr = d.getHours();
int min = d.getMinutes();
int sec = d.getSeconds();
// I don't use DateFormat here because (in my experience), it's much slower
StringBuffer sb = new StringBuffer(256);
sb.append('[');
sb.append(hr);
sb.append(':');
if (min < 10)
sb.append('0');
sb.append(min);
sb.append(':');
if (sec < 10) {
sb.append('0');
}
sb.append(sec);
sb.append("] ");
if (prefix != null) {
sb.append(prefix);
sb.append("| ");
}
sb.append(str);
sb.append("\r\n");
String s = sb.toString();
if (sLogWriter != null) {
try {
sLogWriter.write(s);
sLogWriter.flush();
} catch (IOException e) {
// Something might have happened to the sdcard
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
// If the card is mounted and we can create the writer, retry
LOGGER = new FileLogger();
if (sLogWriter != null) {
try {
log("FileLogger", "Exception writing log; recreating...");
log(prefix, str);
} catch (Exception e1) {
// Nothing to do at this point
}
}
}
}
}
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (C) 2008-2009 Marc Blank
* Licensed to The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.emailsync;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
/**
* MailboxAlarmReceiver is used to "wake up" the ExchangeService at the appropriate time(s). It may
* also be used for individual sync adapters, but this isn't implemented at the present time.
*
*/
public class MailboxAlarmReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
long mailboxId = intent.getLongExtra("mailbox", SyncServiceManager.EXTRA_MAILBOX_ID);
// EXCHANGE_SERVICE_MAILBOX_ID tells us that the service is asking to be started
if (mailboxId == SyncServiceManager.SYNC_SERVICE_MAILBOX_ID) {
context.startService(new Intent(context, SyncServiceManager.class));
} else {
SyncServiceManager.alert(context, mailboxId);
}
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright (C) 2008-2009 Marc Blank
* Licensed to The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.emailsync;
import com.android.emailcommon.provider.EmailContent.Attachment;
/**
* PartRequest is the EAS wrapper for attachment loading requests. In addition to information about
* the attachment to be loaded, it also contains the callback to be used for status/progress
* updates to the UI.
*/
public class PartRequest extends Request {
public final Attachment mAttachment;
public final String mDestination;
public final String mContentUriString;
public final String mLocation;
public PartRequest(Attachment _att, String _destination, String _contentUriString) {
super(_att.mMessageKey);
mAttachment = _att;
mLocation = mAttachment.mLocation;
mDestination = _destination;
mContentUriString = _contentUriString;
}
// PartRequests are unique by their attachment id (i.e. multiple attachments might be queued
// for a particular message, but any individual attachment can only be loaded once)
public boolean equals(Object o) {
if (!(o instanceof PartRequest)) return false;
return ((PartRequest)o).mAttachment.mId == mAttachment.mId;
}
public int hashCode() {
return (int)mAttachment.mId;
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.emailsync;
/**
* Requests for mailbox actions are handled by subclasses of this abstract class.
* Three subclasses are now defined: PartRequest (attachment load), MeetingResponseRequest
* (respond to a meeting invitation), and MessageMoveRequest (move a message to another folder)
*/
public abstract class Request {
public final long mTimeStamp = System.currentTimeMillis();
public final long mMessageId;
public Request(long messageId) {
mMessageId = messageId;
}
// Subclasses of Request may have different semantics regarding equality; therefore,
// we force them to implement the equals method
public abstract boolean equals(Object o);
public abstract int hashCode();
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- small -->
<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_alignParentLeft="true"
android:layout_marginTop="@dimen/setup_buttons_vertical_spacing"
android:layout_marginLeft="48dip"
style="@style/accountSetupButton" />

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- small -->
<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/button"
android:text="@string/account_setup_options_mail_window_auto"
android:layout_height="wrap_content"
android:layout_width="150sp"
android:layout_marginTop="25dip"
android:minWidth="@dimen/button_minWidth"
android:layout_gravity="center_horizontal"
/>

View File

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2011 Google Inc.
Licensed to The Android Open Source Project.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- This layout is used as a template to create custom view CanvasConversationHeaderView
in normal mode. To be able to get the correct measurements, every source field should
be populated with data here. E.g:
- Text View should set text to a random long string (android:text="@string/long_string")
- Image View should set source to a specific asset -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="@dimen/conversation_item_height"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<ImageView
android:id="@+id/reply_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/replystate_margin_top"
android:layout_marginLeft="@dimen/replystate_margin_left"
android:layout_marginRight="@dimen/replystate_margin_right"
android:src="@drawable/ic_badge_reply_holo_light"
/>
<com.android.mail.browse.SendersView
android:id="@+id/senders"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/SendersStyle"
android:text="@string/long_string"
android:textSize="@dimen/senders_font_size"
android:lines="1"
android:layout_toRightOf="@id/reply_state" />
<RelativeLayout
android:layout_height="wrap_content"
android:layout_width="@dimen/total_conversationbar_width"
android:layout_alignParentRight="true">
<View android:id="@+id/color_bar"
android:layout_alignParentTop="true"
android:layout_height="@dimen/color_block_height"
android:layout_width="@dimen/total_conversationbar_width" />
<LinearLayout
android:layout_alignWithParentIfMissing="true"
android:layout_alignParentTop="true"
android:layout_height="wrap_content"
android:layout_width="@dimen/total_conversationbar_width"
android:layout_alignParentRight="true">
<ImageView
android:id="@+id/paperclip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_attachment_holo_light"
android:layout_marginTop="6sp" />
<TextView
android:id="@+id/date"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="wrap_content"
android:text="@string/long_string"
android:textSize="@dimen/date_font_size"
android:lines="1"
android:layout_marginRight="16dip"
android:layout_marginTop="10sp" />
</LinearLayout>
</RelativeLayout>
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
style="@style/CheckmarkStyle"
android:id="@+id/checkmark"
android:src="@drawable/btn_check_on_normal_holo_light"/>
<TextView
android:id="@+id/subject"
android:layout_width="@dimen/subject_width"
style="@style/SubjectStyle"
android:text="@string/long_string"
android:lines="2"/>
<ImageView
android:id="@+id/star"
style="@style/StarStyle"
android:src="@drawable/btn_star_off_normal_email_holo_light" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2011 Google Inc.
Licensed to The Android Open Source Project.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- This layout is used as a template to create custom view CanvasConversationHeaderView
in wide mode. To be able to get the correct measurements, every source field should
be populated with data here. E.g:
- Text View should set text to a random long string (android:text="@string/long_string")
- Image View should set source to a specific asset -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="64sp"
android:orientation="horizontal">
<ImageView
android:id="@+id/checkmark"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dip"
android:layout_marginRight="16dip"
android:layout_gravity="center_vertical"
android:src="@drawable/btn_check_on_normal_holo_light" />
<com.android.mail.browse.SendersView
android:id="@+id/senders"
android:layout_width="224dip"
android:layout_height="wrap_content"
android:text="@string/long_string"
android:textSize="@dimen/wide_senders_font_size"
android:layout_gravity="center_vertical"
android:maxLines="2"
android:layout_marginTop="@dimen/wide_senders_margin_top" />
<ImageView
android:id="@+id/reply_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dip"
android:layout_marginRight="16dip"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_badge_reply_holo_light" />
<TextView
android:id="@+id/subject"
android:layout_width="0dip"
android:layout_weight="0.7"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/long_string"
android:lines="2"
android:textColor="@color/subject_text_color_unread"
android:textSize="@dimen/wide_subject_font_size"
android:layout_marginTop="2sp"
android:layout_marginRight="@dimen/wide_subject_margin_right"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="center_vertical">
<ImageView
android:id="@+id/paperclip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_attachment_holo_light"
android:layout_gravity="center_vertical"
android:layout_marginTop="@dimen/wide_attachment_margin_top"/>
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/date"
android:layout_marginTop="@dimen/wide_date_margin_top" />
</LinearLayout>
</LinearLayout>
<ImageView
android:id="@+id/star"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dip"
android:layout_marginRight="16dip"
android:layout_gravity="center_vertical"
android:src="@drawable/btn_star_off_normal_email_holo_light" />
</LinearLayout>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2011 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:paddingLeft="4dip"
android:paddingTop="6dip"
android:paddingRight="4dip"
android:paddingBottom="6dip">
<EditText
android:id="@+id/quick_response_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</RelativeLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2010 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- tablet, landscape -->
<resources>
<!-- Account Setup Activities -->
<dimen name="setup_padding_top">16dip</dimen>
<dimen name="setup_padding_left">128dip</dimen>
<dimen name="setup_padding_right">128dip</dimen>
</resources>

25
res/xml/searchable.xml Normal file
View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2012 Google Inc.
Licensed to The Android Open Source Project.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:label="@string/search_title"
android:hint="@string/search_hint"
android:icon="@drawable/ic_menu_search_holo_light"
android:searchSuggestAuthority="com.android.email.suggestionsprovider"
android:searchSuggestSelection="query LIKE ?"
android:searchSuggestIntentAction="android.intent.action.SEARCH"
android:imeOptions="actionSearch" />

104
res/xml/services.xml Normal file
View File

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2012 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!--
Email services (protocols) are defined here. For the present, these are baked into the Email
apk; the goal is for remote services to register themselves into this file.
The required attributes are as follows (except that EITHER serviceClass or intent is required):
protocol: the unique name used to identify the protocol
name: the name of the account type option presented to users during account setup
accountType: the AccountManager type of accounts created using this service
serviceClass: a class implementing IEmailService (or null, if the service is remote)
intent: the intent used to connect to a remote IEmailService
port: the (default) port used when creating accounts using this service
portSsl: as above, when SSL is selected
syncIntervalStrings: a reference to an array of sync interval options
syncIntervals: a reference to an array of values corresponding to syncIntervalStrings
defaultSyncInterval: the default sync interval, selected from enums defined in attrs.xml
The following optional attributes default to "false":
offerTls: whether a TLS option (e.g. STARTTLS) is offered for this service
offerCerts: whether or not certificate authentication is an option for this service
usesSmtp: whether SMTP is used as the outgoing protocol for this service
offerPrefix: whether a "prefix" is offered to the user (for IMAP)
offerLocalDeletes: whether an option to delete locally is offered
syncChanges: whether non-deletion changes to messages sync back to the server
offerAttachmentPreload: whether to offer attachment preloading (pre-caching)
usesAutodiscover: whether to attempt using the "autodiscover" API when creating
an account
offerLookback: whether a sync "lookback" is offered (rather than the POP/IMAP
legacy "25 most recent messages synced")
defaultLookback: if "lookback" is offered, an enum of possible lookbacks
syncCalendar: whether this service is capable of syncing a calendar (offering a checkbox)
syncContacts: whether this service is capable of syncing contacts (offering a checkbox)
-->
<emailservices xmlns:email="http://schemas.android.com/apk/res/com.android.email">
<emailservice
email:protocol="pop3"
email:name="POP3"
email:accountType="com.android.email"
email:serviceClass="com.android.email.service.ImapService"
email:port="110"
email:portSsl="995"
email:syncIntervalStrings="@array/account_settings_check_frequency_entries"
email:syncIntervals="@array/account_settings_check_frequency_values"
email:defaultSyncInterval="mins15"
email:offerTls="true"
email:usesSmtp="true"
email:offerLocalDeletes="true"
/>
<emailservice
email:protocol="imap"
email:name="IMAP"
email:accountType="com.android.email"
email:serviceClass="com.android.email.service.Pop3Service"
email:port="143"
email:portSsl="993"
email:syncIntervalStrings="@array/account_settings_check_frequency_entries"
email:syncIntervals="@array/account_settings_check_frequency_values"
email:defaultSyncInterval="mins15"
email:offerTls="true"
email:usesSmtp="true"
email:offerAttachmentPreload="true"
email:offerPrefix="true"
email:syncChanges="true"
/>
<emailservice
email:protocol="eas"
email:name="Exchange"
email:accountType="com.android.exchange"
email:intent="com.android.email.EXCHANGE_INTENT"
email:port="80"
email:portSsl="443"
email:syncIntervalStrings="@array/account_settings_check_frequency_entries_push"
email:syncIntervals="@array/account_settings_check_frequency_values_push"
email:defaultSyncInterval="push"
email:defaultSsl="true"
email:offerCerts="true"
email:syncChanges="true"
email:usesAutodiscover="true"
email:offerAttachmentPreload="true"
email:offerLookback="true"
email:defaultLookback="auto"
email:syncContacts="true"
email:syncCalendar="true"
/>
</emailservices>

View File

@ -0,0 +1,76 @@
/**
* Copyright (c) 2012, Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email.activity;
import android.app.Activity;
import android.content.ContentUris;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.CalendarContract;
import com.android.emailcommon.mail.MeetingInfo;
import com.android.emailcommon.mail.PackedString;
import com.android.emailcommon.provider.EmailContent.Message;
import com.android.emailcommon.utility.Utility;
public class EventViewer extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Uri uri = getIntent().getData();
long messageId = Long.parseLong(uri.getLastPathSegment());
Message msg = Message.restoreMessageWithId(this, messageId);
if (msg == null) {
finish();
} else {
PackedString info = new PackedString(msg.mMeetingInfo);
String uid = info.get(MeetingInfo.MEETING_UID);
long eventId = -1;
if (uid != null) {
Cursor c = getContentResolver().query(CalendarContract.Events.CONTENT_URI,
new String[] {CalendarContract.Events._ID},
CalendarContract.Events.SYNC_DATA2 + "=?",
new String[] {uid}, null);
if (c != null) {
try {
if (c.getCount() == 1) {
c.moveToFirst();
eventId = c.getLong(0);
}
} finally {
c.close();
}
}
}
Intent intent = new Intent(Intent.ACTION_VIEW);
if (eventId != -1) {
uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId);
} else {
long time =
Utility.parseEmailDateTimeToMillis(info.get(MeetingInfo.MEETING_DTSTART));
uri = Uri.parse("content://com.android.calendar/time/" + time);
intent.putExtra("VIEW", "DAY");
}
intent.setData(uri);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
startActivity(intent);
finish();
}
}
}

View File

@ -0,0 +1,138 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email.activity.setup;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.RelativeLayout;
import android.widget.RelativeLayout.LayoutParams;
import com.android.email.R;
import com.android.email.activity.ActivityHelper;
import com.android.email.activity.UiUtilities;
import com.android.email.service.EmailServiceUtils;
import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.HostAuth;
/**
* Prompts the user to select an account type. The account type, along with the
* passed in email address, password and makeDefault are then passed on to the
* AccountSetupIncoming activity.
*/
public class AccountSetupType extends AccountSetupActivity implements OnClickListener {
public static void actionSelectAccountType(Activity fromActivity) {
Intent i = new ForwardingIntent(fromActivity, AccountSetupType.class);
fromActivity.startActivity(i);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityHelper.debugSetWindowFlags(this);
int flowMode = SetupData.getFlowMode();
String accountType = SetupData.getFlowAccountType();
// If we're in account setup flow mode, see if there's just one protocol that matches
if (flowMode == SetupData.FLOW_MODE_ACCOUNT_MANAGER) {
int matches = 0;
String protocol = null;
for (EmailServiceInfo info: EmailServiceUtils.getServiceInfoList(this)) {
if (info.accountType.equals(accountType)) {
protocol = info.protocol;
matches++;
}
}
// If so, select it...
if (matches == 1) {
onSelect(protocol);
return;
}
}
// Otherwise proceed into this screen
setContentView(R.layout.account_setup_account_type);
ViewGroup parent = UiUtilities.getView(this, R.id.accountTypes);
boolean parentRelative = parent instanceof RelativeLayout;
View lastView = parent.getChildAt(0);
int i = 1;
for (EmailServiceInfo info: EmailServiceUtils.getServiceInfoList(this)) {
if (EmailServiceUtils.isServiceAvailable(this, info.protocol)) {
// If we're looking for a specific account type, reject others
if (accountType != null && !accountType.equals(info.accountType)) {
continue;
}
LayoutInflater.from(this).inflate(R.layout.account_type, parent);
Button button = (Button)parent.getChildAt(i);
if (parentRelative) {
LayoutParams params = (LayoutParams)button.getLayoutParams();
params.addRule(RelativeLayout.BELOW, lastView.getId());
}
button.setId(i);
button.setTag(info.protocol);
button.setText(info.name);
button.setOnClickListener(this);
lastView = button;
i++;
// TODO: Remember vendor overlay for exchange name
}
}
final Button previousButton = (Button) findViewById(R.id.previous); // xlarge only
if (previousButton != null) previousButton.setOnClickListener(this);
}
/**
* The user has selected an exchange account type. Set the mail delete policy here, because
* there is no UI (for exchange), and switch the default sync interval to "push".
*/
private void onSelect(String protocol) {
Account account = SetupData.getAccount();
HostAuth recvAuth = account.getOrCreateHostAuthRecv(this);
recvAuth.setConnection(protocol, recvAuth.mAddress, recvAuth.mPort, recvAuth.mFlags);
EmailServiceInfo info = EmailServiceUtils.getServiceInfo(this, protocol);
if (info.usesAutodiscover) {
SetupData.setCheckSettingsMode(SetupData.CHECK_AUTODISCOVER);
} else {
SetupData.setCheckSettingsMode(
SetupData.CHECK_INCOMING | (info.usesSmtp ? SetupData.CHECK_OUTGOING : 0));
}
recvAuth.mLogin = recvAuth.mLogin + "@" + recvAuth.mAddress;
AccountSetupBasics.setDefaultsForProtocol(this, account);
AccountSetupIncoming.actionIncomingSettings(this, SetupData.getFlowMode(), account);
// Back from the incoming screen returns to AccountSetupBasics
finish();
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.previous:
finish();
break;
default:
onSelect((String)v.getTag());
break;
}
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email.activity.setup;
import android.os.Bundle;
import android.preference.PreferenceFragment;
import android.view.Menu;
import android.view.MenuInflater;
public class EmailPreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
//***
//if (!UiUtilities.useTwoPane(getActivity())) {
// menu.clear();
//}
}
}

View File

@ -0,0 +1,30 @@
/* Copyright (C) 2012 Google Inc.
* Licensed to The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/
package com.android.email.activity.setup;
import android.content.Context;
import android.content.Intent;
/**
* An intent that forwards results
*/
public class ForwardingIntent extends Intent {
public ForwardingIntent(Context activity, Class klass) {
super(activity, klass);
setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email.activity.setup;
import android.content.Context;
import android.preference.Preference;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
/**
* Simple text preference allowing a large number of lines
*/
public class PolicyListPreference extends Preference {
// Arbitrary, but large number (we don't, and won't, have nearly this many)
public static final int MAX_POLICIES = 24;
public PolicyListPreference(Context ctx, AttributeSet attrs, int defStyle) {
super(ctx, attrs, defStyle);
}
public PolicyListPreference(Context ctx, AttributeSet attrs) {
super(ctx, attrs);
}
@Override
protected void onBindView(View view) {
super.onBindView(view);
((TextView)view.findViewById(android.R.id.summary)).setMaxLines(MAX_POLICIES);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,150 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email.provider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
import com.android.email.LegacyConversions;
import com.android.emailcommon.Logging;
import com.android.emailcommon.internet.MimeUtility;
import com.android.emailcommon.mail.Message;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.mail.Part;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.MessageColumns;
import com.android.emailcommon.provider.EmailContent.SyncColumns;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.utility.ConversionUtilities;
import java.io.IOException;
import java.util.ArrayList;
public class Utilities {
/**
* Copy one downloaded message (which may have partially-loaded sections)
* into a newly created EmailProvider Message, given the account and mailbox
*
* @param message the remote message we've just downloaded
* @param account the account it will be stored into
* @param folder the mailbox it will be stored into
* @param loadStatus when complete, the message will be marked with this status (e.g.
* EmailContent.Message.LOADED)
*/
public static void copyOneMessageToProvider(Context context, Message message, Account account,
Mailbox folder, int loadStatus) {
EmailContent.Message localMessage = null;
Cursor c = null;
try {
c = context.getContentResolver().query(
EmailContent.Message.CONTENT_URI,
EmailContent.Message.CONTENT_PROJECTION,
EmailContent.MessageColumns.ACCOUNT_KEY + "=?" +
" AND " + MessageColumns.MAILBOX_KEY + "=?" +
" AND " + SyncColumns.SERVER_ID + "=?",
new String[] {
String.valueOf(account.mId),
String.valueOf(folder.mId),
String.valueOf(message.getUid())
},
null);
if (c.moveToNext()) {
localMessage = EmailContent.getContent(c, EmailContent.Message.class);
localMessage.mMailboxKey = folder.mId;
localMessage.mAccountKey = account.mId;
copyOneMessageToProvider(context, message, localMessage, loadStatus);
}
} finally {
if (c != null) {
c.close();
}
}
}
/**
* Copy one downloaded message (which may have partially-loaded sections)
* into an already-created EmailProvider Message
*
* @param message the remote message we've just downloaded
* @param localMessage the EmailProvider Message, already created
* @param loadStatus when complete, the message will be marked with this status (e.g.
* EmailContent.Message.LOADED)
* @param context the context to be used for EmailProvider
*/
public static void copyOneMessageToProvider(Context context, Message message,
EmailContent.Message localMessage, int loadStatus) {
try {
EmailContent.Body body = EmailContent.Body.restoreBodyWithMessageId(context,
localMessage.mId);
if (body == null) {
body = new EmailContent.Body();
}
try {
// Copy the fields that are available into the message object
LegacyConversions.updateMessageFields(localMessage, message,
localMessage.mAccountKey, localMessage.mMailboxKey);
// Now process body parts & attachments
ArrayList<Part> viewables = new ArrayList<Part>();
ArrayList<Part> attachments = new ArrayList<Part>();
MimeUtility.collectParts(message, viewables, attachments);
ConversionUtilities.updateBodyFields(body, localMessage, viewables);
// Commit the message & body to the local store immediately
saveOrUpdate(localMessage, context);
saveOrUpdate(body, context);
// process (and save) attachments
LegacyConversions.updateAttachments(context, localMessage, attachments);
// One last update of message with two updated flags
localMessage.mFlagLoaded = loadStatus;
ContentValues cv = new ContentValues();
cv.put(EmailContent.MessageColumns.FLAG_ATTACHMENT, localMessage.mFlagAttachment);
cv.put(EmailContent.MessageColumns.FLAG_LOADED, localMessage.mFlagLoaded);
Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI,
localMessage.mId);
context.getContentResolver().update(uri, cv, null, null);
} catch (MessagingException me) {
Log.e(Logging.LOG_TAG, "Error while copying downloaded message." + me);
}
} catch (RuntimeException rte) {
Log.e(Logging.LOG_TAG, "Error while storing downloaded message." + rte.toString());
} catch (IOException ioe) {
Log.e(Logging.LOG_TAG, "Error while storing attachment." + ioe.toString());
}
}
public static void saveOrUpdate(EmailContent content, Context context) {
if (content.isSaved()) {
content.update(context, content.toContentValues());
} else {
content.save(context);
}
}
}

View File

@ -0,0 +1,159 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email.service;
import com.android.email.activity.setup.AccountSetupBasics;
import com.android.emailcommon.provider.EmailContent;
import android.accounts.AbstractAccountAuthenticator;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.accounts.NetworkErrorException;
import android.app.Service;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import android.provider.CalendarContract;
import android.provider.ContactsContract;
/**
* A very basic authenticator service for EAS. At the moment, it has no UI hooks. When called
* with addAccount, it simply adds the account to AccountManager directly with a username and
* password.
*/
public class AuthenticatorService extends Service {
public static final String OPTIONS_USERNAME = "username";
public static final String OPTIONS_PASSWORD = "password";
public static final String OPTIONS_CONTACTS_SYNC_ENABLED = "contacts";
public static final String OPTIONS_CALENDAR_SYNC_ENABLED = "calendar";
public static final String OPTIONS_EMAIL_SYNC_ENABLED = "email";
class Authenticator extends AbstractAccountAuthenticator {
public Authenticator(Context context) {
super(context);
}
@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType,
String authTokenType, String[] requiredFeatures, Bundle options)
throws NetworkErrorException {
// There are two cases here:
// 1) We are called with a username/password; this comes from the traditional email
// app UI; we simply create the account and return the proper bundle
if (options != null && options.containsKey(OPTIONS_PASSWORD)
&& options.containsKey(OPTIONS_USERNAME)) {
final Account account = new Account(options.getString(OPTIONS_USERNAME),
accountType);
AccountManager.get(AuthenticatorService.this).addAccountExplicitly(
account, options.getString(OPTIONS_PASSWORD), null);
// Set up contacts syncing, if appropriate
if (options.containsKey(OPTIONS_CONTACTS_SYNC_ENABLED)) {
boolean syncContacts = options.getBoolean(OPTIONS_CONTACTS_SYNC_ENABLED);
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY,
syncContacts);
}
// Set up calendar syncing, if appropriate
if (options.containsKey(OPTIONS_CALENDAR_SYNC_ENABLED)) {
boolean syncCalendar = options.getBoolean(OPTIONS_CALENDAR_SYNC_ENABLED);
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1);
ContentResolver.setSyncAutomatically(account, CalendarContract.AUTHORITY,
syncCalendar);
}
// Set up email syncing (it's always syncable, but we respect the user's choice
// for whether to enable it now)
boolean syncEmail = false;
if (options.containsKey(OPTIONS_EMAIL_SYNC_ENABLED) &&
options.getBoolean(OPTIONS_EMAIL_SYNC_ENABLED)) {
syncEmail = true;
}
ContentResolver.setIsSyncable(account, EmailContent.AUTHORITY, 1);
ContentResolver.setSyncAutomatically(account, EmailContent.AUTHORITY,
syncEmail);
Bundle b = new Bundle();
b.putString(AccountManager.KEY_ACCOUNT_NAME, options.getString(OPTIONS_USERNAME));
b.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType);
return b;
// 2) The other case is that we're creating a new account from an Account manager
// activity. In this case, we add an intent that will be used to gather the
// account information...
} else {
Bundle b = new Bundle();
Intent intent =
AccountSetupBasics.actionGetCreateAccountIntent(AuthenticatorService.this,
accountType);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
b.putParcelable(AccountManager.KEY_INTENT, intent);
return b;
}
}
@Override
public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account,
Bundle options) {
return null;
}
@Override
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
return null;
}
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
String authTokenType, Bundle loginOptions) throws NetworkErrorException {
return null;
}
@Override
public String getAuthTokenLabel(String authTokenType) {
// null means we don't have compartmentalized authtoken types
return null;
}
@Override
public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account,
String[] features) throws NetworkErrorException {
return null;
}
@Override
public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account,
String authTokenType, Bundle loginOptions) {
return null;
}
}
@Override
public IBinder onBind(Intent intent) {
if (AccountManager.ACTION_AUTHENTICATOR_INTENT.equals(intent.getAction())) {
return new Authenticator(this).getIBinder();
} else {
return null;
}
}
}

View File

@ -0,0 +1,561 @@
/* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email.service;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.TrafficStats;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import com.android.email.NotificationController;
import com.android.email.mail.Sender;
import com.android.email.mail.Store;
import com.android.email.provider.Utilities;
import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.AccountManagerTypes;
import com.android.emailcommon.Api;
import com.android.emailcommon.Logging;
import com.android.emailcommon.TrafficFlags;
import com.android.emailcommon.internet.MimeBodyPart;
import com.android.emailcommon.internet.MimeHeader;
import com.android.emailcommon.internet.MimeMultipart;
import com.android.emailcommon.mail.AuthenticationFailedException;
import com.android.emailcommon.mail.FetchProfile;
import com.android.emailcommon.mail.Folder;
import com.android.emailcommon.mail.Folder.MessageRetrievalListener;
import com.android.emailcommon.mail.Folder.OpenMode;
import com.android.emailcommon.mail.Message;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.Attachment;
import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
import com.android.emailcommon.provider.EmailContent.Body;
import com.android.emailcommon.provider.EmailContent.BodyColumns;
import com.android.emailcommon.provider.EmailContent.MailboxColumns;
import com.android.emailcommon.provider.EmailContent.MessageColumns;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.service.EmailServiceStatus;
import com.android.emailcommon.service.IEmailService;
import com.android.emailcommon.service.IEmailServiceCallback;
import com.android.emailcommon.service.SearchParams;
import com.android.emailcommon.utility.AttachmentUtilities;
import com.android.emailcommon.utility.Utility;
import com.android.mail.providers.UIProvider;
import java.util.HashSet;
/**
* EmailServiceStub is an abstract class representing an EmailService
*
* This class provides legacy support for a few methods that are common to both
* IMAP and POP3, including startSync, loadMore, loadAttachment, and sendMail
*/
public abstract class EmailServiceStub extends IEmailService.Stub implements IEmailService {
private static final int MAILBOX_COLUMN_ID = 0;
private static final int MAILBOX_COLUMN_SERVER_ID = 1;
private static final int MAILBOX_COLUMN_TYPE = 2;
public static final String SYNC_EXTRA_MAILBOX_ID = "__mailboxId__";
/** Small projection for just the columns required for a sync. */
private static final String[] MAILBOX_PROJECTION = new String[] {
MailboxColumns.ID,
MailboxColumns.SERVER_ID,
MailboxColumns.TYPE,
};
private Context mContext;
private IEmailServiceCallback.Stub mCallback;
protected void init(Context context, IEmailServiceCallback.Stub callbackProxy) {
mContext = context;
mCallback = callbackProxy;
}
@Override
public Bundle validate(HostAuth hostauth) throws RemoteException {
// TODO Auto-generated method stub
return null;
}
@Override
public void startSync(long mailboxId, boolean userRequest) throws RemoteException {
Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId);
if (mailbox == null) return;
Account account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey);
if (account == null) return;
android.accounts.Account acct = new android.accounts.Account(account.mEmailAddress,
AccountManagerTypes.TYPE_POP_IMAP);
Bundle extras = new Bundle();
extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
extras.putLong(SYNC_EXTRA_MAILBOX_ID, mailboxId);
ContentResolver.requestSync(acct, EmailContent.AUTHORITY, extras);
}
@Override
public void stopSync(long mailboxId) throws RemoteException {
// Not required
}
@Override
public void loadMore(long messageId) throws RemoteException {
// Load a message for view...
try {
// 1. Resample the message, in case it disappeared or synced while
// this command was in queue
EmailContent.Message message =
EmailContent.Message.restoreMessageWithId(mContext, messageId);
if (message == null) {
mCallback.loadMessageStatus(messageId,
EmailServiceStatus.MESSAGE_NOT_FOUND, 0);
return;
}
if (message.mFlagLoaded == EmailContent.Message.FLAG_LOADED_COMPLETE) {
// We should NEVER get here
mCallback.loadMessageStatus(messageId, 0, 100);
return;
}
// 2. Open the remote folder.
// TODO combine with common code in loadAttachment
Account account = Account.restoreAccountWithId(mContext, message.mAccountKey);
Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey);
if (account == null || mailbox == null) {
//mListeners.loadMessageForViewFailed(messageId, "null account or mailbox");
return;
}
TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account));
Store remoteStore = Store.getInstance(account, mContext);
String remoteServerId = mailbox.mServerId;
// If this is a search result, use the protocolSearchInfo field to get the
// correct remote location
if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) {
remoteServerId = message.mProtocolSearchInfo;
}
Folder remoteFolder = remoteStore.getFolder(remoteServerId);
remoteFolder.open(OpenMode.READ_WRITE);
// 3. Set up to download the entire message
Message remoteMessage = remoteFolder.getMessage(message.mServerId);
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.BODY);
remoteFolder.fetch(new Message[] { remoteMessage }, fp, null);
// 4. Write to provider
Utilities.copyOneMessageToProvider(mContext, remoteMessage, account, mailbox,
EmailContent.Message.FLAG_LOADED_COMPLETE);
// 5. Notify UI
mCallback.loadMessageStatus(messageId, 0, 100);
} catch (MessagingException me) {
if (Logging.LOGD) Log.v(Logging.LOG_TAG, "", me);
mCallback.loadMessageStatus(messageId, EmailServiceStatus.REMOTE_EXCEPTION, 0);
} catch (RuntimeException rte) {
mCallback.loadMessageStatus(messageId, EmailServiceStatus.REMOTE_EXCEPTION, 0);
}
}
private void doProgressCallback(long messageId, long attachmentId, int progress) {
try {
mCallback.loadAttachmentStatus(messageId, attachmentId,
EmailServiceStatus.IN_PROGRESS, progress);
} catch (RemoteException e) {
// No danger if the client is no longer around
}
}
@Override
public void loadAttachment(long attachmentId, boolean background) throws RemoteException {
try {
//1. Check if the attachment is already here and return early in that case
Attachment attachment =
Attachment.restoreAttachmentWithId(mContext, attachmentId);
if (attachment == null) {
mCallback.loadAttachmentStatus(0, attachmentId,
EmailServiceStatus.ATTACHMENT_NOT_FOUND, 0);
return;
}
long messageId = attachment.mMessageKey;
EmailContent.Message message =
EmailContent.Message.restoreMessageWithId(mContext, attachment.mMessageKey);
if (message == null) {
mCallback.loadAttachmentStatus(messageId, attachmentId,
EmailServiceStatus.MESSAGE_NOT_FOUND, 0);
}
// If the message is loaded, just report that we're finished
if (Utility.attachmentExists(mContext, attachment)) {
mCallback.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.SUCCESS,
0);
return;
}
// Say we're starting...
doProgressCallback(messageId, attachmentId, 0);
// 2. Open the remote folder.
Account account = Account.restoreAccountWithId(mContext, message.mAccountKey);
Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey);
if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
long sourceId = Utility.getFirstRowLong(mContext, Body.CONTENT_URI,
new String[] {BodyColumns.SOURCE_MESSAGE_KEY},
BodyColumns.MESSAGE_KEY + "=?",
new String[] {Long.toString(messageId)}, null, 0, -1L);
if (sourceId != -1 ) {
EmailContent.Message sourceMsg =
EmailContent.Message.restoreMessageWithId(mContext, sourceId);
if (sourceMsg != null) {
mailbox = Mailbox.restoreMailboxWithId(mContext, sourceMsg.mMailboxKey);
message.mServerId = sourceMsg.mServerId;
}
}
}
if (account == null || mailbox == null) {
// If the account/mailbox are gone, just report success; the UI handles this
mCallback.loadAttachmentStatus(messageId, attachmentId,
EmailServiceStatus.SUCCESS, 0);
return;
}
TrafficStats.setThreadStatsTag(
TrafficFlags.getAttachmentFlags(mContext, account));
Store remoteStore = Store.getInstance(account, mContext);
Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
remoteFolder.open(OpenMode.READ_WRITE);
// 3. Generate a shell message in which to retrieve the attachment,
// and a shell BodyPart for the attachment. Then glue them together.
Message storeMessage = remoteFolder.createMessage(message.mServerId);
MimeBodyPart storePart = new MimeBodyPart();
storePart.setSize((int)attachment.mSize);
storePart.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA,
attachment.mLocation);
storePart.setHeader(MimeHeader.HEADER_CONTENT_TYPE,
String.format("%s;\n name=\"%s\"",
attachment.mMimeType,
attachment.mFileName));
// TODO is this always true for attachments? I think we dropped the
// true encoding along the way
storePart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
MimeMultipart multipart = new MimeMultipart();
multipart.setSubType("mixed");
multipart.addBodyPart(storePart);
storeMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed");
storeMessage.setBody(multipart);
// 4. Now ask for the attachment to be fetched
FetchProfile fp = new FetchProfile();
fp.add(storePart);
remoteFolder.fetch(new Message[] { storeMessage }, fp,
new MessageRetrievalListenerBridge(messageId, attachmentId));
// If we failed to load the attachment, throw an Exception here, so that
// AttachmentDownloadService knows that we failed
if (storePart.getBody() == null) {
throw new MessagingException("Attachment not loaded.");
}
// Save the attachment to wherever it's going
AttachmentUtilities.saveAttachment(mContext, storePart.getBody().getInputStream(),
attachment);
// 6. Report success
mCallback.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.SUCCESS, 0);
// Close the connection
remoteFolder.close(false);
}
catch (MessagingException me) {
if (Logging.LOGD) Log.v(Logging.LOG_TAG, "", me);
// TODO: Fix this up; consider the best approach
ContentValues cv = new ContentValues();
cv.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.FAILED);
Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId);
mContext.getContentResolver().update(uri, cv, null, null);
mCallback.loadAttachmentStatus(0, attachmentId, EmailServiceStatus.CONNECTION_ERROR, 0);
}
}
/**
* Bridge to intercept {@link MessageRetrievalListener#loadAttachmentProgress} and
* pass down to {@link Result}.
*/
public class MessageRetrievalListenerBridge implements MessageRetrievalListener {
private final long mMessageId;
private final long mAttachmentId;
public MessageRetrievalListenerBridge(long messageId, long attachmentId) {
mMessageId = messageId;
mAttachmentId = attachmentId;
}
@Override
public void loadAttachmentProgress(int progress) {
doProgressCallback(mMessageId, mAttachmentId, progress);
}
@Override
public void messageRetrieved(com.android.emailcommon.mail.Message message) {
}
}
@Override
public void updateFolderList(long accountId) throws RemoteException {
Account account = Account.restoreAccountWithId(mContext, accountId);
if (account == null) return;
Mailbox inbox = Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_INBOX);
TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account));
Cursor localFolderCursor = null;
try {
// Step 1: Get remote mailboxes
Store store = Store.getInstance(account, mContext);
Folder[] remoteFolders = store.updateFolders();
HashSet<String> remoteFolderNames = new HashSet<String>();
for (int i = 0, count = remoteFolders.length; i < count; i++) {
remoteFolderNames.add(remoteFolders[i].getName());
}
// Step 2: Get local mailboxes
localFolderCursor = mContext.getContentResolver().query(
Mailbox.CONTENT_URI,
MAILBOX_PROJECTION,
EmailContent.MailboxColumns.ACCOUNT_KEY + "=?",
new String[] { String.valueOf(account.mId) },
null);
// Step 3: Remove any local mailbox not on the remote list
while (localFolderCursor.moveToNext()) {
String mailboxPath = localFolderCursor.getString(MAILBOX_COLUMN_SERVER_ID);
// Short circuit if we have a remote mailbox with the same name
if (remoteFolderNames.contains(mailboxPath)) {
continue;
}
int mailboxType = localFolderCursor.getInt(MAILBOX_COLUMN_TYPE);
long mailboxId = localFolderCursor.getLong(MAILBOX_COLUMN_ID);
switch (mailboxType) {
case Mailbox.TYPE_INBOX:
case Mailbox.TYPE_DRAFTS:
case Mailbox.TYPE_OUTBOX:
case Mailbox.TYPE_SENT:
case Mailbox.TYPE_TRASH:
case Mailbox.TYPE_SEARCH:
// Never, ever delete special mailboxes
break;
default:
// Drop all attachment files related to this mailbox
AttachmentUtilities.deleteAllMailboxAttachmentFiles(
mContext, accountId, mailboxId);
// Delete the mailbox; database triggers take care of related
// Message, Body and Attachment records
Uri uri = ContentUris.withAppendedId(
Mailbox.CONTENT_URI, mailboxId);
mContext.getContentResolver().delete(uri, null, null);
break;
}
}
} catch (MessagingException e) {
// We'll hope this is temporary
} finally {
if (localFolderCursor != null) {
localFolderCursor.close();
}
// If this is a first sync, find the inbox and sync it
if (inbox == null) {
inbox = Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_INBOX);
if (inbox != null) {
startSync(inbox.mId, true);
}
}
}
}
@Override
public boolean createFolder(long accountId, String name) throws RemoteException {
// Not required
return false;
}
@Override
public boolean deleteFolder(long accountId, String name) throws RemoteException {
// Not required
return false;
}
@Override
public boolean renameFolder(long accountId, String oldName, String newName)
throws RemoteException {
// Not required
return false;
}
@Override
public void setCallback(IEmailServiceCallback cb) throws RemoteException {
// Not required
}
@Override
public void setLogging(int on) throws RemoteException {
// Not required
}
@Override
public void hostChanged(long accountId) throws RemoteException {
// Not required
}
@Override
public Bundle autoDiscover(String userName, String password) throws RemoteException {
// Not required
return null;
}
@Override
public void sendMeetingResponse(long messageId, int response) throws RemoteException {
// Not required
}
@Override
public void deleteAccountPIMData(long accountId) throws RemoteException {
MailService.reconcilePopImapAccountsSync(mContext);
}
@Override
public int getApiLevel() throws RemoteException {
return Api.LEVEL;
}
@Override
public int searchMessages(long accountId, SearchParams params, long destMailboxId)
throws RemoteException {
// Not required
return 0;
}
@Override
public void sendMail(long accountId) throws RemoteException {
sendMailImpl(mContext, accountId);
}
public static void sendMailImpl(Context context, long accountId) {
Account account = Account.restoreAccountWithId(context, accountId);
TrafficStats.setThreadStatsTag(TrafficFlags.getSmtpFlags(context, account));
NotificationController nc = NotificationController.getInstance(context);
// 1. Loop through all messages in the account's outbox
long outboxId = Mailbox.findMailboxOfType(context, account.mId, Mailbox.TYPE_OUTBOX);
if (outboxId == Mailbox.NO_MAILBOX) {
return;
}
ContentResolver resolver = context.getContentResolver();
Cursor c = resolver.query(EmailContent.Message.CONTENT_URI,
EmailContent.Message.ID_COLUMN_PROJECTION,
EmailContent.Message.MAILBOX_KEY + "=?", new String[] { Long.toString(outboxId) },
null);
try {
// 2. exit early
if (c.getCount() <= 0) {
return;
}
Sender sender = Sender.getInstance(context, account);
Store remoteStore = Store.getInstance(account, context);
boolean requireMoveMessageToSentFolder = remoteStore.requireCopyMessageToSentFolder();
ContentValues moveToSentValues = null;
if (requireMoveMessageToSentFolder) {
Mailbox sentFolder =
Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_SENT);
moveToSentValues = new ContentValues();
moveToSentValues.put(MessageColumns.MAILBOX_KEY, sentFolder.mId);
}
// 3. loop through the available messages and send them
while (c.moveToNext()) {
long messageId = -1;
moveToSentValues.remove(EmailContent.MessageColumns.FLAGS);
try {
messageId = c.getLong(0);
// Don't send messages with unloaded attachments
if (Utility.hasUnloadedAttachments(context, messageId)) {
if (MailActivityEmail.DEBUG) {
Log.d(Logging.LOG_TAG, "Can't send #" + messageId +
"; unloaded attachments");
}
continue;
}
sender.sendMessage(messageId);
} catch (MessagingException me) {
// report error for this message, but keep trying others
if (me instanceof AuthenticationFailedException) {
nc.showLoginFailedNotification(account.mId);
}
continue;
}
// 4. move to sent, or delete
Uri syncedUri =
ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId);
if (requireMoveMessageToSentFolder) {
// If this is a forwarded message and it has attachments, delete them, as they
// duplicate information found elsewhere (on the server). This saves storage.
EmailContent.Message msg =
EmailContent.Message.restoreMessageWithId(context, messageId);
if (msg != null &&
((msg.mFlags & EmailContent.Message.FLAG_TYPE_FORWARD) != 0)) {
AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId,
messageId);
}
int flags = msg.mFlags & ~(EmailContent.Message.FLAG_TYPE_REPLY |
EmailContent.Message.FLAG_TYPE_FORWARD);
moveToSentValues.put(EmailContent.MessageColumns.FLAGS, flags);
resolver.update(syncedUri, moveToSentValues, null, null);
} else {
AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId,
messageId);
Uri uri =
ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId);
resolver.delete(uri, null, null);
resolver.delete(syncedUri, null, null);
}
}
nc.cancelLoginFailedNotification(account.mId);
} catch (MessagingException me) {
if (me instanceof AuthenticationFailedException) {
nc.showLoginFailedNotification(account.mId);
}
} finally {
c.close();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,127 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email.service;
import android.util.Log;
import com.android.email.FixedLengthInputStream;
import com.android.email.mail.store.imap.ImapResponse;
import com.android.email.mail.store.imap.ImapResponseParser;
import com.android.email.mail.store.imap.ImapString;
import com.android.emailcommon.Logging;
import com.android.emailcommon.TempDirectory;
import com.android.emailcommon.utility.Utility;
import org.apache.commons.io.IOUtils;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Subclass of {@link ImapString} used for literals backed by a temp file.
*/
public class ImapTempFileLiteral extends ImapString {
/* package for test */ final File mFile;
/** Size is purely for toString() */
private final int mSize;
/* package */ ImapTempFileLiteral(FixedLengthInputStream stream) throws IOException {
mSize = stream.getLength();
mFile = File.createTempFile("imap", ".tmp", TempDirectory.getTempDirectory());
// Unfortunately, we can't really use deleteOnExit(), because temp filenames are random
// so it'd simply cause a memory leak.
// deleteOnExit() simply adds filenames to a static list and the list will never shrink.
// mFile.deleteOnExit();
OutputStream out = new FileOutputStream(mFile);
IOUtils.copy(stream, out);
out.close();
}
/**
* Make sure we delete the temp file.
*
* We should always be calling {@link ImapResponse#destroy()}, but it's here as a last resort.
*/
@Override
protected void finalize() throws Throwable {
try {
destroy();
} finally {
super.finalize();
}
}
@Override
public InputStream getAsStream() {
checkNotDestroyed();
try {
return new FileInputStream(mFile);
} catch (FileNotFoundException e) {
// It's probably possible if we're low on storage and the system clears the cache dir.
Log.w(Logging.LOG_TAG, "ImapTempFileLiteral: Temp file not found");
// Return 0 byte stream as a dummy...
return new ByteArrayInputStream(new byte[0]);
}
}
@Override
public String getString() {
checkNotDestroyed();
try {
byte[] bytes = IOUtils.toByteArray(getAsStream());
// Prevent crash from OOM; we've seen this, but only rarely and not reproducibly
if (bytes.length > ImapResponseParser.LITERAL_KEEP_IN_MEMORY_THRESHOLD) {
throw new IOException();
}
return Utility.fromAscii(bytes);
} catch (IOException e) {
Log.w(Logging.LOG_TAG, "ImapTempFileLiteral: Error while reading temp file", e);
return "";
}
}
@Override
public void destroy() {
try {
if (!isDestroyed() && mFile.exists()) {
mFile.delete();
}
} catch (RuntimeException re) {
// Just log and ignore.
Log.w(Logging.LOG_TAG, "Failed to remove temp file: " + re.getMessage());
}
super.destroy();
}
@Override
public String toString() {
return String.format("{%d byte literal(file)}", mSize);
}
public boolean tempFileExistsForTest() {
return mFile.exists();
}
}

View File

@ -0,0 +1,726 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email.service;
import android.app.Service;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.TrafficStats;
import android.net.Uri;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;
import com.android.email.LegacyConversions;
import com.android.email.NotificationController;
import com.android.email.mail.Store;
import com.android.email.provider.Utilities;
import com.android.email2.ui.MailActivityEmail;
import com.android.emailcommon.Logging;
import com.android.emailcommon.TrafficFlags;
import com.android.emailcommon.internet.MimeUtility;
import com.android.emailcommon.mail.AuthenticationFailedException;
import com.android.emailcommon.mail.FetchProfile;
import com.android.emailcommon.mail.Flag;
import com.android.emailcommon.mail.Folder;
import com.android.emailcommon.mail.Folder.FolderType;
import com.android.emailcommon.mail.Folder.MessageRetrievalListener;
import com.android.emailcommon.mail.Folder.OpenMode;
import com.android.emailcommon.mail.Message;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.mail.Part;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.MailboxColumns;
import com.android.emailcommon.provider.EmailContent.MessageColumns;
import com.android.emailcommon.provider.EmailContent.SyncColumns;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.service.EmailServiceStatus;
import com.android.emailcommon.service.IEmailServiceCallback;
import com.android.emailcommon.utility.AttachmentUtilities;
import com.android.mail.providers.UIProvider.AccountCapabilities;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
public class Pop3Service extends Service {
private static final String TAG = "Pop3Service";
private static final int MAX_SMALL_MESSAGE_SIZE = (25 * 1024);
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return Service.START_STICKY;
}
// Callbacks as set up via setCallback
private static final RemoteCallbackList<IEmailServiceCallback> mCallbackList =
new RemoteCallbackList<IEmailServiceCallback>();
private interface ServiceCallbackWrapper {
public void call(IEmailServiceCallback cb) throws RemoteException;
}
/**
* Proxy that can be used by various sync adapters to tie into ExchangeService's callback system
* Used this way: ExchangeService.callback().callbackMethod(args...);
* The proxy wraps checking for existence of a ExchangeService instance
* Failures of these callbacks can be safely ignored.
*/
static private final IEmailServiceCallback.Stub sCallbackProxy =
new IEmailServiceCallback.Stub() {
/**
* Broadcast a callback to the everyone that's registered
*
* @param wrapper the ServiceCallbackWrapper used in the broadcast
*/
private synchronized void broadcastCallback(ServiceCallbackWrapper wrapper) {
RemoteCallbackList<IEmailServiceCallback> callbackList = mCallbackList;
if (callbackList != null) {
// Call everyone on our callback list
int count = callbackList.beginBroadcast();
try {
for (int i = 0; i < count; i++) {
try {
wrapper.call(callbackList.getBroadcastItem(i));
} catch (RemoteException e) {
// Safe to ignore
} catch (RuntimeException e) {
// We don't want an exception in one call to prevent other calls, so
// we'll just log this and continue
Log.e(TAG, "Caught RuntimeException in broadcast", e);
}
}
} finally {
// No matter what, we need to finish the broadcast
callbackList.finishBroadcast();
}
}
}
@Override
public void loadAttachmentStatus(final long messageId, final long attachmentId,
final int status, final int progress) {
broadcastCallback(new ServiceCallbackWrapper() {
@Override
public void call(IEmailServiceCallback cb) throws RemoteException {
cb.loadAttachmentStatus(messageId, attachmentId, status, progress);
}
});
}
@Override
public void loadMessageStatus(final long messageId, final int status, final int progress) {
broadcastCallback(new ServiceCallbackWrapper() {
@Override
public void call(IEmailServiceCallback cb) throws RemoteException {
cb.loadMessageStatus(messageId, status, progress);
}
});
}
@Override
public void sendMessageStatus(final long messageId, final String subject, final int status,
final int progress) {
broadcastCallback(new ServiceCallbackWrapper() {
@Override
public void call(IEmailServiceCallback cb) throws RemoteException {
cb.sendMessageStatus(messageId, subject, status, progress);
}
});
}
@Override
public void syncMailboxListStatus(final long accountId, final int status,
final int progress) {
broadcastCallback(new ServiceCallbackWrapper() {
@Override
public void call(IEmailServiceCallback cb) throws RemoteException {
cb.syncMailboxListStatus(accountId, status, progress);
}
});
}
@Override
public void syncMailboxStatus(final long mailboxId, final int status,
final int progress) {
broadcastCallback(new ServiceCallbackWrapper() {
@Override
public void call(IEmailServiceCallback cb) throws RemoteException {
cb.syncMailboxStatus(mailboxId, status, progress);
}
});
}
};
/**
* Create our EmailService implementation here.
*/
private final EmailServiceStub mBinder = new EmailServiceStub() {
@Override
public void setCallback(IEmailServiceCallback cb) throws RemoteException {
mCallbackList.register(cb);
}
@Override
public int getCapabilities(long accountId) throws RemoteException {
return AccountCapabilities.UNDO;
}
};
@Override
public IBinder onBind(Intent intent) {
mBinder.init(this, sCallbackProxy);
return mBinder;
}
private static void sendMailboxStatus(Mailbox mailbox, int status) {
try {
sCallbackProxy.syncMailboxStatus(mailbox.mId, status, 0);
} catch (RemoteException e) {
}
}
/**
* Start foreground synchronization of the specified folder. This is called by
* synchronizeMailbox or checkMail.
* TODO this should use ID's instead of fully-restored objects
* @param account
* @param folder
* @throws MessagingException
*/
public static void synchronizeMailboxSynchronous(Context context, final Account account,
final Mailbox folder) throws MessagingException {
sendMailboxStatus(folder, EmailServiceStatus.IN_PROGRESS);
TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account));
if ((folder.mFlags & Mailbox.FLAG_HOLDS_MAIL) == 0) {
sendMailboxStatus(folder, EmailServiceStatus.SUCCESS);
}
NotificationController nc = NotificationController.getInstance(context);
try {
processPendingActionsSynchronous(context, account);
synchronizeMailboxGeneric(context, account, folder);
// Clear authentication notification for this account
nc.cancelLoginFailedNotification(account.mId);
sendMailboxStatus(folder, EmailServiceStatus.SUCCESS);
} catch (MessagingException e) {
if (Logging.LOGD) {
Log.v(Logging.LOG_TAG, "synchronizeMailbox", e);
}
if (e instanceof AuthenticationFailedException) {
// Generate authentication notification
nc.showLoginFailedNotification(account.mId);
}
sendMailboxStatus(folder, e.getExceptionType());
throw e;
}
}
/**
* Lightweight record for the first pass of message sync, where I'm just seeing if
* the local message requires sync. Later (for messages that need syncing) we'll do a full
* readout from the DB.
*/
private static class LocalMessageInfo {
private static final int COLUMN_ID = 0;
private static final int COLUMN_FLAG_READ = 1;
private static final int COLUMN_FLAG_FAVORITE = 2;
private static final int COLUMN_FLAG_LOADED = 3;
private static final int COLUMN_SERVER_ID = 4;
private static final int COLUMN_FLAGS = 7;
private static final String[] PROJECTION = new String[] {
EmailContent.RECORD_ID,
MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_LOADED,
SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY,
MessageColumns.FLAGS
};
final long mId;
final boolean mFlagRead;
final boolean mFlagFavorite;
final int mFlagLoaded;
final String mServerId;
final int mFlags;
public LocalMessageInfo(Cursor c) {
mId = c.getLong(COLUMN_ID);
mFlagRead = c.getInt(COLUMN_FLAG_READ) != 0;
mFlagFavorite = c.getInt(COLUMN_FLAG_FAVORITE) != 0;
mFlagLoaded = c.getInt(COLUMN_FLAG_LOADED);
mServerId = c.getString(COLUMN_SERVER_ID);
mFlags = c.getInt(COLUMN_FLAGS);
// Note: mailbox key and account key not needed - they are projected for the SELECT
}
}
private static void saveOrUpdate(EmailContent content, Context context) {
if (content.isSaved()) {
content.update(context, content.toContentValues());
} else {
content.save(context);
}
}
/**
* Load the structure and body of messages not yet synced
* @param account the account we're syncing
* @param remoteFolder the (open) Folder we're working on
* @param unsyncedMessages an array of Message's we've got headers for
* @param toMailbox the destination mailbox we're syncing
* @throws MessagingException
*/
static void loadUnsyncedMessages(final Context context, final Account account,
Folder remoteFolder, ArrayList<Message> unsyncedMessages, final Mailbox toMailbox)
throws MessagingException {
// 1. Divide the unsynced messages into small & large (by size)
// TODO doing this work here (synchronously) is problematic because it prevents the UI
// from affecting the order (e.g. download a message because the user requested it.) Much
// of this logic should move out to a different sync loop that attempts to update small
// groups of messages at a time, as a background task. However, we can't just return
// (yet) because POP messages don't have an envelope yet....
ArrayList<Message> largeMessages = new ArrayList<Message>();
ArrayList<Message> smallMessages = new ArrayList<Message>();
for (Message message : unsyncedMessages) {
if (message.getSize() > (MAX_SMALL_MESSAGE_SIZE)) {
largeMessages.add(message);
} else {
smallMessages.add(message);
}
}
// 2. Download small messages
// TODO Problems with this implementation. 1. For IMAP, where we get a real envelope,
// this is going to be inefficient and duplicate work we've already done. 2. It's going
// back to the DB for a local message that we already had (and discarded).
// For small messages, we specify "body", which returns everything (incl. attachments)
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.BODY);
remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]), fp,
new MessageRetrievalListener() {
@Override
public void messageRetrieved(Message message) {
// Store the updated message locally and mark it fully loaded
Utilities.copyOneMessageToProvider(context, message, account, toMailbox,
EmailContent.Message.FLAG_LOADED_COMPLETE);
}
@Override
public void loadAttachmentProgress(int progress) {
}
});
// 3. Download large messages. We ask the server to give us the message structure,
// but not all of the attachments.
fp.clear();
fp.add(FetchProfile.Item.STRUCTURE);
remoteFolder.fetch(largeMessages.toArray(new Message[largeMessages.size()]), fp, null);
for (Message message : largeMessages) {
if (message.getBody() == null) {
// POP doesn't support STRUCTURE mode, so we'll just do a partial download
// (hopefully enough to see some/all of the body) and mark the message for
// further download.
fp.clear();
fp.add(FetchProfile.Item.BODY_SANE);
// TODO a good optimization here would be to make sure that all Stores set
// the proper size after this fetch and compare the before and after size. If
// they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED
remoteFolder.fetch(new Message[] { message }, fp, null);
// Store the partially-loaded message and mark it partially loaded
Utilities.copyOneMessageToProvider(context, message, account, toMailbox,
EmailContent.Message.FLAG_LOADED_PARTIAL);
} else {
// We have a structure to deal with, from which
// we can pull down the parts we want to actually store.
// Build a list of parts we are interested in. Text parts will be downloaded
// right now, attachments will be left for later.
ArrayList<Part> viewables = new ArrayList<Part>();
ArrayList<Part> attachments = new ArrayList<Part>();
MimeUtility.collectParts(message, viewables, attachments);
// Download the viewables immediately
for (Part part : viewables) {
fp.clear();
fp.add(part);
// TODO what happens if the network connection dies? We've got partial
// messages with incorrect status stored.
remoteFolder.fetch(new Message[] { message }, fp, null);
}
// Store the updated message locally and mark it fully loaded
Utilities.copyOneMessageToProvider(context, message, account, toMailbox,
EmailContent.Message.FLAG_LOADED_COMPLETE);
}
}
}
public static void downloadFlagAndEnvelope(final Context context, final Account account,
final Mailbox mailbox, Folder remoteFolder, ArrayList<Message> unsyncedMessages,
HashMap<String, LocalMessageInfo> localMessageMap, final ArrayList<Long> unseenMessages)
throws MessagingException {
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.FLAGS);
fp.add(FetchProfile.Item.ENVELOPE);
final HashMap<String, LocalMessageInfo> localMapCopy;
if (localMessageMap != null)
localMapCopy = new HashMap<String, LocalMessageInfo>(localMessageMap);
else {
localMapCopy = new HashMap<String, LocalMessageInfo>();
}
remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp,
new MessageRetrievalListener() {
@Override
public void messageRetrieved(Message message) {
try {
// Determine if the new message was already known (e.g. partial)
// And create or reload the full message info
LocalMessageInfo localMessageInfo =
localMapCopy.get(message.getUid());
EmailContent.Message localMessage = null;
if (localMessageInfo == null) {
localMessage = new EmailContent.Message();
} else {
localMessage = EmailContent.Message.restoreMessageWithId(
context, localMessageInfo.mId);
}
if (localMessage != null) {
try {
// Copy the fields that are available into the message
LegacyConversions.updateMessageFields(localMessage,
message, account.mId, mailbox.mId);
// Commit the message to the local store
saveOrUpdate(localMessage, context);
// Track the "new" ness of the downloaded message
if (!message.isSet(Flag.SEEN) && unseenMessages != null) {
unseenMessages.add(localMessage.mId);
}
} catch (MessagingException me) {
Log.e(Logging.LOG_TAG,
"Error while copying downloaded message." + me);
}
}
}
catch (Exception e) {
Log.e(Logging.LOG_TAG,
"Error while storing downloaded message." + e.toString());
}
}
@Override
public void loadAttachmentProgress(int progress) {
}
});
}
/**
* Synchronizer for IMAP.
*
* TODO Break this method up into smaller chunks.
*
* @param account the account to sync
* @param mailbox the mailbox to sync
* @return results of the sync pass
* @throws MessagingException
*/
private static void synchronizeMailboxGeneric(final Context context,
final Account account, final Mailbox mailbox) throws MessagingException {
/*
* A list of IDs for messages that were downloaded and did not have the seen flag set.
* This serves as the "true" new message count reported to the user via notification.
*/
final ArrayList<Long> unseenMessages = new ArrayList<Long>();
ContentResolver resolver = context.getContentResolver();
// 0. We do not ever sync DRAFTS or OUTBOX (down or up)
if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) {
return;
}
// 1. Get the message list from the local store and create an index of the uids
Cursor localUidCursor = null;
HashMap<String, LocalMessageInfo> localMessageMap = new HashMap<String, LocalMessageInfo>();
try {
localUidCursor = resolver.query(
EmailContent.Message.CONTENT_URI,
LocalMessageInfo.PROJECTION,
EmailContent.MessageColumns.ACCOUNT_KEY + "=?" +
" AND " + MessageColumns.MAILBOX_KEY + "=?",
new String[] {
String.valueOf(account.mId),
String.valueOf(mailbox.mId)
},
null);
while (localUidCursor.moveToNext()) {
LocalMessageInfo info = new LocalMessageInfo(localUidCursor);
localMessageMap.put(info.mServerId, info);
}
} finally {
if (localUidCursor != null) {
localUidCursor.close();
}
}
// 2. Open the remote folder and create the remote folder if necessary
Store remoteStore = Store.getInstance(account, context);
// The account might have been deleted
if (remoteStore == null) return;
Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
/*
* If the folder is a "special" folder we need to see if it exists
* on the remote server. It if does not exist we'll try to create it. If we
* can't create we'll abort. This will happen on every single Pop3 folder as
* designed and on Imap folders during error conditions. This allows us
* to treat Pop3 and Imap the same in this code.
*/
if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_SENT
|| mailbox.mType == Mailbox.TYPE_DRAFTS) {
if (!remoteFolder.exists()) {
if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) {
return;
}
}
}
// 3, Open the remote folder. This pre-loads certain metadata like message count.
remoteFolder.open(OpenMode.READ_WRITE);
// 4. Trash any remote messages that are marked as trashed locally.
// TODO - this comment was here, but no code was here.
// 5. Get the remote message count.
int remoteMessageCount = remoteFolder.getMessageCount();
ContentValues values = new ContentValues();
values.put(MailboxColumns.TOTAL_COUNT, remoteMessageCount);
mailbox.update(context, values);
// 6. Determine the limit # of messages to download
int visibleLimit = mailbox.mVisibleLimit;
if (visibleLimit <= 0) {
visibleLimit = MailActivityEmail.VISIBLE_LIMIT_DEFAULT;
}
// 7. Create a list of messages to download
Message[] remoteMessages = new Message[0];
final ArrayList<Message> unsyncedMessages = new ArrayList<Message>();
HashMap<String, Message> remoteUidMap = new HashMap<String, Message>();
if (remoteMessageCount > 0) {
/*
* Message numbers start at 1.
*/
int remoteStart = Math.max(0, remoteMessageCount - visibleLimit) + 1;
int remoteEnd = remoteMessageCount;
remoteMessages = remoteFolder.getMessages(remoteStart, remoteEnd, null);
// TODO Why are we running through the list twice? Combine w/ for loop below
for (Message message : remoteMessages) {
remoteUidMap.put(message.getUid(), message);
}
/*
* Get a list of the messages that are in the remote list but not on the
* local store, or messages that are in the local store but failed to download
* on the last sync. These are the new messages that we will download.
* Note, we also skip syncing messages which are flagged as "deleted message" sentinels,
* because they are locally deleted and we don't need or want the old message from
* the server.
*/
for (Message message : remoteMessages) {
LocalMessageInfo localMessage = localMessageMap.get(message.getUid());
// localMessage == null -> message has never been created (not even headers)
// mFlagLoaded = UNLOADED -> message created, but none of body loaded
// mFlagLoaded = PARTIAL -> message created, a "sane" amt of body has been loaded
// mFlagLoaded = COMPLETE -> message body has been completely loaded
// mFlagLoaded = DELETED -> message has been deleted
// Only the first two of these are "unsynced", so let's retrieve them
if (localMessage == null ||
(localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_UNLOADED)) {
unsyncedMessages.add(message);
}
}
}
// 8. Download basic info about the new/unloaded messages (if any)
/*
* Fetch the flags and envelope only of the new messages. This is intended to get us
* critical data as fast as possible, and then we'll fill in the details.
*/
if (unsyncedMessages.size() > 0) {
downloadFlagAndEnvelope(context, account, mailbox, remoteFolder, unsyncedMessages,
localMessageMap, unseenMessages);
}
// 9. Refresh the flags for any messages in the local store that we didn't just download.
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.FLAGS);
remoteFolder.fetch(remoteMessages, fp, null);
boolean remoteSupportsSeen = false;
boolean remoteSupportsFlagged = false;
boolean remoteSupportsAnswered = false;
for (Flag flag : remoteFolder.getPermanentFlags()) {
if (flag == Flag.SEEN) {
remoteSupportsSeen = true;
}
if (flag == Flag.FLAGGED) {
remoteSupportsFlagged = true;
}
if (flag == Flag.ANSWERED) {
remoteSupportsAnswered = true;
}
}
// Update SEEN/FLAGGED/ANSWERED (star) flags (if supported remotely - e.g. not for POP3)
if (remoteSupportsSeen || remoteSupportsFlagged || remoteSupportsAnswered) {
for (Message remoteMessage : remoteMessages) {
LocalMessageInfo localMessageInfo = localMessageMap.get(remoteMessage.getUid());
if (localMessageInfo == null) {
continue;
}
boolean localSeen = localMessageInfo.mFlagRead;
boolean remoteSeen = remoteMessage.isSet(Flag.SEEN);
boolean newSeen = (remoteSupportsSeen && (remoteSeen != localSeen));
boolean localFlagged = localMessageInfo.mFlagFavorite;
boolean remoteFlagged = remoteMessage.isSet(Flag.FLAGGED);
boolean newFlagged = (remoteSupportsFlagged && (localFlagged != remoteFlagged));
int localFlags = localMessageInfo.mFlags;
boolean localAnswered = (localFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0;
boolean remoteAnswered = remoteMessage.isSet(Flag.ANSWERED);
boolean newAnswered = (remoteSupportsAnswered && (localAnswered != remoteAnswered));
if (newSeen || newFlagged || newAnswered) {
Uri uri = ContentUris.withAppendedId(
EmailContent.Message.CONTENT_URI, localMessageInfo.mId);
ContentValues updateValues = new ContentValues();
updateValues.put(MessageColumns.FLAG_READ, remoteSeen);
updateValues.put(MessageColumns.FLAG_FAVORITE, remoteFlagged);
if (remoteAnswered) {
localFlags |= EmailContent.Message.FLAG_REPLIED_TO;
} else {
localFlags &= ~EmailContent.Message.FLAG_REPLIED_TO;
}
updateValues.put(MessageColumns.FLAGS, localFlags);
resolver.update(uri, updateValues, null, null);
}
}
}
// 10. Remove any messages that are in the local store but no longer on the remote store.
HashSet<String> localUidsToDelete = new HashSet<String>(localMessageMap.keySet());
localUidsToDelete.removeAll(remoteUidMap.keySet());
for (String uidToDelete : localUidsToDelete) {
LocalMessageInfo infoToDelete = localMessageMap.get(uidToDelete);
// Delete associated data (attachment files)
// Attachment & Body records are auto-deleted when we delete the Message record
AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId,
infoToDelete.mId);
// Delete the message itself
Uri uriToDelete = ContentUris.withAppendedId(
EmailContent.Message.CONTENT_URI, infoToDelete.mId);
resolver.delete(uriToDelete, null, null);
// Delete extra rows (e.g. synced or deleted)
Uri syncRowToDelete = ContentUris.withAppendedId(
EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId);
resolver.delete(syncRowToDelete, null, null);
Uri deletERowToDelete = ContentUris.withAppendedId(
EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId);
resolver.delete(deletERowToDelete, null, null);
}
loadUnsyncedMessages(context, account, remoteFolder, unsyncedMessages, mailbox);
// 14. Clean up and report results
remoteFolder.close(false);
}
/**
* Find messages in the updated table that need to be written back to server.
*
* Handles:
* Read/Unread
* Flagged
* Append (upload)
* Move To Trash
* Empty trash
* TODO:
* Move
*
* @param account the account to scan for pending actions
* @throws MessagingException
*/
private static void processPendingActionsSynchronous(Context context, Account account)
throws MessagingException {
TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account));
String[] accountIdArgs = new String[] { Long.toString(account.mId) };
// Handle deletes first, it's always better to get rid of things first
processPendingDeletesSynchronous(context, account, accountIdArgs);
}
/**
* Scan for messages that are in the Message_Deletes table, look for differences that
* we can deal with, and do the work.
*
* @param account
* @param resolver
* @param accountIdArgs
*/
private static void processPendingDeletesSynchronous(Context context, Account account,
String[] accountIdArgs) {
Cursor deletes = context.getContentResolver().query(
EmailContent.Message.DELETED_CONTENT_URI,
EmailContent.Message.CONTENT_PROJECTION,
EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs,
EmailContent.MessageColumns.MAILBOX_KEY);
try {
// loop through messages marked as deleted
while (deletes.moveToNext()) {
EmailContent.Message oldMessage =
EmailContent.getContent(deletes, EmailContent.Message.class);
// Finally, delete the update
Uri uri = ContentUris.withAppendedId(EmailContent.Message.DELETED_CONTENT_URI,
oldMessage.mId);
context.getContentResolver().delete(uri, null, null);
}
} finally {
deletes.close();
}
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright (C) 2012 Google Inc.
* Licensed to The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email2.ui;
import com.android.mail.providers.Account;
import com.android.mail.ui.FolderSelectionActivity;
import com.android.mail.ui.MailboxSelectionActivity;
import com.android.mail.utils.AccountUtils;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
public class CreateShortcutActivityEmail extends Activity {
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
Account[] cachedAccounts = AccountUtils.getSyncingAccounts(this);
Intent intent = getIntent();
if (cachedAccounts != null && cachedAccounts.length == 1) {
intent.setClass(this, FolderSelectionActivity.class);
intent.setFlags(
Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_FORWARD_RESULT);
intent.setAction(Intent.ACTION_CREATE_SHORTCUT);
intent.putExtra(FolderSelectionActivity.EXTRA_ACCOUNT_SHORTCUT,
cachedAccounts[0]);
} else {
intent.setClass(this, MailboxSelectionActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
}
startActivity(intent);
finish();
}
}

View File

@ -0,0 +1,240 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email2.ui;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Bundle;
import android.util.Log;
import com.android.email.NotificationController;
import com.android.email.Preferences;
import com.android.email.R;
import com.android.email.service.AttachmentDownloadService;
import com.android.email.service.EmailServiceUtils;
import com.android.email.service.MailService;
import com.android.emailcommon.Logging;
import com.android.emailcommon.TempDirectory;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.service.EmailServiceProxy;
import com.android.emailcommon.utility.EmailAsyncTask;
import com.android.emailcommon.utility.Utility;
public class MailActivityEmail extends com.android.mail.ui.MailActivity {
/**
* If this is enabled there will be additional logging information sent to
* Log.d, including protocol dumps.
*
* This should only be used for logs that are useful for debbuging user problems,
* not for internal/development logs.
*
* This can be enabled by typing "debug" in the AccountFolderList activity.
* Changing the value to 'true' here will likely have no effect at all!
*
* TODO: rename this to sUserDebug, and rename LOGD below to DEBUG.
*/
public static boolean DEBUG;
// Exchange debugging flags (passed to Exchange, when available, via EmailServiceProxy)
public static boolean DEBUG_EXCHANGE;
public static boolean DEBUG_VERBOSE;
public static boolean DEBUG_FILE;
/**
* If true, inhibit hardware graphics acceleration in UI (for a/b testing)
*/
public static boolean sDebugInhibitGraphicsAcceleration = false;
/**
* Specifies how many messages will be shown in a folder by default. This number is set
* on each new folder and can be incremented with "Load more messages..." by the
* VISIBLE_LIMIT_INCREMENT
*/
public static final int VISIBLE_LIMIT_DEFAULT = 25;
/**
* Number of additional messages to load when a user selects "Load more messages..."
*/
public static final int VISIBLE_LIMIT_INCREMENT = 25;
/**
* This is used to force stacked UI to return to the "welcome" screen any time we change
* the accounts list (e.g. deleting accounts in the Account Manager preferences.)
*/
private static boolean sAccountsChangedNotification = false;
private static String sMessageDecodeErrorString;
private static Thread sUiThread;
/**
* Asynchronous version of {@link #setServicesEnabledSync(Context)}. Use when calling from
* UI thread (or lifecycle entry points.)
*
* @param context
*/
public static void setServicesEnabledAsync(final Context context) {
EmailAsyncTask.runAsyncParallel(new Runnable() {
@Override
public void run() {
setServicesEnabledSync(context);
}
});
}
/**
* Called throughout the application when the number of accounts has changed. This method
* enables or disables the Compose activity, the boot receiver and the service based on
* whether any accounts are configured.
*
* Blocking call - do not call from UI/lifecycle threads.
*
* @param context
* @return true if there are any accounts configured.
*/
public static boolean setServicesEnabledSync(Context context) {
Cursor c = null;
try {
c = context.getContentResolver().query(
Account.CONTENT_URI,
Account.ID_PROJECTION,
null, null, null);
boolean enable = c.getCount() > 0;
setServicesEnabled(context, enable);
return enable;
} finally {
if (c != null) {
c.close();
}
}
}
private static void setServicesEnabled(Context context, boolean enabled) {
PackageManager pm = context.getPackageManager();
pm.setComponentEnabledSetting(
new ComponentName(context, MailService.class),
enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
pm.setComponentEnabledSetting(
new ComponentName(context, AttachmentDownloadService.class),
enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
// Start/stop the various services depending on whether there are any accounts
startOrStopService(enabled, context, new Intent(context, AttachmentDownloadService.class));
NotificationController.getInstance(context).watchForMessages(enabled);
}
/**
* Starts or stops the service as necessary.
* @param enabled If {@code true}, the service will be started. Otherwise, it will be stopped.
* @param context The context to manage the service with.
* @param intent The intent of the service to be managed.
*/
private static void startOrStopService(boolean enabled, Context context, Intent intent) {
if (enabled) {
context.startService(intent);
} else {
context.stopService(intent);
}
}
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
sUiThread = Thread.currentThread();
Preferences prefs = Preferences.getPreferences(this);
DEBUG = prefs.getEnableDebugLogging();
sDebugInhibitGraphicsAcceleration = prefs.getInhibitGraphicsAcceleration();
enableStrictMode(prefs.getEnableStrictMode());
TempDirectory.setTempDirectory(this);
// Enable logging in the EAS service, so it starts up as early as possible.
updateLoggingFlags(this);
// Get a helper string used deep inside message decoders (which don't have context)
sMessageDecodeErrorString = getString(R.string.message_decode_error);
// Make sure all required services are running when the app is started (can prevent
// issues after an adb sync/install)
setServicesEnabledAsync(this);
}
/**
* Load enabled debug flags from the preferences and update the EAS debug flag.
*/
public static void updateLoggingFlags(Context context) {
Preferences prefs = Preferences.getPreferences(context);
int debugLogging = prefs.getEnableDebugLogging() ? EmailServiceProxy.DEBUG_BIT : 0;
int verboseLogging =
prefs.getEnableExchangeLogging() ? EmailServiceProxy.DEBUG_VERBOSE_BIT : 0;
int fileLogging =
prefs.getEnableExchangeFileLogging() ? EmailServiceProxy.DEBUG_FILE_BIT : 0;
int enableStrictMode =
prefs.getEnableStrictMode() ? EmailServiceProxy.DEBUG_ENABLE_STRICT_MODE : 0;
int debugBits = debugLogging | verboseLogging | fileLogging | enableStrictMode;
EmailServiceUtils.setRemoteServicesLogging(context, debugBits);
}
/**
* Internal, utility method for logging.
* The calls to log() must be guarded with "if (Email.LOGD)" for performance reasons.
*/
public static void log(String message) {
Log.d(Logging.LOG_TAG, message);
}
/**
* Called by the accounts reconciler to notify that accounts have changed, or by "Welcome"
* to clear the flag.
* @param setFlag true to set the notification flag, false to clear it
*/
public static synchronized void setNotifyUiAccountsChanged(boolean setFlag) {
sAccountsChangedNotification = setFlag;
}
/**
* Called from activity onResume() functions to check for an accounts-changed condition, at
* which point they should finish() and jump to the Welcome activity.
*/
public static synchronized boolean getNotifyUiAccountsChanged() {
return sAccountsChangedNotification;
}
public static void warnIfUiThread() {
if (Thread.currentThread().equals(sUiThread)) {
Log.w(Logging.LOG_TAG, "Method called on the UI thread", new Exception("STACK TRACE"));
}
}
/**
* Retrieve a simple string that can be used when message decoders encounter bad data.
* This is provided here because the protocol decoders typically don't have mContext.
*/
public static String getMessageDecodeErrorString() {
return sMessageDecodeErrorString != null ? sMessageDecodeErrorString : "";
}
public static void enableStrictMode(boolean enabled) {
Utility.enableStrictMode(enabled);
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email2.ui;
/* An activity that shows the list of all the available accounts and return the
* one selected in onResult().
*/
public class MailboxSelectionActivityEmail extends com.android.mail.ui.MailboxSelectionActivity {
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.mail.browse;
import com.android.mail.browse.ConversationCursor.ConversationProvider;
import java.lang.Override;
public class EmailConversationProvider extends ConversationProvider {
// The authority of our conversation provider (a forwarding provider)
// This string must match the declaration in AndroidManifest.xml
private static final String sAuthority = "com.android.email2.conversation.provider";
@Override
protected String getAuthority() {
return sAuthority;
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.mail.providers;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import com.android.email.activity.setup.AccountSettings;
public class EmailAccountCacheProvider extends MailAppProvider {
// Content provider for Email
private static final String sAuthority = "com.android.email2.accountcache";
/**
* Authority for the suggestions provider. This is specified in AndroidManifest.xml and
* res/xml/searchable.xml.
*/
private static final String sSuggestionsAuthority = "com.android.email.suggestionsprovider";
@Override
protected String getAuthority() {
return sAuthority;
}
@Override
protected Intent getNoAccountsIntent(Context context) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_EDIT);
intent.setData(Uri.parse("content://ui.email.android.com/settings"));
intent.putExtra(AccountSettings.EXTRA_NO_ACCOUNTS, true);
return intent;
}
@Override
public String getSuggestionAuthority() {
return sSuggestionsAuthority;
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.mail.providers.protos.boot;
import com.android.mail.providers.MailAppProvider;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
public class AccountReceiver extends BroadcastReceiver {
/**
* Intent used to notify interested parties that the Mail provider has been created.
*/
public static final String ACTION_PROVIDER_CREATED
= "com.android.email2.providers.protos.boot.intent.ACTION_PROVIDER_CREATED";
private static final Uri ACCOUNTS_URI =
Uri.parse("content://com.android.email.provider/uiaccts");
@Override
public void onReceive(Context context, Intent intent) {
MailAppProvider.addAccountsForUriAsync(ACCOUNTS_URI);
}
}

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) 2012, Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.mail.utils;
public class LogTag {
private static String LOG_TAG = "Email";
/**
* Get the log tag to apply to logging.
*/
public static String getLogTag() {
return LOG_TAG;
}
}

34
tests/oldAndroid.mk Normal file
View File

@ -0,0 +1,34 @@
# Copyright 2008, The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
# We only want this apk build for tests.
LOCAL_MODULE_TAGS := tests
LOCAL_JAVA_LIBRARIES := android.test.runner
# Include all test java files.
LOCAL_SRC_FILES := $(call all-java-files-under, src)
# Notice that we don't have to include the src files of Email because, by
# running the tests using an instrumentation targeting Email, we
# automatically get all of its classes loaded into our environment.
LOCAL_PACKAGE_NAME := EmailTests
LOCAL_INSTRUMENTATION_FOR := Email
include $(BUILD_PACKAGE)