1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email;
18
19import com.android.emailcommon.internet.MimeUtility;
20import com.android.emailcommon.provider.EmailContent.Attachment;
21import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
22import com.android.emailcommon.utility.AttachmentUtilities;
23import com.android.emailcommon.utility.Utility;
24
25import android.content.Context;
26import android.content.Intent;
27import android.content.pm.PackageManager;
28import android.content.pm.ResolveInfo;
29import android.database.Cursor;
30import android.net.ConnectivityManager;
31import android.net.Uri;
32import android.provider.Settings;
33import android.text.TextUtils;
34
35import java.util.List;
36
37/**
38 * Encapsulates commonly used attachment information related to suitability for viewing and saving,
39 * based on the attachment's filename and mimetype.
40 */
41public class AttachmentInfo {
42    // Projection which can be used with the constructor taking a Cursor argument
43    public static final String[] PROJECTION = {
44            AttachmentColumns._ID,
45            AttachmentColumns.SIZE,
46            AttachmentColumns.FILENAME,
47            AttachmentColumns.MIME_TYPE,
48            AttachmentColumns.ACCOUNT_KEY,
49            AttachmentColumns.FLAGS
50    };
51    // Offsets into PROJECTION
52    public static final int COLUMN_ID = 0;
53    public static final int COLUMN_SIZE = 1;
54    public static final int COLUMN_FILENAME = 2;
55    public static final int COLUMN_MIME_TYPE = 3;
56    public static final int COLUMN_ACCOUNT_KEY = 4;
57    public static final int COLUMN_FLAGS = 5;
58
59    /** Attachment not denied */
60    public static final int ALLOW           = 0x00;
61    /** Attachment suspected of being malware */
62    public static final int DENY_MALWARE    = 0x01;
63    /** Attachment too large; must download over wi-fi */
64    public static final int DENY_WIFIONLY   = 0x02;
65    /** No receiving intent to handle attachment type */
66    public static final int DENY_NOINTENT   = 0x04;
67    /** Side load of applications is disabled */
68    public static final int DENY_NOSIDELOAD = 0x08;
69    // TODO Remove DENY_APKINSTALL when we can install directly from the Email activity
70    /** Unable to install any APK */
71    public static final int DENY_APKINSTALL = 0x10;
72    /** Security policy prohibits install */
73    public static final int DENY_POLICY = 0x20;
74
75    public final long mId;
76    public final long mSize;
77    public final String mName;
78    public final String mContentType;
79    public final long mAccountKey;
80    public final int mFlags;
81
82    /** Whether or not this attachment can be viewed */
83    public final boolean mAllowView;
84    /** Whether or not this attachment can be saved */
85    public final boolean mAllowSave;
86    /** Whether or not this attachment can be installed [only true for APKs] */
87    public final boolean mAllowInstall;
88    /** Reason(s) why this attachment is denied from being viewed */
89    public final int mDenyFlags;
90
91    public AttachmentInfo(Context context, Attachment attachment) {
92        this(context, attachment.mId, attachment.mSize, attachment.mFileName, attachment.mMimeType,
93                attachment.mAccountKey, attachment.mFlags);
94    }
95
96    public AttachmentInfo(Context context, Cursor c) {
97        this(context, c.getLong(COLUMN_ID), c.getLong(COLUMN_SIZE), c.getString(COLUMN_FILENAME),
98                c.getString(COLUMN_MIME_TYPE), c.getLong(COLUMN_ACCOUNT_KEY),
99                c.getInt(COLUMN_FLAGS));
100    }
101
102    public AttachmentInfo(Context context, AttachmentInfo info) {
103        this(context, info.mId, info.mSize, info.mName, info.mContentType, info.mAccountKey,
104                info.mFlags);
105    }
106
107    public AttachmentInfo(Context context, long id, long size, String fileName, String mimeType,
108            long accountKey, int flags) {
109        mSize = size;
110        mContentType = AttachmentUtilities.inferMimeType(fileName, mimeType);
111        mName = fileName;
112        mId = id;
113        mAccountKey = accountKey;
114        mFlags = flags;
115        boolean canView = true;
116        boolean canSave = true;
117        boolean canInstall = false;
118        int denyFlags = ALLOW;
119
120        // Don't enable the "save" button if we've got no place to save the file
121        if (!Utility.isExternalStorageMounted()) {
122            canSave = false;
123        }
124
125        // Check for acceptable / unacceptable attachments by MIME-type
126        if ((!MimeUtility.mimeTypeMatches(mContentType,
127                AttachmentUtilities.ACCEPTABLE_ATTACHMENT_VIEW_TYPES)) ||
128            (MimeUtility.mimeTypeMatches(mContentType,
129                    AttachmentUtilities.UNACCEPTABLE_ATTACHMENT_VIEW_TYPES))) {
130            canView = false;
131        }
132
133        // Check for unacceptable attachments by filename extension
134        String extension = AttachmentUtilities.getFilenameExtension(mName);
135        if (!TextUtils.isEmpty(extension) &&
136                Utility.arrayContains(AttachmentUtilities.UNACCEPTABLE_ATTACHMENT_EXTENSIONS,
137                        extension)) {
138            canView = false;
139            canSave = false;
140            denyFlags |= DENY_MALWARE;
141        }
142
143        // Check for policy restrictions on download
144        if ((flags & Attachment.FLAG_POLICY_DISALLOWS_DOWNLOAD) != 0) {
145            canView = false;
146            canSave = false;
147            denyFlags |= DENY_POLICY;
148        }
149
150        // Check for installable attachments by filename extension
151        extension = AttachmentUtilities.getFilenameExtension(mName);
152        if (!TextUtils.isEmpty(extension) &&
153                Utility.arrayContains(AttachmentUtilities.INSTALLABLE_ATTACHMENT_EXTENSIONS,
154                        extension)) {
155            boolean sideloadEnabled;
156            sideloadEnabled = Settings.Secure.getInt(context.getContentResolver(),
157                    Settings.Secure.INSTALL_NON_MARKET_APPS, 0 /* sideload disabled */) == 1;
158            canSave &= sideloadEnabled;
159            canView = canSave;
160            canInstall = canSave;
161            if (!sideloadEnabled) {
162                denyFlags |= DENY_NOSIDELOAD;
163            }
164        }
165
166        // Check for file size exceeded
167        // The size limit is overridden when on a wifi connection - any size is OK
168        if (mSize > AttachmentUtilities.MAX_ATTACHMENT_DOWNLOAD_SIZE) {
169            int networkType = EmailConnectivityManager.getActiveNetworkType(context);
170            if (networkType != ConnectivityManager.TYPE_WIFI) {
171                canView = false;
172                canSave = false;
173                denyFlags |= DENY_WIFIONLY;
174            }
175        }
176
177        // Check to see if any activities can view this attachment; if none, we can't view it
178        Intent intent = getAttachmentIntent(context, 0);
179        PackageManager pm = context.getPackageManager();
180        List<ResolveInfo> activityList = pm.queryIntentActivities(intent, 0 /*no account*/);
181        if (activityList.isEmpty()) {
182            canView = false;
183            canSave = false;
184            denyFlags |= DENY_NOINTENT;
185        }
186
187        mAllowView = canView;
188        mAllowSave = canSave;
189        mAllowInstall = canInstall;
190        mDenyFlags = denyFlags;
191    }
192
193    /**
194     * Returns an <code>Intent</code> to load the given attachment.
195     * @param context the caller's context
196     * @param accountId the account associated with the attachment (or 0 if we don't need to
197     *     resolve from attachmentUri to contentUri)
198     * @return an Intent suitable for viewing the attachment
199     */
200    public Intent getAttachmentIntent(Context context, long accountId) {
201        Uri contentUri = getUriForIntent(context, accountId);
202        Intent intent = new Intent(Intent.ACTION_VIEW);
203        intent.setDataAndType(contentUri, mContentType);
204        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
205                | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
206        return intent;
207    }
208
209    protected Uri getUriForIntent(Context context, long accountId) {
210        Uri contentUri = AttachmentUtilities.getAttachmentUri(accountId, mId);
211        if (accountId > 0) {
212            contentUri = AttachmentUtilities.resolveAttachmentIdToContentUri(
213                    context.getContentResolver(), contentUri);
214        }
215
216        return contentUri;
217    }
218
219    /**
220     * An attachment is eligible for download if it can either be viewed or saved (or both)
221     * @return whether the attachment is eligible for download
222     */
223    public boolean isEligibleForDownload() {
224        return mAllowView || mAllowSave;
225    }
226
227    @Override
228    public int hashCode() {
229        return (int) (mId ^ (mId >>> 32));
230    }
231
232    @Override
233    public boolean equals(Object o) {
234        if (o == this) {
235            return true;
236        }
237
238        if ((o == null) || (o.getClass() != getClass())) {
239            return false;
240        }
241
242        return ((AttachmentInfo) o).mId == mId;
243    }
244
245    @Override
246    public String toString() {
247        return "{Attachment " + mId + ":" + mName + "," + mContentType + "," + mSize + "}";
248    }
249}
250