Java tutorial
/* * Copyright 2016 Mobicage NV * * 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. * * @@license_version:1.1@@ */ package com.mobicage.rogerthat.plugins.messaging; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.DataInput; import java.io.DataOutput; 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; import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.params.HttpClientParams; import org.jivesoftware.smack.util.Base64; import org.json.simple.JSONArray; import org.json.simple.JSONValue; import android.annotation.SuppressLint; import android.app.DownloadManager; import android.app.DownloadManager.Query; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.text.TextUtils; import android.util.SparseIntArray; import com.mobicage.rogerth.at.R; import com.mobicage.rogerthat.MainService; import com.mobicage.rogerthat.config.Configuration; import com.mobicage.rogerthat.config.ConfigurationProvider; import com.mobicage.rogerthat.plugins.friends.FriendsPlugin; import com.mobicage.rogerthat.plugins.system.JSEmbedding; import com.mobicage.rogerthat.plugins.system.SystemPlugin; import com.mobicage.rogerthat.util.IOUtils; import com.mobicage.rogerthat.util.RegexPatterns; import com.mobicage.rogerthat.util.Security; import com.mobicage.rogerthat.util.http.HTTPUtil; import com.mobicage.rogerthat.util.logging.L; import com.mobicage.rogerthat.util.pickle.PickleException; import com.mobicage.rogerthat.util.pickle.Pickleable; import com.mobicage.rogerthat.util.pickle.Pickler; import com.mobicage.rogerthat.util.system.SafeBroadcastReceiver; import com.mobicage.rogerthat.util.system.SafeRunnable; import com.mobicage.rogerthat.util.system.SystemUtils; import com.mobicage.rogerthat.util.system.T; import com.mobicage.rogerthat.util.time.TimeUtils; import com.mobicage.rogerthat.util.ui.ImageHelper; import com.mobicage.rpc.CallReceiver; import com.mobicage.rpc.Credentials; import com.mobicage.rpc.IJSONable; import com.mobicage.rpc.IncompleteMessageException; import com.mobicage.rpc.RpcCall; import com.mobicage.rpc.config.CloudConstants; import com.mobicage.to.friends.FriendTO; import com.mobicage.to.friends.ServiceMenuItemTO; import com.mobicage.to.js_embedding.JSEmbeddingItemTO; import com.mobicage.to.messaging.MessageTO; import com.soundcloud.android.crop.CropUtil; public class BrandingMgr implements Pickleable, Closeable { private static class DownloadNotCompletedException extends Exception { private static final long serialVersionUID = 243661742668367591L; } protected static class BrandedItem implements IJSONable, Comparable<BrandedItem> { protected static final int TYPE_MESSAGE = 1; protected static final int TYPE_FRIEND = 2; protected static final int TYPE_GENERIC = 3; protected static final int TYPE_JS_EMBEDDING_PACKET = 4; protected static final int TYPE_LOCAL_FLOW_ATTACHMENT = 5; protected static final int TYPE_LOCAL_FLOW_BRANDING = 6; protected static final int TYPE_ATTACHMENT = 7; protected static final int STATUS_TODO = 1; protected static final int STATUS_DONE = 2; protected static final int STATUS_PROCESSING_CALLS = 3; protected static final int STATUS_DELETED = 4; protected static final SparseIntArray DOWNLOAD_PRIORITIES = new SparseIntArray(); static { DOWNLOAD_PRIORITIES.put(TYPE_MESSAGE, 12); DOWNLOAD_PRIORITIES.put(TYPE_ATTACHMENT, 12); DOWNLOAD_PRIORITIES.put(TYPE_LOCAL_FLOW_ATTACHMENT, 8); DOWNLOAD_PRIORITIES.put(TYPE_LOCAL_FLOW_BRANDING, 8); DOWNLOAD_PRIORITIES.put(TYPE_JS_EMBEDDING_PACKET, 4); DOWNLOAD_PRIORITIES.put(TYPE_GENERIC, 0); DOWNLOAD_PRIORITIES.put(TYPE_FRIEND, -4); } public int type; public int status; public IJSONable object; public String brandingKey; private List<RpcCall> calls = new ArrayList<RpcCall>(); public int attemptsLeft; public Long downloadId; // id used by DownloadManager public BrandedItem(int type, IJSONable object, String brandingKey) { this.type = type; this.status = STATUS_TODO; this.object = object; this.brandingKey = brandingKey; if (type == TYPE_LOCAL_FLOW_ATTACHMENT || type == TYPE_LOCAL_FLOW_BRANDING) { this.attemptsLeft = 3; } else { this.attemptsLeft = 1; } } public BrandedItem(MessageTO message) { this.type = TYPE_MESSAGE; this.status = STATUS_TODO; this.object = message; this.brandingKey = message.branding; this.attemptsLeft = 1; } public BrandedItem(JSEmbeddingItemTO packet) { this.type = TYPE_JS_EMBEDDING_PACKET; this.status = STATUS_TODO; this.object = packet; this.brandingKey = packet.hash; this.attemptsLeft = 3; } public boolean usesDownloadManager() { // DownloadManager.COLUMN_LOCAL_FILENAME is added in API level 11 return Build.VERSION.SDK_INT >= 11 && (this.type == BrandedItem.TYPE_ATTACHMENT || this.type == BrandedItem.TYPE_LOCAL_FLOW_ATTACHMENT); } @Override @SuppressWarnings("unchecked") public Map<String, Object> toJSONMap() { Map<String, Object> obj = new LinkedHashMap<String, Object>(); obj.put("type", this.type); obj.put("status", this.status); obj.put("object", this.object == null ? null : this.object.toJSONMap()); obj.put("branding", this.brandingKey); JSONArray arr = new JSONArray(); for (RpcCall call : this.calls) { Map<String, Object> map = new HashMap<String, Object>(); map.put("function", call.function); map.put("arguments", call.arguments); arr.add(map); } obj.put("calls", arr); obj.put("attempsLeft", this.attemptsLeft); if (this.downloadId != null) { obj.put("downloadId", this.downloadId); } return obj; } @SuppressWarnings("unchecked") public BrandedItem(Map<String, Object> source) throws IncompleteMessageException { this.type = ((Long) source.get("type")).intValue(); this.status = ((Long) source.get("status")).intValue(); this.brandingKey = (String) source.get("branding"); switch (this.type) { case TYPE_MESSAGE: object = new Message((Map<String, Object>) source.get("object")); break; case TYPE_FRIEND: object = new FriendTO((Map<String, Object>) source.get("object")); break; case TYPE_GENERIC: object = (IJSONable) source.get("object"); break; case TYPE_JS_EMBEDDING_PACKET: object = new JSEmbeddingItemTO((Map<String, Object>) source.get("object")); break; case TYPE_LOCAL_FLOW_ATTACHMENT: object = new StartFlowRequest((Map<String, Object>) source.get("object")); break; case TYPE_LOCAL_FLOW_BRANDING: object = new StartFlowRequest((Map<String, Object>) source.get("object")); break; case TYPE_ATTACHMENT: object = new AttachmentDownload((Map<String, Object>) source.get("object")); } this.calls = new ArrayList<RpcCall>(); JSONArray val_arr = (JSONArray) source.get("calls"); if (val_arr != null) { for (int i = 0; i < val_arr.size(); i++) { Map<String, Object> map = (Map<String, Object>) val_arr.get(i); this.calls.add(BrandingMgr.createRpcCall((String) map.get("function"), (Map<String, Object>) map.get("arguments"))); } } Long attemptsLeft = (Long) source.get("attempsLeft"); this.attemptsLeft = attemptsLeft == null ? 1 : attemptsLeft.intValue(); this.downloadId = (Long) source.get("downloadId"); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (!(obj instanceof BrandedItem)) return false; BrandedItem other = (BrandedItem) obj; if (type != other.type) return false; if (status != other.status) return false; if (brandingKey == null) { if (other.brandingKey != null) return false; } else if (!brandingKey.equals(other.brandingKey)) { return false; } if ((object == null && other.object != null) || (object != null && other.object == null)) { return false; } else { if (type == TYPE_MESSAGE) { MessageTO msg = (MessageTO) object; MessageTO otherMsg = (MessageTO) other.object; if (!msg.key.equals(otherMsg.key)) { return false; } } else if (type == TYPE_FRIEND) { FriendTO friend = (FriendTO) object; FriendTO otherFriend = (FriendTO) other.object; if (!friend.email.equals(otherFriend.email)) { return false; } } else if (type == TYPE_JS_EMBEDDING_PACKET) { JSEmbeddingItemTO packet = (JSEmbeddingItemTO) object; JSEmbeddingItemTO otherPacket = (JSEmbeddingItemTO) other.object; if (!packet.name.equals(otherPacket.name)) { return false; } } else if (type == BrandedItem.TYPE_LOCAL_FLOW_ATTACHMENT || type == BrandedItem.TYPE_LOCAL_FLOW_BRANDING) { StartFlowRequest req = (StartFlowRequest) object; StartFlowRequest otherReq = (StartFlowRequest) other.object; if (!req.thread_key.equals(otherReq.thread_key)) { return false; } } else if (type == TYPE_ATTACHMENT) { AttachmentDownload ad = (AttachmentDownload) object; AttachmentDownload otherAd = (AttachmentDownload) other.object; if (!(ad.threadKey.equals(otherAd.threadKey) && ad.messageKey.equals(otherAd.messageKey))) { return false; } } } return true; } @Override public int compareTo(BrandedItem another) { // importance: MESSAGES > JS_EMBEDDING > GENERIC > FRIENDS if (this == another || this.equals(another)) return 0; int thisPriority = BrandedItem.DOWNLOAD_PRIORITIES.get(this.type); int otherPriority = BrandedItem.DOWNLOAD_PRIORITIES.get(another.type); return thisPriority > otherPriority ? -1 : 1; } } public enum ColorScheme { light, dark } public static class Dimension { public final int width; public final int height; public Dimension(int width, int height) { this.width = width; this.height = height; } } public static class BrandingResult { public final File dir; public final File file; public final File watermark; public final Integer color; public final Integer menuItemColor; public final ColorScheme scheme; public final boolean showHeader; public final Dimension dimension1; public final Dimension dimension2; public final String contentType; public final boolean wakelockEnabeld; public final List<String> externalUrlPatterns; public BrandingResult(File dir, File file, File watermark, Integer color, Integer menuItemColor, ColorScheme scheme, boolean showHeader, Dimension dimension1, Dimension dimension2, String contentType, boolean wakelockEnabled, List<String> externalUrlPatterns) { this.dir = dir; this.file = file; this.watermark = watermark; this.color = color; this.menuItemColor = menuItemColor; this.scheme = scheme; this.showHeader = showHeader; this.dimension1 = dimension1; this.dimension2 = dimension2; this.contentType = contentType; this.wakelockEnabeld = wakelockEnabled; this.externalUrlPatterns = externalUrlPatterns; } } private static final String ENCRYPTION_KEY = "acec505e55e120f6"; // secret used in 1.0.1013.A for AES encryption private static final byte[] ENCRYPTION_IV = new byte[] { -66, -70, 3, 86, -32, -49, -37, 46, -88, -126, -108, 26, 113, -37, 27, -111 }; // IV used in 1.0.1013.A for AES encryption private static final String NUNTIUZ_MESSAGE = "<nuntiuz_message/>"; private static final String NUNTIUZ_TIMESTAMP = "<nuntiuz_timestamp/>"; private static final String NUNTIUZ_IDENTITY_NAME = "<nuntiuz_identity_name/>"; protected static final String CONFIGKEY = "BRANDING_MGR"; protected static final String CONFIG_QUEUE = "QUEUE"; public static final String SERVICE_BRANDING_AVAILABLE_INTENT = "com.mobicage.rogerthat.plugins.friends.BRANDING_AVAILABLE"; public static final String GENERIC_BRANDING_AVAILABLE_INTENT = "com.mobicage.rogerthat.plugins.messaging.GENERIC_BRANDING_AVAILABLE"; public static final String SERVICE_EMAIL = "email"; public static final String BRANDING_KEY = "branding"; public static final String ATTACHMENT_AVAILABLE_INTENT = "com.mobicage.rogerthat.plugins.messaging.ATTACHMENT_AVAILABLE_INTENT"; public static final String THREAD_KEY = "thread_key"; public static final String MESSAGE_KEY = "message_key"; public static final String ATTACHMENT_URL_HASH = "attachment_url_hash"; public static final String JS_EMBEDDING_AVAILABLE_INTENT = "com.mobicage.rogerthat.plugins.system.JS_EMBEDDING_AVAILABLE_INTENT"; public static final String JS_EMBEDDING_NAME = "js_embedding_name"; public static final String MUST_DELETE_ATTACHMENTS_INTENT = "com.mobicage.rogerthat.plugins.messaging.MUST_DELETE_ATTACHMENTS_INTENT"; private static final int BUFFER_SIZE = 16384; protected final List<BrandedItem> mQueue = Collections.synchronizedList(new ArrayList<BrandedItem>()); @SuppressLint("UseSparseArrays") protected final Map<Long, BrandedItem> mDownloadMgrQueue = Collections .synchronizedMap(new HashMap<Long, BrandedItem>()); protected HandlerThread mDownloaderThread; protected Handler mDownloaderHandler; protected Object mLock = new Object(); protected Object mFileLock = new Object(); // TBD: all this volatile stuff... why? protected volatile Context mContext; protected volatile ConfigurationProvider mCfgProvider; protected volatile boolean mExternalStorageAvailable; protected volatile boolean mExternalStorageWriteable; protected volatile MainService mMainService; private boolean mInitialized; private byte[] mEncryptionKeyBytes = null; public static BrandingMgr createBrandingMgr(ConfigurationProvider cfgProvider, MainService mainService) { T.UI(); final Configuration cfg = cfgProvider.getConfiguration(CONFIGKEY); final String serializedQueue = cfg.get(CONFIG_QUEUE, ""); BrandingMgr mgr = null; if (!"".equals(serializedQueue)) { try { mgr = (BrandingMgr) Pickler.createObjectFromPickle(Base64.decode(serializedQueue)); } catch (PickleException e) { L.bug(e); } } if (mgr == null) mgr = new BrandingMgr(); return mgr; } private byte[] getEncryptionKey() { T.dontCare(); if (mEncryptionKeyBytes == null) { mEncryptionKeyBytes = Security.md5(ENCRYPTION_KEY + mMainService.getPackageName()); } return mEncryptionKeyBytes; } @Override public void close() { T.UI(); if (!mInitialized) return; mContext.unregisterReceiver(mBroadcastReceiver); Looper looper = mDownloaderThread.getLooper(); if (looper != null) { looper.quit(); } try { mDownloaderThread.join(); } catch (InterruptedException e) { L.d(e); } mInitialized = false; } public boolean isMessageInBrandingQueue(String key) { return getMessageFromQueue(key) != null; } private BrandedItem getMessageFromQueue(String key) { synchronized (mLock) { for (BrandedItem item : mQueue) { if (item.type == BrandedItem.TYPE_MESSAGE && ((MessageTO) item.object).key.equals(key)) { return item; } } return null; } } public boolean isAttachmentInBrandingQueue(String threadKey, String messageKey, String downloadUrl) { return getAttachmentFromQueue(threadKey, messageKey, downloadUrl) != null; } private BrandedItem getAttachmentFromQueue(String threadKey, String messageKey, String downloadUrl) { synchronized (mLock) { for (BrandedItem item : mQueue) { if (item.type == BrandedItem.TYPE_ATTACHMENT) { AttachmentDownload ad = (AttachmentDownload) item.object; if (ad.threadKey.equals(threadKey) && ad.messageKey.equals(messageKey) && ad.download_url.equals(downloadUrl)) { return item; } } } return null; } } public boolean queueIfNeeded(final String function, final IJSONable request, final String messageKey) { T.BIZZ(); synchronized (mLock) { BrandedItem item = getMessageFromQueue(messageKey); if (item == null || item.status == BrandedItem.STATUS_PROCESSING_CALLS) return false; item.calls.add(createRpcCall(function, request.toJSONMap())); save(); return true; } } public void deleteConversation(final String threadKey) { synchronized (mLock) { List<BrandedItem> toBeDeleted = new ArrayList<BrandingMgr.BrandedItem>(); for (BrandedItem item : mQueue) { if (item.type == BrandedItem.TYPE_MESSAGE) { MessageTO message = (MessageTO) item.object; if (threadKey.equals(message.parent_key == null ? message.key : message.parent_key)) { toBeDeleted.add(item); } } else if (item.type == BrandedItem.TYPE_LOCAL_FLOW_ATTACHMENT || item.type == BrandedItem.TYPE_LOCAL_FLOW_BRANDING) { StartFlowRequest req = (StartFlowRequest) item.object; if (threadKey.equals(req.thread_key)) { toBeDeleted.add(item); } } else if (item.type == BrandedItem.TYPE_ATTACHMENT) { AttachmentDownload attachment = (AttachmentDownload) item.object; if (threadKey.equals(attachment.threadKey)) { toBeDeleted.add(item); } } } if (toBeDeleted.size() != 0) { for (BrandedItem item : toBeDeleted) { L.d("Canceling download of " + item.brandingKey); item.status = BrandedItem.STATUS_DELETED; // is needed when item is currently downloading mQueue.remove(item); } save(); } } } private static RpcCall createRpcCall(final String function, final Map<String, Object> request) { final Map<String, Object> arguments = new HashMap<String, Object>(); arguments.put("request", request); return new RpcCall(null, -1, function, arguments); } public boolean queue(MessageTO message) { T.dontCare(); return queue(new BrandedItem(message)); } public boolean queue(FriendTO friend) { T.dontCare(); Set<String> brandings = new HashSet<String>(); if (friend.descriptionBranding != null) brandings.add(friend.descriptionBranding); if (friend.actionMenu != null) { if (friend.actionMenu.branding != null) brandings.add(friend.actionMenu.branding); for (ServiceMenuItemTO smi : friend.actionMenu.items) if (smi.screenBranding != null) brandings.add(smi.screenBranding); if (friend.actionMenu.staticFlowBrandings != null) for (String branding : friend.actionMenu.staticFlowBrandings) brandings.add(branding); } if (friend.contentBrandingHash != null) { brandings.add(friend.contentBrandingHash); } boolean hasQueuedBrandings = false; for (String branding : brandings) { boolean brandingAvailable = false; try { brandingAvailable = isBrandingAvailable(branding); } catch (BrandingFailureException e) { // Assume not available } if (!brandingAvailable) { if (queue(new BrandedItem(BrandedItem.TYPE_FRIEND, friend, branding))) { hasQueuedBrandings = true; } } } return hasQueuedBrandings; } public boolean queue(JSEmbeddingItemTO packet) { T.dontCare(); return queue(new BrandedItem(packet)); } public boolean queue(StartFlowRequest flow) { T.dontCare(); final List<String> items = new ArrayList<String>(); for (String attachment : flow.attachments_to_dwnl) { BrandedItem item = new BrandedItem(BrandedItem.TYPE_LOCAL_FLOW_ATTACHMENT, flow, attachment); if (queue(item)) { items.add(item.brandingKey); } } for (String branding : flow.brandings_to_dwnl) { boolean brandingAvailable = false; try { brandingAvailable = isBrandingAvailable(branding); } catch (BrandingFailureException e) { // Assume not available } if (!brandingAvailable) { BrandedItem item = new BrandedItem(BrandedItem.TYPE_LOCAL_FLOW_BRANDING, flow, branding); if (queue(item)) { items.add(item.brandingKey); } } } if (items.size() > 0) { synchronized (mFileLock) { try { IOUtils.writeToFile(getLocalFlowContentFile(flow.thread_key), items); } catch (Exception e) { L.w("Failed to write localFlow .content file. Flow will start when 1st attachment is downloaded."); } } return true; } return false; } public boolean queue(AttachmentDownload attachment) { T.dontCare(); return queue(new BrandedItem(BrandedItem.TYPE_ATTACHMENT, attachment, attachment.download_url)); } public boolean queueGenericBranding(String brandingKey) { return queue(new BrandedItem(BrandedItem.TYPE_GENERIC, null, brandingKey)); } private boolean queue(BrandedItem item) { if (item.type != BrandedItem.TYPE_MESSAGE && com.mobicage.rogerthat.util.TextUtils.isEmptyOrWhitespace(item.brandingKey)) return false; if (item.type == BrandedItem.TYPE_FRIEND) { // Need to summarize Friend to prevent java.io.UTFDataFormatException: String more than 65535 UTF bytes long FriendTO friend = (FriendTO) item.object; FriendTO friendSummary = new FriendTO(); friendSummary.email = friend.email; item.object = friendSummary; } if (item.usesDownloadManager()) { DownloadManager dwnlManager = getDownloadManager(); DownloadManager.Request request = new DownloadManager.Request(Uri.parse(item.brandingKey)); int flags = DownloadManager.Request.NETWORK_WIFI; if (!mMainService.getPlugin(SystemPlugin.class).getWifiOnlyDownloads()) { flags |= DownloadManager.Request.NETWORK_MOBILE; } request.setAllowedNetworkTypes(flags); final Long id = dwnlManager.enqueue(request); synchronized (mLock) { item.downloadId = id; mDownloadMgrQueue.put(id, item); save(); } } else { synchronized (mLock) { mQueue.add(item); Collections.sort(mQueue); save(); } if (mExternalStorageWriteable) { mDownloaderHandler.post(mQueueProcessor); } } return true; } private DownloadManager getDownloadManager() { return (DownloadManager) mMainService.getSystemService(MainService.DOWNLOAD_SERVICE); } private void deleteItemFromQueue(final BrandedItem item) { deleteItemFromQueue(item, true); } private void deleteItemFromQueue(final BrandedItem item, boolean logIfNotFound) { T.dontCare(); synchronized (mLock) { final boolean found; if (item.usesDownloadManager()) { found = mDownloadMgrQueue.remove(item.downloadId) != null; } else { found = mQueue.remove(item); } if (found) { save(); } else if (logIfNotFound) { L.bug("BrandingMgr.deleteItemFromQueue: item was not in mQueue!"); } } } protected void dequeue(final BrandedItem item, final boolean failed) { synchronized (mLock) { if (item.status == BrandedItem.STATUS_DELETED) { deleteItemFromQueue(item, false); return; } if (failed) { item.attemptsLeft -= 1; if (item.attemptsLeft > 0) { item.status = BrandedItem.STATUS_TODO; deleteItemFromQueue(item, false); queue(item); return; } } item.status = BrandedItem.STATUS_DONE; save(); } if (item.type == BrandedItem.TYPE_MESSAGE) { final MessageTO message = (MessageTO) item.object; mMainService.postOnBIZZHandler(new SafeRunnable() { @Override protected void safeRun() throws Exception { synchronized (mLock) { T.BIZZ(); if (item.status == BrandedItem.STATUS_DELETED) { return; } final MessagingPlugin plugin = mMainService.getPlugin(MessagingPlugin.class); plugin.newMessage(message, true, true); item.status = BrandedItem.STATUS_PROCESSING_CALLS; for (int i = 0; i < item.calls.size(); i++) { try { CallReceiver.processCall(item.calls.get(i)); } catch (Exception e) { L.bug(e); } } deleteItemFromQueue(item); } } }); } else if (item.type == BrandedItem.TYPE_JS_EMBEDDING_PACKET) { mMainService.postOnBIZZHandler(new SafeRunnable() { @Override protected void safeRun() throws Exception { synchronized (mLock) { T.BIZZ(); final JSEmbeddingItemTO packet = (JSEmbeddingItemTO) item.object; deleteItemFromQueue(item); if (!failed) { try { extractJSEmbedding(packet); SystemPlugin systemPlugin = mMainService.getPlugin(SystemPlugin.class); systemPlugin.updateJSEmbeddedPacket(packet.name, packet.hash, JSEmbedding.STATUS_AVAILABLE); Intent intent = new Intent(JS_EMBEDDING_AVAILABLE_INTENT); intent.putExtra(JS_EMBEDDING_NAME, attachmentDownloadUrlHash(packet.name)); mMainService.sendBroadcast(intent); } catch (Exception e) { L.bug("Could not unpack JS Embedding packet", e); } } cleanupJSEmbeddingTmpDownloadFile(packet.name); } } }); } else if (item.type == BrandedItem.TYPE_LOCAL_FLOW_ATTACHMENT || item.type == BrandedItem.TYPE_LOCAL_FLOW_BRANDING) { mMainService.postOnBIZZHandler(new SafeRunnable() { @Override protected void safeRun() throws Exception { T.BIZZ(); synchronized (mLock) { final StartFlowRequest flow = (StartFlowRequest) item.object; deleteItemFromQueue(item); validateStartFlowReady(item, flow); } } }); } else if (item.type == BrandedItem.TYPE_ATTACHMENT) { final AttachmentDownload attachment = (AttachmentDownload) item.object; mMainService.postOnBIZZHandler(new SafeRunnable() { @Override protected void safeRun() throws Exception { synchronized (mLock) { T.BIZZ(); if (item.status == BrandedItem.STATUS_DELETED) { return; } if (attachment.content_type.toLowerCase(Locale.US).startsWith("image/")) { try { // Make sure the image orientation is correct final File attachmentFile = getAttachmentFile(attachment); final String attachmentPath = attachmentFile.getPath(); final int exifRotation = CropUtil.getExifRotation(attachmentPath); Bitmap bm = BitmapFactory.decodeFile(attachmentPath); bm = ImageHelper.rotateBitmap(bm, exifRotation); if (bm != null) { final File tmpFile = new File(attachmentPath + ".tmp"); final FileOutputStream stream = new FileOutputStream(tmpFile); try { bm.compress(Bitmap.CompressFormat.PNG, 100, stream); } finally { stream.close(); } tmpFile.renameTo(attachmentFile); } } catch (Exception e) { L.bug("Failed to rotate the attachment.", e); } } try { final MessagingPlugin messagingPlugin = mMainService.getPlugin(MessagingPlugin.class); messagingPlugin.createAttachmentThumbnail(attachment); } catch (Exception e) { L.bug("Failed to generate attachment thumbnail", e); } Intent intent = new Intent(ATTACHMENT_AVAILABLE_INTENT); intent.putExtra(THREAD_KEY, attachment.threadKey); intent.putExtra(MESSAGE_KEY, attachment.messageKey); intent.putExtra(ATTACHMENT_URL_HASH, attachmentDownloadUrlHash(attachment.download_url)); mMainService.sendBroadcast(intent); deleteItemFromQueue(item); } } }); } else { boolean brandingAvailable = false; try { brandingAvailable = isBrandingAvailable(item.brandingKey); } catch (BrandingFailureException e) { // Assume not available } deleteItemFromQueue(item); if (brandingAvailable) { if (item.type == BrandedItem.TYPE_FRIEND) { FriendTO friend = (FriendTO) item.object; // Caution: this Friend object only has an email property populated Intent intent = new Intent(SERVICE_BRANDING_AVAILABLE_INTENT); intent.putExtra(SERVICE_EMAIL, friend.email); intent.putExtra(BRANDING_KEY, item.brandingKey); mMainService.sendBroadcast(intent); } else if (item.type == BrandedItem.TYPE_GENERIC) { Intent intent = new Intent(GENERIC_BRANDING_AVAILABLE_INTENT); if (item.object != null && item.object instanceof FriendTO) { intent.putExtra(SERVICE_EMAIL, ((FriendTO) item.object).email); } intent.putExtra(BRANDING_KEY, item.brandingKey); mMainService.sendBroadcast(intent); } } } } public boolean isBrandingAvailable(String brandingKey) throws BrandingFailureException { T.dontCare(); if (brandingKey == null) return true; if (!mExternalStorageAvailable) return false; return getBrandingFile(brandingKey).exists() || getOldBrandingFile(brandingKey).exists(); } public BrandingResult prepareBranding(MessageTO message) throws BrandingFailureException { T.UI(); return prepareBranding(new BrandedItem(message), false); } /** * Prepares branding for description screen * * @param friend * @return * @throws BrandingFailureException */ public BrandingResult prepareBranding(FriendTO friend) throws BrandingFailureException { T.UI(); return prepareBranding(new BrandedItem(BrandedItem.TYPE_FRIEND, friend, friend.descriptionBranding), false); } public BrandingResult prepareBranding(String brandingKey, FriendTO friend, boolean jsEnabled) throws BrandingFailureException { T.UI(); BrandedItem item = new BrandedItem(BrandedItem.TYPE_GENERIC, null, brandingKey); item.object = friend; return prepareBranding(item, jsEnabled); } protected BrandingResult prepareBranding(final BrandedItem item, boolean jsEnabled) throws BrandingFailureException { T.UI(); boolean aesEncrypted = true; File brandingCache = getBrandingFile(item.brandingKey); if (!brandingCache.exists()) { brandingCache = getOldBrandingFile(item.brandingKey); aesEncrypted = false; } if (!brandingCache.exists()) throw new BrandingFailureException("Branding package " + item.brandingKey + " not found!"); File tmpBrandingLocation = getBrandingDirectory(); if (!(tmpBrandingLocation.exists() || tmpBrandingLocation.mkdir())) throw new BrandingFailureException("Could not create private branding dir!"); File tmpBrandingDir = getBrandingDirectory(item.brandingKey, tmpBrandingLocation); if (tmpBrandingDir.exists() && !SystemUtils.deleteDir(tmpBrandingDir)) throw new BrandingFailureException("Could not delete existing branding dir"); if (!tmpBrandingDir.mkdir()) throw new BrandingFailureException("Could not create branding dir"); if (jsEnabled) { SystemPlugin systemPlugin = mMainService.getPlugin(SystemPlugin.class); Map<String, JSEmbedding> packets = systemPlugin.getJSEmbeddedPackets(); for (String key : packets.keySet()) { final JSEmbedding packet = packets.get(key); if (packet.getStatus() == JSEmbedding.STATUS_AVAILABLE) { File sourceDir = getJSEmbeddingPacketDirectory(packet.getName()); File targetDir = getJSEmbeddingUnpackDirectory(tmpBrandingDir, packet.getName()); try { L.i("Copying JSEmbedding packet: " + packet.getName()); IOUtils.copyDirectory(sourceDir, targetDir); } catch (IOException e) { L.bug("Could not copy js embedding packet '" + packet.getName() + "'.", e); } } else { L.bug("JSEmbedding packet '" + packet.getName() + "' not downloaded yet. "); JSEmbeddingItemTO jseito = new JSEmbeddingItemTO(); jseito.name = packet.getName(); jseito.hash = packet.getEmeddingHash(); queue(jseito); } } } L.i("Decrypting " + brandingCache); final File tmpDecryptFile = new File(getBrandingRootDirectory(), ".tmp_decrypted_file"); if (tmpDecryptFile.exists() && !tmpDecryptFile.delete()) { throw new BrandingFailureException("Could not remove " + tmpDecryptFile); } try { final BufferedInputStream is = new BufferedInputStream(new FileInputStream(brandingCache)); final FileOutputStream os = new FileOutputStream(tmpDecryptFile); if (aesEncrypted) { Security.decryptAES(getEncryptionKey(), ENCRYPTION_IV, is, os); } } catch (Exception e1) { brandingCache.delete(); queue(item); throw new BrandingFailureException("Could not decrypt branding file " + brandingCache, e1); } BrandingResult br = extractBranding(item, brandingCache, tmpDecryptFile, tmpBrandingDir); if (!aesEncrypted) { L.d("lazily convert " + brandingCache + " encrypted in 1.0.1012.A from DES to AES encryption"); final File dest = getBrandingFile(item.brandingKey); try { if (!dest.exists()) Security.encryptAES(getEncryptionKey(), ENCRYPTION_IV, new FileInputStream(tmpDecryptFile), new FileOutputStream(dest)); } catch (Exception e) { if (dest.exists() && !dest.delete()) L.d("Could not remove " + dest); throw new BrandingFailureException("Could not encrypt " + tmpDecryptFile, e); } finally { brandingCache.delete(); } } return br; } private BrandingResult extractBranding(final BrandedItem item, final File encryptedBrandingFile, final File tmpDecryptedBrandingFile, final File tmpBrandingDir) throws BrandingFailureException { try { L.i("Extracting " + tmpDecryptedBrandingFile + " (" + item.brandingKey + ")"); File brandingFile = new File(tmpBrandingDir, "branding.html"); File watermarkFile = null; Integer backgroundColor = null; Integer menuItemColor = null; ColorScheme scheme = ColorScheme.light; boolean showHeader = true; String contentType = null; boolean wakelockEnabled = false; ByteArrayOutputStream brandingBos = new ByteArrayOutputStream(); try { MessageDigest digester = MessageDigest.getInstance("SHA256"); DigestInputStream dis = new DigestInputStream( new BufferedInputStream(new FileInputStream(tmpDecryptedBrandingFile)), digester); try { ZipInputStream zis = new ZipInputStream(dis); try { byte data[] = new byte[BUFFER_SIZE]; ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { L.d("Extracting: " + entry); int count = 0; if (entry.getName().equals("branding.html")) { while ((count = zis.read(data, 0, BUFFER_SIZE)) != -1) { brandingBos.write(data, 0, count); } } else { if (entry.isDirectory()) { L.d("Skipping branding dir " + entry.getName()); continue; } File destination = new File(tmpBrandingDir, entry.getName()); destination.getParentFile().mkdirs(); if ("__watermark__".equals(entry.getName())) { watermarkFile = destination; } final OutputStream fos = new BufferedOutputStream(new FileOutputStream(destination), BUFFER_SIZE); try { while ((count = zis.read(data, 0, BUFFER_SIZE)) != -1) { fos.write(data, 0, count); } } finally { fos.close(); } } } while (dis.read(data) >= 0) ; } finally { zis.close(); } } finally { dis.close(); } String hexDigest = com.mobicage.rogerthat.util.TextUtils.toHex(digester.digest()); if (!hexDigest.equals(item.brandingKey)) { encryptedBrandingFile.delete(); SystemUtils.deleteDir(tmpBrandingDir); throw new BrandingFailureException("Branding cache was invalid!"); } brandingBos.flush(); byte[] brandingBytes = brandingBos.toByteArray(); if (brandingBytes.length == 0) { encryptedBrandingFile.delete(); SystemUtils.deleteDir(tmpBrandingDir); throw new BrandingFailureException("Invalid branding package!"); } String brandingHtml = new String(brandingBytes, "UTF8"); switch (item.type) { case BrandedItem.TYPE_MESSAGE: MessageTO message = (MessageTO) item.object; brandingHtml = brandingHtml.replace(NUNTIUZ_MESSAGE, TextUtils.htmlEncode(message.message).replace("\r", "").replace("\n", "<br>")); brandingHtml = brandingHtml.replace(NUNTIUZ_TIMESTAMP, TimeUtils.getDayTimeStr(mContext, message.timestamp * 1000)); FriendsPlugin friendsPlugin = mMainService.getPlugin(FriendsPlugin.class); brandingHtml = brandingHtml.replace(NUNTIUZ_IDENTITY_NAME, TextUtils.htmlEncode(friendsPlugin.getName(message.sender))); break; case BrandedItem.TYPE_FRIEND: FriendTO friend = (FriendTO) item.object; // In this case Friend is fully populated brandingHtml = brandingHtml.replace(NUNTIUZ_MESSAGE, TextUtils.htmlEncode(friend.description).replace("\r", "").replace("\n", "<br>")); brandingHtml = brandingHtml.replace(NUNTIUZ_IDENTITY_NAME, TextUtils.htmlEncode(friend.name)); break; case BrandedItem.TYPE_GENERIC: if (item.object instanceof FriendTO) { brandingHtml = brandingHtml.replace(NUNTIUZ_IDENTITY_NAME, TextUtils.htmlEncode(((FriendTO) item.object).name)); } break; } Matcher matcher = RegexPatterns.BRANDING_BACKGROUND_COLOR.matcher(brandingHtml); if (matcher.find()) { String bg = matcher.group(1); if (bg.length() == 4) { StringBuilder sb = new StringBuilder(); sb.append("#"); sb.append(bg.charAt(1)); sb.append(bg.charAt(1)); sb.append(bg.charAt(2)); sb.append(bg.charAt(2)); sb.append(bg.charAt(3)); sb.append(bg.charAt(3)); bg = sb.toString(); } backgroundColor = Color.parseColor(bg); } matcher = RegexPatterns.BRANDING_MENU_ITEM_COLOR.matcher(brandingHtml); if (matcher.find()) { String bg = matcher.group(1); if (bg.length() == 4) { StringBuilder sb = new StringBuilder(); sb.append("#"); sb.append(bg.charAt(1)); sb.append(bg.charAt(1)); sb.append(bg.charAt(2)); sb.append(bg.charAt(2)); sb.append(bg.charAt(3)); sb.append(bg.charAt(3)); bg = sb.toString(); } menuItemColor = Color.parseColor(bg); } matcher = RegexPatterns.BRANDING_COLOR_SCHEME.matcher(brandingHtml); if (matcher.find()) { String schemeStr = matcher.group(1); scheme = "dark".equalsIgnoreCase(schemeStr) ? ColorScheme.dark : ColorScheme.light; } matcher = RegexPatterns.BRANDING_SHOW_HEADER.matcher(brandingHtml); if (matcher.find()) { String showHeaderStr = matcher.group(1); showHeader = "true".equalsIgnoreCase(showHeaderStr); } matcher = RegexPatterns.BRANDING_CONTENT_TYPE.matcher(brandingHtml); if (matcher.find()) { String contentTypeStr = matcher.group(1); L.i("Branding content-type: " + contentTypeStr); if (AttachmentViewerActivity.CONTENT_TYPE_PDF.equalsIgnoreCase(contentTypeStr)) { File tmpBrandingFile = new File(tmpBrandingDir, "embed.pdf"); if (tmpBrandingFile.exists()) { contentType = AttachmentViewerActivity.CONTENT_TYPE_PDF; } } } Dimension dimension1 = null; Dimension dimension2 = null; matcher = RegexPatterns.BRANDING_DIMENSIONS.matcher(brandingHtml); if (matcher.find()) { String dimensionsStr = matcher.group(1); L.i("Branding dimensions: " + dimensionsStr); String[] dimensions = dimensionsStr.split(","); try { dimension1 = new Dimension(Integer.parseInt(dimensions[0]), Integer.parseInt(dimensions[1])); dimension2 = new Dimension(Integer.parseInt(dimensions[2]), Integer.parseInt(dimensions[3])); } catch (Exception e) { L.bug("Invalid branding dimension: " + matcher.group(), e); } } matcher = RegexPatterns.BRANDING_WAKELOCK_ENABLED.matcher(brandingHtml); if (matcher.find()) { String wakelockEnabledStr = matcher.group(1); wakelockEnabled = "true".equalsIgnoreCase(wakelockEnabledStr); } final List<String> externalUrlPatterns = new ArrayList<String>(); matcher = RegexPatterns.BRANDING_EXTERNAL_URLS.matcher(brandingHtml); while (matcher.find()) { externalUrlPatterns.add(matcher.group(1)); } FileOutputStream fos = new FileOutputStream(brandingFile); try { fos.write(brandingHtml.getBytes("UTF8")); } finally { fos.close(); } if (contentType != null && AttachmentViewerActivity.CONTENT_TYPE_PDF.equalsIgnoreCase(contentType)) { brandingFile = new File(tmpBrandingDir, "embed.pdf"); } return new BrandingResult(tmpBrandingDir, brandingFile, watermarkFile, backgroundColor, menuItemColor, scheme, showHeader, dimension1, dimension2, contentType, wakelockEnabled, externalUrlPatterns); } finally { brandingBos.close(); } } catch (IOException e) { L.e(e); throw new BrandingFailureException("Error copying cached branded file to private space", e); } catch (NoSuchAlgorithmException e) { L.e(e); throw new BrandingFailureException("Cannot validate ", e); } } private void extractJSEmbedding(final JSEmbeddingItemTO packet) throws BrandingFailureException, NoSuchAlgorithmException, FileNotFoundException, IOException { File brandingCache = getJSEmbeddingPacketFile(packet.name); if (!brandingCache.exists()) throw new BrandingFailureException("Javascript package not found!"); File jsRootDir = getJSEmbeddingRootDirectory(); if (!(jsRootDir.exists() || jsRootDir.mkdir())) throw new BrandingFailureException("Could not create private javascript dir!"); File jsPacketDir = getJSEmbeddingPacketDirectory(packet.name); if (jsPacketDir.exists() && !SystemUtils.deleteDir(jsPacketDir)) throw new BrandingFailureException("Could not delete existing javascript dir"); if (!jsPacketDir.mkdir()) throw new BrandingFailureException("Could not create javascript dir"); MessageDigest digester = MessageDigest.getInstance("SHA256"); DigestInputStream dis = new DigestInputStream(new BufferedInputStream(new FileInputStream(brandingCache)), digester); try { ZipInputStream zis = new ZipInputStream(dis); try { byte data[] = new byte[BUFFER_SIZE]; ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { L.d("Extracting: " + entry); int count = 0; if (entry.isDirectory()) { L.d("Skipping javascript dir " + entry.getName()); continue; } File destination = new File(jsPacketDir, entry.getName()); destination.getParentFile().mkdirs(); final OutputStream fos = new BufferedOutputStream(new FileOutputStream(destination), BUFFER_SIZE); try { while ((count = zis.read(data, 0, BUFFER_SIZE)) != -1) { fos.write(data, 0, count); } } finally { fos.close(); } } while (dis.read(data) >= 0) ; } finally { zis.close(); } } finally { dis.close(); } } public void validateStartFlowReady(final BrandedItem item, final StartFlowRequest flow) { T.BIZZ(); boolean ready = false; synchronized (mFileLock) { try { final File localFlowContentFile = getLocalFlowContentFile(flow.thread_key); List<String> content; try { content = IOUtils.readAllLinesFromFile(localFlowContentFile); } catch (IOException e) { content = new ArrayList<String>(0); } if (content.contains(item.brandingKey)) { content.remove(item.brandingKey); try { IOUtils.writeToFile(localFlowContentFile, content); } catch (IOException e) { L.w("Error while writing local flow .content file. Launching flow...", e); ready = true; } } ready = content.size() == 0; } catch (BrandingFailureException e) { L.w("Error while checking if local flow was ready. Launching flow...", e); ready = true; } } if (ready) { final MessagingPlugin plugin = mMainService.getPlugin(MessagingPlugin.class); mMainService.postOnUIHandler(new SafeRunnable() { @Override public void safeRun() { T.UI(); plugin.startLocalFlow(flow); } }); } } public void cleanupBranding(String brandingKey) { T.dontCare(); L.d("Cleanup branding " + brandingKey); File parentDir = getBrandingDirectory(); if (!parentDir.exists()) return; File dir = getBrandingDirectory(brandingKey, parentDir); if (!dir.exists()) return; SystemUtils.deleteDir(dir); } public void cleanupJSEmbeddingPacket(String name) { T.dontCare(); try { L.d("Cleanup javascript packet: " + name); File parentDir = getJSEmbeddingRootDirectory(); if (!parentDir.exists()) return; File dir = getJSEmbeddingPacketDirectory(name); if (!dir.exists()) return; SystemUtils.deleteDir(dir); } catch (BrandingFailureException ex) { L.bug("Failed to cleanup JSEmbedding tmp download file with name: " + name, ex); } } private void cleanupJSEmbeddingTmpDownloadFile(String name) { T.dontCare(); try { L.d("Cleanup javascript download packet: " + name); File parentDir = getJSEmbeddingRootDirectory(); if (!parentDir.exists()) return; File tmpFile = getJSEmbeddingPacketFile(name); if (!tmpFile.exists()) return; tmpFile.delete(); } catch (BrandingFailureException ex) { L.bug("Failed to cleanup JSEmbedding tmp download file with name: " + name, ex); } } private File getBrandingDirectory(String brandingKey, File brandingDir) { T.dontCare(); return new File(brandingDir, brandingKey); } private File getBrandingDirectory() { T.dontCare(); return new File(mContext.getCacheDir(), "branding"); } public void initialize(ConfigurationProvider cfgProvider, MainService mainService) { T.UI(); mCfgProvider = cfgProvider; mMainService = mainService; mContext = mainService; List<BrandedItem> copy = new ArrayList<BrandingMgr.BrandedItem>(); Collections.copy(mQueue, copy); for (BrandedItem item : copy) if (item.status != BrandedItem.STATUS_TODO) dequeue(item, false); mDownloaderThread = new HandlerThread("rogerthat_branding_worker"); mDownloaderThread.start(); Looper looper = mDownloaderThread.getLooper(); mDownloaderHandler = new Handler(looper); IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_MEDIA_MOUNTED); filter.addAction(Intent.ACTION_MEDIA_REMOVED); filter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE); mContext.registerReceiver(mBroadcastReceiver, filter); initStorageSettings(); mInitialized = true; mMainService.postOnBIZZHandler(new SafeRunnable() { @Override protected void safeRun() throws Exception { if (mMainService.getPluginDBUpdates(BrandingMgr.class).contains(MUST_DELETE_ATTACHMENTS_INTENT)) { synchronized (mLock) { IOUtils.deleteRecursive(getAttachmentsRootDirectory()); } mMainService.clearPluginDBUpdate(BrandingMgr.class, MUST_DELETE_ATTACHMENTS_INTENT); } } }); } private final BroadcastReceiver mBroadcastReceiver = new SafeBroadcastReceiver() { @Override public String[] onSafeReceive(Context context, Intent intent) { T.UI(); if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) { final Long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0); try { downloadCompleted(downloadId); } catch (BrandingFailureException e) { L.e(e); } return null; } else { if (mInitialized) initStorageSettings(); return new String[] { intent.getAction() }; } } }; // Not using DownloadManager under api lvl 11 @SuppressLint("InlinedApi") private File getDownloadedFile(final Long downloadId) throws DownloadNotCompletedException { final DownloadManager dwnlMgr = getDownloadManager(); final Cursor cursor = dwnlMgr.query(new Query().setFilterById(downloadId)); try { if (!cursor.moveToFirst()) { L.w("Download with id " + downloadId + " not found!"); return null; } final int status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)); switch (status) { case DownloadManager.STATUS_SUCCESSFUL: final String filePath = cursor .getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME)); return new File(filePath); case DownloadManager.STATUS_FAILED: return null; default: // Not completed L.w("Unexpected DownloadManager.STATUS: " + status); throw new BrandingMgr.DownloadNotCompletedException(); } } finally { cursor.close(); } } private void downloadCompleted(final Long downloadId) throws BrandingFailureException { final File downloadedFile; try { downloadedFile = getDownloadedFile(downloadId); } catch (DownloadNotCompletedException e) { return; } try { final BrandedItem item; synchronized (mLock) { item = mDownloadMgrQueue.get(downloadId); } if (item == null) { return; } boolean success; if (downloadedFile == null) { success = false; } else { try { storeDownloadedBranding(downloadedFile, item); success = true; } catch (Exception e) { success = false; } } dequeue(item, !success); } finally { getDownloadManager().remove(downloadId); } } private void storeDownloadedBranding(File tmpFile, BrandedItem item) throws BrandingFailureException, IOException { final File dstFile; if (item.type == BrandedItem.TYPE_JS_EMBEDDING_PACKET) { final JSEmbeddingItemTO packet = (JSEmbeddingItemTO) item.object; dstFile = getJSEmbeddingPacketFile(packet.name); } else if (item.type == BrandedItem.TYPE_LOCAL_FLOW_ATTACHMENT) { final StartFlowRequest flow = (StartFlowRequest) item.object; dstFile = getAttachmentFile(flow.thread_key, attachmentDownloadUrlHash(item.brandingKey)); } else if (item.type == BrandedItem.TYPE_ATTACHMENT) { final AttachmentDownload attachment = (AttachmentDownload) item.object; dstFile = getAttachmentFile(attachment); } else { dstFile = getBrandingFile(item.brandingKey); } if (!tmpFile.renameTo(dstFile)) { IOUtils.copyFile(tmpFile, dstFile); if (!item.usesDownloadManager()) { tmpFile.delete(); } } } private final SafeRunnable mQueueProcessor = new SafeRunnable() { private void copyAndVerify(BrandedItem item, InputStream input, FileOutputStream output) throws IOException, BrandingFailureException { T.dontCare(); String brandingKey = item.brandingKey; MessageDigest digester; try { digester = MessageDigest.getInstance("SHA256"); } catch (NoSuchAlgorithmException e) { throw new BrandingFailureException("Cannot validate SHA256", e); } DigestInputStream dis = new DigestInputStream(input, digester); try { if (item.type == BrandedItem.TYPE_JS_EMBEDDING_PACKET || item.type == BrandedItem.TYPE_LOCAL_FLOW_ATTACHMENT || item.type == BrandedItem.TYPE_ATTACHMENT) { IOUtils.copy(dis, output, BUFFER_SIZE); } else { try { Security.encryptAES(getEncryptionKey(), ENCRYPTION_IV, dis, output); } catch (Exception e) { throw new BrandingFailureException("Failed to encrypt branding " + brandingKey, e); } } } finally { dis.close(); } if (item.type == BrandedItem.TYPE_LOCAL_FLOW_ATTACHMENT || item.type == BrandedItem.TYPE_ATTACHMENT) { } else { String hexDigest = com.mobicage.rogerthat.util.TextUtils.toHex(digester.digest()); if (!brandingKey.equals(hexDigest)) throw new BrandingFailureException( "SHA256 digest could not be validated against branding key\nExpected " + brandingKey + "\nGot " + hexDigest); } } private void download(BrandedItem item) { T.dontCare(); String url; if (item.type == BrandedItem.TYPE_JS_EMBEDDING_PACKET) { JSEmbeddingItemTO packet = (JSEmbeddingItemTO) item.object; url = CloudConstants.JS_EMBEDDING_URL_PREFIX + packet.name; } else if (item.type == BrandedItem.TYPE_LOCAL_FLOW_ATTACHMENT || item.type == BrandedItem.TYPE_ATTACHMENT) { url = item.brandingKey; } else { url = CloudConstants.BRANDING_URL_PREFIX + item.brandingKey; } L.d("Downloading branding: " + url); boolean success; try { final HttpClient httpClient = HTTPUtil.getHttpClient(60000, 0); // allow redirects HttpClientParams.setRedirecting(httpClient.getParams(), true); final HttpGet httpGet = new HttpGet(url); if (item.type == BrandedItem.TYPE_JS_EMBEDDING_PACKET) { Credentials credentials = mMainService.getCredentials(); if (credentials != null) { httpGet.addHeader("X-MCTracker-User", Base64.encodeBytes(credentials.getUsername().getBytes(), Base64.DONT_BREAK_LINES)); httpGet.addHeader("X-MCTracker-Pass", Base64.encodeBytes(credentials.getPassword().getBytes(), Base64.DONT_BREAK_LINES)); } else { L.bug("Failed to download JS Embedding packet, Credentials were NULL"); throw new Exception("Failed to download JS Embedding packet, Credentials were NULL"); } } final HttpResponse response = httpClient.execute(httpGet); final int statusCode = response.getStatusLine().getStatusCode(); final InputStream stream = response.getEntity().getContent(); if (statusCode != HttpStatus.SC_OK) { // We need to consume the whole entity byte data[] = new byte[BUFFER_SIZE]; while (stream.read(data) != -1) ; throw new Exception("Received unexpected statusCode: " + statusCode); } try { final File tmpFile = new File(getBrandingRootDirectory(), ".tmp_download_file"); if (tmpFile.exists()) { if (!tmpFile.delete()) throw new BrandingFailureException("Could not cleanup tmp download file"); } FileOutputStream fos = new FileOutputStream(tmpFile); try { copyAndVerify(item, stream, fos); } finally { fos.close(); } storeDownloadedBranding(tmpFile, item); } finally { stream.close(); } success = true; } catch (Exception e) { String errorMessage = "Failed to download branding file " + item.brandingKey; if (item.type == BrandedItem.TYPE_MESSAGE) { MessageTO msg = (MessageTO) item.object; errorMessage += " for message: " + msg.key; } else if (item.type == BrandedItem.TYPE_FRIEND) { FriendTO friend = (FriendTO) item.object; // Caution: this Friend object only has an email property populated errorMessage += " for friend: " + friend.email; } else if (item.type == BrandedItem.TYPE_JS_EMBEDDING_PACKET) { JSEmbeddingItemTO packet = (JSEmbeddingItemTO) item.object; errorMessage += " for JSEmbedding: " + packet.name; } else if (item.type == BrandedItem.TYPE_LOCAL_FLOW_ATTACHMENT || item.type == BrandedItem.TYPE_LOCAL_FLOW_BRANDING) { StartFlowRequest flow = (StartFlowRequest) item.object; errorMessage += " for service '" + flow.service + "' and start flow hash: " + flow.static_flow_hash; } else if (item.type == BrandedItem.TYPE_ATTACHMENT) { AttachmentDownload attachment = (AttachmentDownload) item.object; errorMessage += " for attachment " + attachment.download_url + " in message " + attachment.messageKey; } if (e instanceof IOException) { L.e(errorMessage, e); } else { L.bug(errorMessage, e); } success = false; } dequeue(item, !success); return; } /** * Get first brandedItem with status=STATUS_TODO */ private BrandedItem getNextBrandedItemToDownload() { synchronized (mLock) { for (BrandedItem item : mQueue) { if (item.status == BrandedItem.STATUS_TODO) { return item; } } return null; } } private boolean shouldDequeueItem(BrandedItem item) { try { return item.brandingKey == null || isBrandingAvailable(item.brandingKey) || item.status != BrandedItem.STATUS_TODO; } catch (BrandingFailureException e) { L.e(e); return true; } } @Override protected void safeRun() throws Exception { T.dontCare(); while (mQueue.size() > 0 && mExternalStorageWriteable) { BrandedItem item = getNextBrandedItemToDownload(); if (item == null) break; if (shouldDequeueItem(item)) { dequeue(item, false); continue; } if (!mExternalStorageWriteable) break; download(item); } } }; private File getOldBrandingFile(String branding) throws BrandingFailureException { File dir = getBrandingRootDirectory(); return new File(dir, branding); } private File getBrandingFile(String branding) throws BrandingFailureException { T.dontCare(); File dir = getBrandingRootDirectory(); return new File(dir, branding + ".branding"); } private File getBrandingRootDirectory() throws BrandingFailureException { T.dontCare(); File file = IOUtils.getFilesDirectory(mMainService); createDirIfNotExists(file); file = new File(file, "brandings"); createDirIfNotExists(file); return file; } private File getJSEmbeddingPacketFile(String packet) throws BrandingFailureException { T.dontCare(); File dir = getJSEmbeddingRootDirectory(); return new File(dir, packet + ".tmp"); } private File getJSEmbeddingUnpackDirectory(File brandingDir, String packet) throws BrandingFailureException { File file = new File(brandingDir, packet); createDirIfNotExists(file); return file; } private File getJSEmbeddingPacketDirectory(String packet) throws BrandingFailureException { T.dontCare(); File dir = getJSEmbeddingRootDirectory(); File file = new File(dir, packet); createDirIfNotExists(file); return file; } private File getJSEmbeddingRootDirectory() throws BrandingFailureException { T.dontCare(); File file = new File(mMainService.getFilesDir(), "javascript"); createDirIfNotExists(file); return file; } private File getAttachmentsRootDirectory() throws BrandingFailureException { T.dontCare(); File file = IOUtils.getFilesDirectory(mMainService); createDirIfNotExists(file); file = new File(file, "attachments"); createDirIfNotExists(file); return file; } private File getAttachmentsThreadDirectory(String threadKey) throws BrandingFailureException { T.dontCare(); File dir = getAttachmentsRootDirectory(); File file = new File(dir, threadKey); createDirIfNotExists(file); return file; } private File getAttachmentsDirectory(String threadKey, String messageKey) throws BrandingFailureException { T.dontCare(); File dir = getAttachmentsThreadDirectory(threadKey); File file = new File(dir, messageKey); createDirIfNotExists(file); return file; } private String attachmentDownloadUrlHash(String downloadUrl) { return Security.sha256(downloadUrl); } public File getAttachmentFile(AttachmentDownload attachment) throws BrandingFailureException { T.dontCare(); return getAttachmentFile(attachment.threadKey, attachment.messageKey, attachmentDownloadUrlHash(attachment.download_url)); } private File getAttachmentFile(String threadKey, String messageKey, String attachmentUrlHash) throws BrandingFailureException { T.dontCare(); File dir = getAttachmentsDirectory(threadKey, messageKey); return new File(dir, attachmentUrlHash); } private File getAttachmentFile(String threadKey, String attachmentUrlHash) throws BrandingFailureException { T.dontCare(); File dir = getAttachmentsThreadDirectory(threadKey); return new File(dir, attachmentUrlHash); } private File getLocalFlowContentFile(String threadKey) throws BrandingFailureException { T.dontCare(); return new File(getAttachmentsThreadDirectory(threadKey), ".content"); } private void createDirIfNotExists(File file) throws BrandingFailureException { T.dontCare(); if (!file.exists()) { if (!file.mkdir()) throw new BrandingFailureException( mContext.getString(R.string.failed_to_create_directory, file.getAbsolutePath())); } } protected void save() { T.dontCare(); synchronized (mLock) { String serializedMgr; try { serializedMgr = Base64.encodeBytes(Pickler.getPickleFromObject(this)); } catch (PickleException e) { L.bug(e); return; } Configuration cfg = new Configuration(); cfg.put(CONFIG_QUEUE, serializedMgr); mCfgProvider.updateConfigurationNow(CONFIGKEY, cfg); } } private void initStorageSettings() { T.UI(); if (IOUtils.shouldCheckExternalStorageAvailable()) { String state = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(state)) { mExternalStorageAvailable = mExternalStorageWriteable = true; if (mQueue.size() > 0) mDownloaderHandler.post(mQueueProcessor); } else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { mExternalStorageAvailable = true; mExternalStorageWriteable = false; if (mQueue.size() > 0) mDownloaderHandler.post(mQueueProcessor); } else { mExternalStorageAvailable = mExternalStorageWriteable = false; } } else { mExternalStorageAvailable = mExternalStorageWriteable = true; if (mQueue.size() > 0) mDownloaderHandler.post(mQueueProcessor); } } @Override public void writePickle(DataOutput out) throws IOException { T.dontCare(); out.writeInt(mQueue.size()); for (BrandedItem item : mQueue) { out.writeUTF(JSONValue.toJSONString(item.toJSONMap())); } out.writeInt(mDownloadMgrQueue.size()); for (BrandedItem item : mDownloadMgrQueue.values()) { out.writeUTF(JSONValue.toJSONString(item.toJSONMap())); } } @Override public void readFromPickle(int version, DataInput in) throws IOException, PickleException { T.dontCare(); if (version < 4) { deserializeStashedLegacyManager(version, in); } else { // Read branding queue int queueSize = in.readInt(); for (int i = 0; i < queueSize; i++) { @SuppressWarnings("unchecked") Map<String, Object> jsonMap = (Map<String, Object>) JSONValue.parse(in.readUTF()); try { mQueue.add(new BrandedItem(jsonMap)); } catch (IncompleteMessageException e) { L.bug(e); } } if (version < 6) { readLegacyStashedMemberStatusUpdates(in); if (version >= 5) { readLegacyStashedLockRequests(in); } } if (version >= 7) { int downloadQueueSize = in.readInt(); for (int i = 0; i < downloadQueueSize; i++) { @SuppressWarnings("unchecked") Map<String, Object> jsonMap = (Map<String, Object>) JSONValue.parse(in.readUTF()); try { final BrandedItem item = new BrandedItem(jsonMap); mDownloadMgrQueue.put(item.downloadId, item); } catch (IncompleteMessageException e) { L.bug(e); } } } } } @SuppressWarnings("unchecked") private void readLegacyStashedMemberStatusUpdates(DataInput in) throws IOException, PickleException { int size = in.readInt(); for (int i = 0; i < size; i++) { String key = in.readUTF(); BrandedItem item = getMessageFromQueue(key); int updatesCount = in.readInt(); for (int j = 0; j < updatesCount; j++) { item.calls.add(createRpcCall("com.mobicage.capi.messaging.updateMessageMemberStatus", (Map<String, Object>) JSONValue.parse(in.readUTF()))); } } } @SuppressWarnings("unchecked") private void readLegacyStashedLockRequests(DataInput in) throws IOException, PickleException { int size = in.readInt(); for (int i = 0; i < size; i++) { String key = in.readUTF(); BrandedItem item = getMessageFromQueue(key); item.calls.add(createRpcCall("com.mobicage.capi.messaging.messageLocked", (Map<String, Object>) JSONValue.parse(in.readUTF()))); } } @SuppressWarnings("unchecked") private void deserializeStashedLegacyManager(int version, DataInput in) throws IOException, PickleException { int msgQueueSize = in.readInt(); for (int i = 0; i < msgQueueSize; i++) { try { Message message = new Message((Map<String, Object>) JSONValue.parse(in.readUTF())); mQueue.add(new BrandedItem(message)); } catch (IncompleteMessageException e) { L.bug(e); } } if (version == 3) { readLegacyStashedMemberStatusUpdates(in); } if (version >= 2) { int friendQueueSize = in.readInt(); for (int i = 0; i < friendQueueSize; i++) { try { FriendTO friend = new FriendTO((Map<String, Object>) JSONValue.parse(in.readUTF())); mQueue.add(new BrandedItem(BrandedItem.TYPE_FRIEND, friend, friend.descriptionBranding)); } catch (IncompleteMessageException e) { L.bug(e); } } } } @Override public int getPickleClassVersion() { return 7; } public static int calculateHeight(BrandingResult br, int width) { return calculateHeight(br.dimension1, br.dimension2, width); } public static int calculateHeight(Dimension dimension1, Dimension dimension2, int width) { if (dimension1 == null || dimension2 == null) { return 0; } int w0 = dimension1.width; int h0 = dimension1.height; int w1 = dimension2.width; int h1 = dimension2.height; if (w1 == w0) { // prevent division by zero return 0; } int height = h0 + (h1 - h0) * (width - w0) / (w1 - w0); L.d("Calculated branding height: " + height); return height; } }