/* * Copyright 2012 The Stanford MobiSocial Laboratory * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package mobisocial.musubi.obj; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import mobisocial.crypto.IBHashedIdentity; import mobisocial.musubi.App; import mobisocial.musubi.Helpers; import mobisocial.musubi.R; import mobisocial.musubi.feed.iface.Activator; import mobisocial.musubi.feed.iface.DbEntryHandler; import mobisocial.musubi.feed.iface.FeedRenderer; import mobisocial.musubi.model.MApp; import mobisocial.musubi.model.MIdentity; import mobisocial.musubi.model.MObject; import mobisocial.musubi.model.helpers.AppManager; import mobisocial.musubi.model.helpers.DatabaseManager; import mobisocial.musubi.model.helpers.IdentitiesManager; import mobisocial.musubi.model.helpers.ObjectManager; import mobisocial.musubi.obj.iface.ObjAction; import mobisocial.musubi.objects.AppObj; import mobisocial.musubi.objects.AppStateObj; import mobisocial.musubi.objects.DeleteObj; import mobisocial.musubi.objects.FeedNameObj; import mobisocial.musubi.objects.FileObj; import mobisocial.musubi.objects.IntroductionObj; import mobisocial.musubi.objects.JoinRequestObj; import mobisocial.musubi.objects.LikeObj; import mobisocial.musubi.objects.LocationObj; import mobisocial.musubi.objects.MusicObj; import mobisocial.musubi.objects.MusubiWizardObj; import mobisocial.musubi.objects.OutOfBandInvitedObj; import mobisocial.musubi.objects.PhoneStateObj; import mobisocial.musubi.objects.PictureObj; import mobisocial.musubi.objects.ProfileObj; import mobisocial.musubi.objects.SharedSecretObj; import mobisocial.musubi.objects.StatusObj; import mobisocial.musubi.objects.StoryObj; import mobisocial.musubi.objects.UnknownObj; import mobisocial.musubi.objects.VideoObj; import mobisocial.musubi.objects.VoiceObj; import mobisocial.musubi.objects.WebAppObj; import mobisocial.musubi.provider.MusubiContentProvider; import mobisocial.musubi.provider.MusubiContentProvider.Provided; import mobisocial.musubi.ui.MusubiBaseActivity; import mobisocial.musubi.ui.ViewProfileActivity; import mobisocial.musubi.ui.fragments.FeedListFragment.FeedSummary; import mobisocial.musubi.ui.util.AddToWhitelistListener; import mobisocial.musubi.ui.util.EmojiSpannableFactory; import mobisocial.musubi.ui.util.UiUtil; import mobisocial.musubi.ui.widget.DbObjCursorAdapter; import mobisocial.musubi.ui.widget.DbObjCursorAdapter.DbObjCursor; import mobisocial.musubi.util.IdentityCache.CachedIdentity; import mobisocial.musubi.util.RelativeDate; import mobisocial.musubi.util.Util; import mobisocial.musubi.webapp.WebAppActivity; import mobisocial.socialkit.Obj; import mobisocial.socialkit.musubi.DbFeed; import mobisocial.socialkit.musubi.DbObj; import mobisocial.socialkit.musubi.Musubi; import mobisocial.socialkit.obj.MemObj; import org.json.JSONObject; import android.app.ActionBar.LayoutParams; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.database.sqlite.SQLiteOpenHelper; import android.graphics.Color; import android.graphics.Typeface; import android.net.Uri; import android.text.Spannable; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.TextView.BufferType; import android.widget.Toast; public final class ObjHelpers { public static final String TAG = "ObjHelpers"; private static OnClickViewProfile sViewProfileAction; private static final int sDeletedColor = Color.parseColor("#66FF3333"); private static final boolean showLikeCount = true; // Basic property names for all objects public static final String TYPE = "type"; public static final String FEED_NAME = "feedName"; public static final String SEQUENCE_ID = "sequenceId"; public static final String TIMESTAMP = "timestamp"; public static final String APP_ID = "appId"; // A ContentValue field used when a Musubi sends data on a 3rd party app's behalf // See AppObj.CLAIMED_APP_ID for json value public static final String CALLER_APP_ID = "callerAppId"; /** * Common types that don't need to be stored in the database, * but also don't require a {@link DbEntryHandler} class. */ public static final Set<String> sDiscardTypes = new HashSet<String>(); static { sDiscardTypes.add("locUpdate"); sDiscardTypes.add("userAttributes"); } /** * {@see DbRelation} */ public static final String TARGET_HASH = "target_hash"; public static final String TARGET_RELATION = "target_relation"; private static final List<DbEntryHandler> objs = new ArrayList<DbEntryHandler>(); private static UnknownObj mUnknownObjHandler = new UnknownObj(); static { objs.add(new AppObj()); objs.add(new AppStateObj()); objs.add(new StoryObj()); objs.add(new ProfileObj()); objs.add(new StatusObj()); objs.add(new IntroductionObj()); objs.add(new JoinRequestObj()); objs.add(new OutOfBandInvitedObj()); objs.add(new LocationObj()); objs.add(new PictureObj()); objs.add(new VideoObj()); objs.add(new VoiceObj()); objs.add(new PhoneStateObj()); objs.add(new MusicObj()); objs.add(new SharedSecretObj()) ; objs.add(new DeleteObj()); objs.add(new LikeObj()); objs.add(new MusubiWizardObj()); objs.add(new FeedNameObj()); objs.add(new FileObj()); objs.add(new WebAppObj()); } public static FeedRenderer getFeedRenderer(String type) { for (DbEntryHandler obj : objs) { if (obj instanceof FeedRenderer && obj.getType().equals(type)) { return (FeedRenderer)obj; } } return getGenericRenderer(); } private static FeedRenderer genericRenderer; static FeedRenderer getGenericRenderer() { if (genericRenderer == null) { genericRenderer = new FeedRenderer() { @Override public void render(Context context, View view, DbObjCursor obj, boolean allowInteractions) throws Exception { LinearLayout frame = (LinearLayout)view; frame.removeAllViews(); JSONObject json = obj.getJson(); if (json == null) { return; } if (json.has(Obj.FIELD_RENDER_TYPE)) { String type = json.getString(Obj.FIELD_RENDER_TYPE); if (Obj.RENDER_LATEST.equals(type)) { new AppObj().render(context, frame, obj, allowInteractions); } } else if (obj.getJson().has(Obj.FIELD_HTML)) { String html = obj.getJson().optString(Obj.FIELD_HTML); AppStateObj.renderHtml(context, frame, html); } } @Override public View createView(Context context, ViewGroup parent) { LinearLayout frame = new LinearLayout(context); return frame; } @Override public void getSummaryText(Context context, TextView view, FeedSummary summary) { view.setTypeface(null, Typeface.ITALIC); view.setText(summary.getSender() + " just did something."); } }; } return genericRenderer; } public static Activator getActivator(String type) { for (DbEntryHandler obj : objs) { if (obj instanceof Activator && obj.getType().equals(type)) { return (Activator)obj; } } return null; } public static String[] getRenderableTypes() { List<String> renderables = new ArrayList<String>(); for (DbEntryHandler o : objs) { if (o instanceof FeedRenderer){ renderables.add(o.getType()); } } return renderables.toArray(new String[renderables.size()]); } public static DbEntryHandler forType(String requestedType) { if (requestedType == null) { return null; } for (DbEntryHandler type : objs) { if (type.getType().equals(requestedType)) { return type; } } return mUnknownObjHandler; }; /** * {@see DbObject#RENDERABLE} */ @Deprecated public static String getFeedObjectClause(String[] types) { if(types == null) { types = ObjHelpers.getRenderableTypes(); } StringBuffer allowed = new StringBuffer(); for (String type : types) { allowed.append(",'").append(type).append("'"); } return MObject.COL_TYPE + " in (" + allowed.substring(1) + ")"; } public static class ItemClickListener implements View.OnClickListener { private static ItemClickListener sInstance; public static ItemClickListener getInstance() { if (sInstance == null) { sInstance = new ItemClickListener(); } return sInstance; } @Override public void onClick(View v) { Context context = v.getContext(); Musubi musubi = App.getMusubi(context); Object tag = v.getTag(); if (tag == null || !(tag instanceof Long)) { Log.d(TAG, "no id for dbobj " + v + "; parent: " + v.getParent()); return; } long objId = (Long)tag; DbObj obj = musubi.objForId(objId); if (MusubiBaseActivity.DBG) Log.i(TAG, "Clicked object " + obj.getType()); Activator activator = getActivator(obj.getType()); if (activator != null) { activator.activate(context, obj); } else { activateGeneric(context, obj); } } } public static class ItemLongClickListener implements View.OnLongClickListener { private final Context mContext; private Musubi mMusubi; private ItemLongClickListener(Context context) { mContext = context; mMusubi = App.getMusubi(context); } public static ItemLongClickListener getInstance(Context context) { return new ItemLongClickListener(context); } @Override public boolean onLongClick(View v) { if (v == null || v.getTag() == null) { //Log.d(TAG, "missing objId for " + v); return false; } long objId = (Long)v.getTag(); DbObj obj = mMusubi.objForId(objId); //maybe it was deleted and disappeared if(obj == null) return false; ObjHelpers.createActionDialog(mContext, obj).show(); return false; } } public static Dialog createActionDialog(final Context context, final DbObj obj) { final DbEntryHandler dbType = forType(obj.getType()); final List<ObjAction> actions = new ArrayList<ObjAction>(); for (ObjAction action : ObjActions.getObjActions()) { if (action.isActive(context, dbType, obj)) { actions.add(action); } } final String[] actionLabels = new String[actions.size()]; int i = 0; for (ObjAction action : actions) { actionLabels[i++] = action.getLabel(context); } return new AlertDialog.Builder(context) .setItems(actionLabels, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { actions.get(which).actOn(context, dbType, obj); } }).create(); } /** * A re-usable StringBuilder. */ static final StringBuilder sStringBuilder = new StringBuilder(50); /** * Binds the generic frame of a feed object. This method is not threadsafe. * * @param v the view to bind * @param context standard activity context * @param c the cursor source for the object in the db object table. * Must include _id in the projection. * * @param allowInteractions controls whether the bound view is * allowed to intercept touch events and do its own processing. */ public static void bindObjViewFrame(Context context, DatabaseManager db, ViewGroup frame, DbObjCursorAdapter.ViewHolder viewHolder, DbObjCursor objRow) { if (objRow == null) { Log.d(TAG, "Missing object!"); viewHolder.senderName.setText("Missing object!"); return; } frame.setTag(objRow.objId); final CachedIdentity sender = App.getContactCache(context).get(objRow.senderId); if (sender == null) { viewHolder.senderName.setText("Message from unknown contact."); Log.w(TAG, "unknown contact " + objRow.senderId); return; } Spannable span = EmojiSpannableFactory.getInstance(context).newSpannable(sender.name); viewHolder.senderName.setText(span, BufferType.SPANNABLE); //viewHolder.senderName.setText(sender.name); final ImageView icon = viewHolder.senderIcon; if (sViewProfileAction == null) { sViewProfileAction = new OnClickViewProfile((Activity)context); } if (!sender.midentity.whitelisted_ && !sender.midentity.owned_ && sender.midentity.type_ != IBHashedIdentity.Authority.Local) { viewHolder.addContact.setVisibility(View.VISIBLE); viewHolder.addContact.setOnClickListener(new AddToWhitelistListener(context, sender.midentity)); } else { viewHolder.addContact.setVisibility(View.GONE); } icon.setTag(sender.midentity.id_); icon.setOnClickListener(sViewProfileAction); icon.setImageBitmap(sender.thumbnail); if (objRow.deleted) { frame.setBackgroundColor(sDeletedColor); } else { frame.setBackgroundColor(Color.TRANSPARENT); } sStringBuilder.setLength(0); //check to see if we should print date or not sStringBuilder.append(RelativeDate.getRelativeDate(objRow.timestamp)); String appId = objRow.appId; if (appId != null && !MusubiContentProvider.SUPER_APP_ID.equals(appId)) { if(appId != null) { if (objRow.appName != null) { sStringBuilder.append(" via ").append(objRow.appName); } } } if (sStringBuilder.length() == 0) { viewHolder.timeText.setVisibility(View.GONE); } else { viewHolder.timeText.setVisibility(View.VISIBLE); viewHolder.timeText.setText(sStringBuilder); } frame.setTag(objRow.objId); try { if (!objRow.sent) { viewHolder.sendingIcon.setVisibility(View.VISIBLE); viewHolder.attachmentsIcon.setVisibility(View.GONE); viewHolder.attachmentsText.setVisibility(View.GONE); } else { viewHolder.sendingIcon.setVisibility(View.GONE); if (!showLikeCount) { viewHolder.attachmentsIcon.setVisibility(View.GONE); viewHolder.attachmentsText.setVisibility(View.GONE); } else { viewHolder.attachmentsIcon.setVisibility(View.VISIBLE); int likeCount = objRow.likeCount; sStringBuilder.setLength(0); sStringBuilder.append("+").append(likeCount); viewHolder.attachmentsText.setText(sStringBuilder); if (likeCount > 0) { viewHolder.attachmentsIcon.setImageResource(R.drawable.ic_menu_love_red); viewHolder.attachmentsText.setVisibility(View.VISIBLE); } else { viewHolder.attachmentsIcon.setImageResource(R.drawable.ic_menu_love); viewHolder.attachmentsText.setVisibility(View.INVISIBLE); } viewHolder.attachmentsIcon.setTag(objRow.objId); LikeListener ll = LikeListener.getInstance(); viewHolder.attachmentsIcon.setOnClickListener(ll); viewHolder.attachmentsText.setOnClickListener(ll); } } } catch (Throwable t) { Log.e(TAG, "failed to handle rendering of an obj", t); TextView tv = new TextView(context); tv.setText("Unable to render object: " + t.getLocalizedMessage()); //TODO: this should fill in something frame.removeAllViews(); frame.addView(tv, LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); } } public static boolean isRenderable(Obj obj) { if (forType(obj.getType()) instanceof FeedRenderer) { return true; } JSONObject json = obj.getJson(); if (json == null) { return false; } return json.has(Obj.FIELD_HTML) || json.has(Obj.FIELD_RENDER_TYPE); } private static void activateGeneric(Context context, DbObj obj) { String pkg = obj.getAppId(); if (MusubiContentProvider.isSuperApp(pkg)) { return; } Log.d(TAG, "Activating for app " + pkg); MApp app = new AppManager(App.getDatabaseSource(context)).lookupAppByAppId(obj.getAppId()); String mimeType = mimeTypeOf(obj.getType()); Intent view = new Intent(Intent.ACTION_VIEW); view.setDataAndType(obj.getUri(), mimeType); view.putExtra(Musubi.EXTRA_FEED_URI, obj.getContainingFeed().getUri()); if (app != null && app.webAppUrl_ != null) { view.setClass(context, WebAppActivity.class); view.putExtra(WebAppActivity.EXTRA_APP_URI, Uri.parse(app.webAppUrl_)); view.putExtra(WebAppActivity.EXTRA_APP_ID, pkg); } else if (!MusubiContentProvider.UNKNOWN_APP_ID.equals(pkg)) { // When Musubi shares data using the SEND intent, // the obj gets an unknown app id. view.setPackage(pkg); } try { context.startActivity(view); } catch (ActivityNotFoundException e) { Intent m = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + pkg)); try { context.startActivity(m); } catch (ActivityNotFoundException e2) { Toast.makeText(context, "Cannot view this object.", Toast.LENGTH_SHORT).show(); } } } public static class OnClickViewProfile implements View.OnClickListener { private final Activity mmContext; public OnClickViewProfile(Activity c) { mmContext = c; } @Override public void onClick(View v) { Log.e(TAG, "profile for " + v.getTag()); SQLiteOpenHelper SQLiteOpenHelper = App.getDatabaseSource(mmContext); IdentitiesManager identitiesManager = new IdentitiesManager(SQLiteOpenHelper); MIdentity person = identitiesManager.getIdentityForId(((Long)v.getTag()).longValue()); Log.w(TAG, UiUtil.safeNameForIdentity(person)); Intent intent = new Intent(mmContext, ViewProfileActivity.class); intent.putExtra(ViewProfileActivity.PROFILE_ID, ((Long)v.getTag()).longValue()); mmContext.startActivity(intent); } } private static class LikeListener implements View.OnClickListener { private static LikeListener sInstance; private LikeListener() { } public static LikeListener getInstance() { if (sInstance == null) { sInstance = new LikeListener(); } return sInstance; } @Override public void onClick(View v) { View p = (View)(v.getParent()); ((ImageView)p.findViewById(R.id.obj_attachments_icon)).setImageResource( R.drawable.ic_menu_love_red); Object objIdObj = v.getTag(); if (objIdObj == null) { Log.e(TAG, "Error liking object; no tag found"); return; } long objId = (Long)objIdObj; MObject obj = new ObjectManager(App.getDatabaseSource(v.getContext())).getObjectForId(objId); //unsent objects may be displayed, don't let liking them crash if(obj.universalHash_ == null) return; String hashString = Util.convertToHex(obj.universalHash_); long feedId = obj.feedId_; MemObj like = LikeObj.forObj(hashString); Helpers.sendToFeed(v.getContext(), like, DbFeed.uriForId(feedId)); } } /** * Checks a type against a list of internally known types that * should not be stored in the database. */ public static boolean isDiscardableObjType(String type) { return sDiscardTypes.contains(type); }; public static Uri uriForId(long id) { return MusubiContentProvider.uriForItem(Provided.OBJECTS, id); } /** * Returns the hex encoding of a byte array, which is the standard * string representation for an Obj's universal hash. */ public static String hashToString(byte[] data) { StringBuffer buf = new StringBuffer(); for (int i = 0; i < data.length; i++) { int halfbyte = (data[i] >>> 4) & 0x0F; int two_halfs = 0; do { if ((0 <= halfbyte) && (halfbyte <= 9)) { buf.append((char) ('0' + halfbyte)); } else { buf.append((char) ('a' + (halfbyte - 10))); } halfbyte = data[i] & 0x0F; } while (two_halfs++ < 1); } return buf.toString(); } /** * Converts the string representation of a hash to a byte array. */ public static byte[] stringToHash(String hashString) { if (hashString.length() % 2 != 0) { throw new IllegalArgumentException("Hash must have even length"); } int j = 0; byte[] hash = new byte[hashString.length() / 2]; for (int i = 0; i < hashString.length(); i += 2) { hash[j++] = Byte.parseByte(hashString.substring(i, i + 2)); } return hash; } public static final String mimeTypeOf(String objType) { return "vnd.musubi.obj/" + objType; } }