Java tutorial
/* * Copyright (C) 2010 Felix Bechstein * * This file is part of SMSdroid. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 3 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * this program; If not, see <http://www.gnu.org/licenses/>. */ package me.myatminsoe.myansms; import android.Manifest; import android.app.Activity; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Environment; import android.provider.CallLog.Calls; import android.util.Log; import android.view.View; import android.view.View.OnLongClickListener; import android.widget.Toast; import org.apache.commons.io.IOUtils; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.LinkedHashMap; /** * Class holding a single message. * * @author flx */ public final class Message { /** * Tag for logging. */ static final String TAG = "msg"; /** * Bitmap showing the play button. */ public static final Bitmap BITMAP_PLAY = Bitmap.createBitmap(1, 1, Config.RGB_565); /** * Filename for saved attachments. */ public static final String ATTACHMENT_FILE = "mms."; /** * Cache size. */ private static final int CAHCESIZE = 50; /** * Internal Cache. */ private static final LinkedHashMap<Integer, Message> CACHE = new LinkedHashMap<>(26, 0.9f, true); /** * INDEX: id. */ public static final int INDEX_ID = 0; /** * INDEX: read. */ public static final int INDEX_READ = 1; /** * INDEX: date. */ public static final int INDEX_DATE = 2; /** * INDEX: thread_id. */ public static final int INDEX_THREADID = 3; /** * INDEX: type. */ public static final int INDEX_TYPE = 4; /** * INDEX: address. */ public static final int INDEX_ADDRESS = 5; /** * INDEX: body. */ public static final int INDEX_BODY = 6; /** * INDEX: subject. */ public static final int INDEX_SUBJECT = 7; /** * INDEX: m_type. */ public static final int INDEX_MTYPE = 8; /** * INDEX: mid. */ public static final int INDEX_MID = 1; /** * INDEX: content type. */ public static final int INDEX_CT = 2; /** * Cursor's projection. */ public static final String[] PROJECTION = { // "_id", // 0 "read", // 1 Calls.DATE, // 2 "thread_id", // 3 Calls.TYPE, // 4 "address", // 5 "body", // 6 }; /** * Cursor's projection. */ public static final String[] PROJECTION_SMS = { // PROJECTION[INDEX_ID], // 0 PROJECTION[INDEX_READ], // 1 PROJECTION[INDEX_DATE], // 2 PROJECTION[INDEX_THREADID], // 3 PROJECTION[INDEX_TYPE], // 4 PROJECTION[INDEX_ADDRESS], // 5 PROJECTION[INDEX_BODY], // 6 }; /** * Cursor's projection. */ public static final String[] PROJECTION_MMS = { // PROJECTION[INDEX_ID], // 0 PROJECTION[INDEX_READ], // 1 PROJECTION[INDEX_DATE], // 2 PROJECTION[INDEX_THREADID], // 3 "m_type", // 4 PROJECTION[INDEX_ID], // 5 PROJECTION[INDEX_ID], // 6 "sub", // 7 "m_type", // 8 }; /** * Cursor's projection. */ public static final String[] PROJECTION_JOIN = { // PROJECTION[INDEX_ID], // 0 PROJECTION[INDEX_READ], // 1 PROJECTION[INDEX_DATE], // 2 PROJECTION[INDEX_THREADID], // 3 PROJECTION[INDEX_TYPE], // 4 PROJECTION[INDEX_ADDRESS], // 5 PROJECTION[INDEX_BODY], // 6 "sub", // 7 "m_type", // 8 }; /** * Cursor's projection for set read/unread operations. */ public static final String[] PROJECTION_READ = { // PROJECTION[INDEX_ID], // 0 PROJECTION[INDEX_READ], // 1 PROJECTION[INDEX_DATE], // 2 PROJECTION[INDEX_THREADID], // 3 }; /** * {@link Uri} for parts. */ private static final Uri URI_PARTS = Uri.parse("content://mms/part/"); /** * Cursor's projection for parts. */ public static final String[] PROJECTION_PARTS = { // "_id", // 0 "mid", // 1 "ct", // 2 }; /** * SQL WHERE: read/unread messages. */ static final String SELECTION_READ_UNREAD = "read = ?"; /** * SQL WHERE: unread messages. */ static final String[] SELECTION_UNREAD = new String[] { "0" }; /** * SQL WHERE: read messages. */ static final String[] SELECTION_READ = new String[] { "1" }; /** * Cursor's sort, upside down. */ public static final String SORT_USD = Calls.DATE + " ASC"; /** * Cursor's sort, normal. */ public static final String SORT_NORM = Calls.DATE + " DESC"; /** * Type for incoming sms. */ public static final int SMS_IN = Calls.INCOMING_TYPE; /** * Type for outgoing sms. */ public static final int SMS_OUT = Calls.OUTGOING_TYPE; /** * Type for sms drafts. */ public static final int SMS_DRAFT = 3; /** Type for pending sms. */ // TODO public static final int SMS_PENDING = 4; /** * Type for incoming mms. */ public static final int MMS_IN = 132; /** * Type for outgoing mms. */ public static final int MMS_OUT = 128; /** Type for mms drafts. */ // public static final int MMS_DRAFT = 128; /** Type for pending mms. */ // public static final int MMS_PENDING = 128; /** * Type for not yet loaded mms. */ public static final int MMS_TOLOAD = 130; /** * Id. */ private final long id; /** * ThreadId. */ private final long threadId; /** * Date. */ private long date; /** * Address. */ private String address; /** * Body. */ private CharSequence body; /** * Type. */ private int type; /** * Read status. */ private int read; /** * Subject. */ private String subject = null; /** * Picture. */ private Bitmap picture = null; /** * {@link Integer} to for viewing the content. */ private Intent contentIntent = null; /** * Is this message a MMS? */ private final boolean isMms; /** * Default constructor. * * @param context {@link Context} to spawn the {@link SmileyParser}. * @param cursor {@link Cursor} to read the data */ private Message(final Context context, final Cursor cursor) { id = cursor.getLong(INDEX_ID); threadId = cursor.getLong(INDEX_THREADID); date = cursor.getLong(INDEX_DATE); if (date < ConversationListActivity.MIN_DATE) { date *= ConversationListActivity.MILLIS; } if (cursor.getColumnIndex(PROJECTION_JOIN[INDEX_TYPE]) >= 0) { address = cursor.getString(INDEX_ADDRESS); body = cursor.getString(INDEX_BODY); } else { body = null; address = null; } type = cursor.getInt(INDEX_TYPE); read = cursor.getInt(INDEX_READ); if (body == null) { isMms = true; try { fetchMmsParts(context); } catch (OutOfMemoryError e) { Log.e(TAG, "error loading parts", e); try { Toast.makeText(context, e.getMessage(), Toast.LENGTH_LONG).show(); } catch (Exception e1) { Log.e(TAG, "error creating Toast", e1); } } } else { isMms = false; } try { subject = cursor.getString(INDEX_SUBJECT); } catch (IllegalStateException e) { subject = null; } try { if (cursor.getColumnCount() > INDEX_MTYPE) { final int t = cursor.getInt(INDEX_MTYPE); if (t != 0) { type = t; } } } catch (IllegalStateException e) { subject = null; } // Log.d(TAG, "subject: ", subject); // Log.d(TAG, "body: ", body); // Log.d(TAG, "type: ", type); } /** * Update {@link Message}. * * @param cursor {@link Cursor} to read from */ public void update(final Cursor cursor) { read = cursor.getInt(INDEX_READ); type = cursor.getInt(INDEX_TYPE); try { if (cursor.getColumnCount() > INDEX_MTYPE) { final int t = cursor.getInt(INDEX_MTYPE); if (t != 0) { type = t; } } } catch (IllegalStateException e) { Log.e(TAG, "wrong projection?", e); } } /** * Fetch a part. * * @param is {@link InputStream} * @return part */ private CharSequence fetchPart(final InputStream is) { Log.d(TAG, "fetchPart(cr, is)"); String ret = null; // get part final ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { byte[] buffer = new byte[256]; int len = is.read(buffer); while (len >= 0) { baos.write(buffer, 0, len); len = is.read(buffer); } ret = new String(baos.toByteArray()); } catch (IOException e) { } finally { if (is != null) { try { is.close(); } catch (IOException e) { } // Ignore } } return ret; } /** * Fetch MMS parts. * * @param context {@link Context} */ private void fetchMmsParts(final Context context) { final ContentResolver cr = context.getContentResolver(); Cursor cursor = cr.query(URI_PARTS, null, PROJECTION_PARTS[INDEX_MID] + " = ?", new String[] { String.valueOf(id) }, null); if (cursor == null || !cursor.moveToFirst()) { return; } final int iID = cursor.getColumnIndex(PROJECTION_PARTS[INDEX_ID]); final int iCT = cursor.getColumnIndex(PROJECTION_PARTS[INDEX_CT]); final int iText = cursor.getColumnIndex("text"); do { final int pid = cursor.getInt(iID); final String ct = cursor.getString(iCT); // get part InputStream is = null; final Uri uri = ContentUris.withAppendedId(URI_PARTS, pid); if (uri == null) { continue; } try { is = cr.openInputStream(uri); } catch (IOException | NullPointerException e) { } if (is == null) { if (iText >= 0 && ct != null && ct.startsWith("text/")) { body = cursor.getString(iText); } continue; } if (ct == null) { continue; } if (ct.startsWith("image/")) { picture = BitmapFactory.decodeStream(is); final Intent i = new Intent(Intent.ACTION_VIEW); i.setDataAndType(uri, ct); i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); contentIntent = i; continue; // skip the rest } else if (ct.startsWith("video/") || ct.startsWith("audio/")) { picture = BITMAP_PLAY; final Intent i = new Intent(Intent.ACTION_VIEW); i.setDataAndType(uri, ct); i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); contentIntent = i; continue; // skip the rest } else if (ct.startsWith("text/")) { body = fetchPart(is); } try { is.close(); } catch (IOException e) { } // Ignore } while (cursor.moveToNext()); } /** * Get a {@link Message} from cache or {@link Cursor}. * * @param context {@link Context} * @param cursor {@link Cursor} * @return {@link Message} */ public static Message getMessage(final Context context, final Cursor cursor) { synchronized (CACHE) { String body = cursor.getString(INDEX_BODY); int id = cursor.getInt(INDEX_ID); if (body == null) { // MMS id *= -1; } Message ret = CACHE.get(id); if (ret == null) { ret = new Message(context, cursor); CACHE.put(id, ret); while (CACHE.size() > CAHCESIZE) { Integer i = CACHE.keySet().iterator().next(); Message cc = CACHE.remove(i); if (cc == null) { break; } } } else { ret.update(cursor); } return ret; } } /** * Flush all cached messages. */ public static void flushCache() { synchronized (CACHE) { CACHE.clear(); } } /** * @return the id */ public long getId() { return id; } /** * @return the threadId */ public long getThreadId() { return threadId; } /** * @return the date */ public long getDate() { return date; } /** * @param context {@link Context} to query SMS DB for an address. * @return the address */ public String getAddress(final Context context) { if (address == null && context != null) { final String select = Message.PROJECTION[Message.INDEX_THREADID] + " = '" + getThreadId() + "' and " + Message.PROJECTION[Message.INDEX_ADDRESS] + " != ''"; final Cursor cur = context.getContentResolver().query(Uri.parse("content://sms/"), Message.PROJECTION, select, null, null); if (cur != null && cur.moveToFirst()) { address = cur.getString(Message.INDEX_ADDRESS); } if (cur != null) { cur.close(); } } return address; } /** * @return the body */ public CharSequence getBody() { return body; } /** * @return the type */ public int getType() { return type; } /** * @return the read */ public int getRead() { return read; } /** * @return is this message a MMS? */ public boolean isMMS() { return isMms; } /** * @return the subject */ public String getSubject() { return subject; } /** * @return the picture */ public Bitmap getPicture() { return picture; } /** * @return {@link Intent} to the content */ public Intent getContentIntent() { return contentIntent; } /** * Get a {@link OnLongClickListener} to save the attachment. * * @param context {@link Context} * @return {@link OnLongClickListener} */ public OnLongClickListener getSaveAttachmentListener(final Activity context) { if (contentIntent == null) { return null; } return new OnLongClickListener() { @Override public boolean onLongClick(final View v) { // check/request permission Manifest.permission.WRITE_EXTERNAL_STORAGE if (!SMSdroid.requestPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE, 0, R.string.permissions_write_external_storage, null)) { return true; } try { String fn = ATTACHMENT_FILE; final Intent ci = Message.this.contentIntent; final String ct = ci.getType(); if (ct == null) { fn += "null"; } else if (ct.startsWith("image/")) { switch (ct) { case "image/jpeg": fn += "jpg"; break; case "image/gif": fn += "gif"; break; default: fn += "png"; break; } } else if (ct.startsWith("audio/")) { switch (ct) { case "audio/3gpp": fn += "3gpp"; break; case "audio/mpeg": fn += "mp3"; break; case "audio/mid": fn += "mid"; break; default: fn += "wav"; break; } } else if (ct.startsWith("video/")) { if (ct.equals("video/3gpp")) { fn += "3gpp"; } else { fn += "avi"; } } else { fn += "ukn"; } final File file = Message.this.createUniqueFile(Environment.getExternalStorageDirectory(), fn); //noinspection ConstantConditions InputStream in = context.getContentResolver().openInputStream(ci.getData()); OutputStream out = new FileOutputStream(file); IOUtils.copy(in, out); out.flush(); out.close(); //noinspection ConstantConditions in.close(); Toast.makeText(context, context.getString(R.string.attachment_saved) + " " + fn, Toast.LENGTH_LONG).show(); return true; } catch (IOException e) { Toast.makeText(context, R.string.attachment_not_saved, Toast.LENGTH_LONG).show(); } catch (NullPointerException e) { Toast.makeText(context, R.string.attachment_not_saved, Toast.LENGTH_LONG).show(); } return true; } }; } /** * @return {@link Uri} of this {@link Message} */ public Uri getUri() { if (isMms) { return Uri.parse("content://mms/" + id); } else { return Uri.parse("content://sms/" + id); } } /** * Creates a unique file in the given directory by appending a hyphen and a number to the given * filename. * * @param directory directory name * @param filename file name * @return path to file * @author k9mail team */ private File createUniqueFile(final File directory, final String filename) { File file = new File(directory, filename); if (!file.exists()) { return file; } // Get the extension of the file, if any. int index = filename.lastIndexOf('.'); String format; if (index != -1) { String name = filename.substring(0, index); String extension = filename.substring(index); format = name + "-%d" + extension; } else { format = filename + "-%d"; } for (int i = 2; i < Integer.MAX_VALUE; i++) { file = new File(directory, String.format(format, i)); if (!file.exists()) { return file; } } return null; } }