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