/* * 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.objects; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import mobisocial.crypto.IBHashedIdentity; import mobisocial.crypto.IBHashedIdentity.Authority; import mobisocial.musubi.App; import mobisocial.musubi.MembersActivity; 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.MFeed; import mobisocial.musubi.model.MIdentity; import mobisocial.musubi.model.MObject; import mobisocial.musubi.model.helpers.FeedManager; import mobisocial.musubi.model.helpers.IdentitiesManager; import mobisocial.musubi.model.helpers.ObjectManager; import mobisocial.musubi.provider.MusubiContentProvider; import mobisocial.musubi.provider.MusubiContentProvider.Provided; import mobisocial.musubi.service.MusubiService; import mobisocial.musubi.ui.fragments.FeedListFragment.FeedSummary; import mobisocial.musubi.ui.util.UiUtil; import mobisocial.musubi.ui.widget.DbObjCursorAdapter.DbObjCursor; import mobisocial.musubi.util.Util; import mobisocial.socialkit.Obj; import mobisocial.socialkit.musubi.DbObj; import mobisocial.socialkit.obj.MemObj; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.database.MatrixCursor; import android.database.sqlite.SQLiteOpenHelper; import android.graphics.Bitmap; import android.graphics.Typeface; import android.net.Uri; import android.support.v4.widget.CursorAdapter; import android.util.Base64; import android.util.DisplayMetrics; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.MarginLayoutParams; import android.widget.AbsListView.LayoutParams; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.Gallery; import android.widget.ImageView; import android.widget.ImageView.ScaleType; import android.widget.LinearLayout; import android.widget.TextView; /** * An object that provides minimal information about introduced participants to a group. * This ensures the minimum latency in a person being able to tell who is participating * in a feed. Other approaches, such as sending profile to people we discover (which we still do), * have one round-trip of latency. * */ public class IntroductionObj extends DbEntryHandler implements FeedRenderer, Activator { public static final String TYPE = "introduction"; public static final String IDENTITIES = "identities"; public static final String ID_AUTHORITY = "authority"; public static final String ID_PRINCIPAL = "principal"; public static final String ID_PRINCIPAL_HASH = "hash"; public static final String ID_NAME = "name"; @Override public String getType() { return TYPE; } public static MemObj from(Collection<MIdentity> identities, boolean tellPrincipals) { return new MemObj(TYPE, json(identities, tellPrincipals)); } static JSONObject json(Collection<MIdentity> identities, boolean tellPrincipals){ JSONArray array = new JSONArray(); JSONObject obj = new JSONObject(); try{ for (MIdentity id : identities) { JSONObject identity = new JSONObject(); identity.put(ID_AUTHORITY, id.type_.ordinal()); identity.put(ID_PRINCIPAL_HASH, Base64.encodeToString(id.principalHash_, Base64.DEFAULT)); identity.put(ID_PRINCIPAL, id.principal_); if(tellPrincipals) identity.put(ID_PRINCIPAL, id.principal_); identity.put(ID_NAME, UiUtil.safeNameForIdentity(id)); array.put(identity); } obj.put(IDENTITIES, array); }catch(JSONException e){} return obj; } @Override public View createView(Context context, ViewGroup frame) { LinearLayout wrap = new LinearLayout(context); wrap.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT)); wrap.setOrientation(LinearLayout.VERTICAL); wrap.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); wrap.setEnabled(false); wrap.setFocusableInTouchMode(false); wrap.setFocusable(false); wrap.setClickable(false); TextView title = new TextView(context); title.setText(R.string.introduced); title.setTypeface(null, Typeface.BOLD); title.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); wrap.addView(title); Gallery intro = new Gallery(context); intro.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); hackGalleryInit(context, intro); wrap.addView(intro); return wrap; } @Override public void render(Context context, final View frame, DbObjCursor obj, final boolean allowInteractions) { Gallery intro = (Gallery)((ViewGroup)frame).getChildAt(1); // TODO: LoaderManager requires access to a SupportActivity. intro.setAdapter(FacesAdapter.forObj(context, obj)); intro.setSpacing(1); intro.setOnItemClickListener(mIdentityClickListener); } void hackGalleryInit(Context context, Gallery gallery) { DisplayMetrics metrics = context.getResources().getDisplayMetrics(); MarginLayoutParams mlp = (MarginLayoutParams) gallery.getLayoutParams(); mlp.setMargins(-(metrics.widthPixels/2), mlp.topMargin, mlp.rightMargin, mlp.bottomMargin ); } @Override public boolean processObject(Context context, MFeed feed, MIdentity sender, MObject object) { boolean anyChanged = false; SQLiteOpenHelper databaseSource = App.getDatabaseSource(context); IdentitiesManager identitiesManager = new IdentitiesManager(databaseSource); if (object.json_ == null) { Log.w(TAG, "bad introduction format"); return false; } JSONObject json; try { json = new JSONObject(object.json_); } catch (JSONException e) { Log.e(TAG, "Bad json in database", e); return false; } JSONArray array; try { array = json.getJSONArray(IDENTITIES); } catch (JSONException e) { Log.e(TAG, "json identity array missing", e); return false; } // TODO: use getIdentitiesForObj for(int i = 0; i < array.length(); ++i) { JSONObject identity; try { identity = array.getJSONObject(i); } catch (JSONException e) { Log.e(TAG, "identity entry in introduction access error", e); continue; } int authority = -1; String principalHashString = null; try { authority = identity.getInt(ID_AUTHORITY); principalHashString = identity.getString(ID_PRINCIPAL_HASH); } catch (JSONException e) { Log.e(TAG, "identity entry in introduction missing key fields", e); continue; } String principal = null; try { principal = identity.getString(ID_PRINCIPAL); } catch (JSONException e) { } String name = null; try { name = identity.getString(ID_NAME); } catch (JSONException e) { } if(name == null && principal == null) { //not much of an introduction continue; } byte[] principalHash = Base64.decode(principalHashString, Base64.DEFAULT); IBHashedIdentity hid = new IBHashedIdentity(Authority.values()[authority], principalHash, 0); MIdentity ident = identitiesManager.getIdentityForIBHashedIdentity(hid); if(ident == null) { //this introduction has to be sent to both participants, so the low leve //will already have added the identity Log.e(TAG, "identity introduction for totally unseen identities"); continue; } if(ident.owned_) { //we won't have a received profile version, so owned keeps us from self updating continue; } //TODO: rely on deferred handling for gray list participants //TODO: check that the person is actually in the feed boolean changed = false; if(principal != null && ident.principal_ == null) { if(!Arrays.equals(Util.sha256(principal.getBytes()), principalHash)) { Log.e(TAG, "received mismatched principal and principal hash"); continue; } changed = true; ident.principal_ = principal; } if(name != null && ident.receivedProfileVersion_ == 0) { changed = true; //each time someone introduces us, we'll just accept the new name //as long as we never got a real profile. ident.musubiName_ = name; } if(changed) { identitiesManager.updateIdentity(ident); anyChanged = true; } } if(anyChanged) { context.getContentResolver().notifyChange(MusubiService.PRIMARY_CONTENT_CHANGED, null); } return true; } @Override public void activate(Context context, DbObj obj) { SQLiteOpenHelper databaseSource = App.getDatabaseSource(context); ObjectManager objectManger = new ObjectManager(databaseSource); FeedManager feedManager = new FeedManager(databaseSource); MObject object = objectManger.getObjectForId(obj.getLocalId()); if (!(context instanceof Activity)) { return; } if(object == null) { return; } MFeed feed = feedManager.lookupFeed(object.feedId_); if(feed == null) { return; } Intent members = new Intent(context, MembersActivity.class); members.putExtra(MembersActivity.INTENT_EXTRA_FEED_URI, MusubiContentProvider.uriForItem(Provided.FEEDS, feed.id_)); context.startActivity(members); } static class FacesAdapter extends CursorAdapter { static IdentitiesManager sIdentitiesManager; // TODO: No access to LoaderManager. private FacesAdapter(Context context, Cursor c) { super(context, c); } public static CursorAdapter forObj(Context context, Obj obj) { MatrixCursor recruits = new MatrixCursor(new String[] {MIdentity.COL_ID}); sIdentitiesManager = new IdentitiesManager(App.getDatabaseSource(context)); List<IBHashedIdentity> ids = getIdentitiesForObj(obj); for (IBHashedIdentity id : ids) { MIdentity ident = sIdentitiesManager.getIdentityForIBHashedIdentity(id); if (ident != null) { recruits.addRow(new Object[] { ident.id_ }); } } return new FacesAdapter(context, recruits); } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { ImageView face = new ImageView(context); int size = (int)context.getResources().getDisplayMetrics().density * 80; Gallery.LayoutParams spec = new Gallery.LayoutParams(size, size); face.setLayoutParams(spec); face.setScaleType(ScaleType.FIT_XY); face.setPadding(6, 6, 6, 6); face.setBackgroundResource(android.R.drawable.picture_frame); return face; } @Override public void bindView(View view, Context context, Cursor cursor) { long id = cursor.getLong(0); view.setTag(id); MIdentity stub = new MIdentity(); stub.id_ = id; Bitmap bm = UiUtil.safeGetContactThumbnail(context, sIdentitiesManager, stub); if (bm != null) { ((ImageView)view).setImageBitmap(bm); } else { Log.w(TAG, "safe thumbnail lookup not safe"); } } } /** * Extracts the list of identities from an introductionObj. */ static List<IBHashedIdentity> getIdentitiesForObj(Obj obj) { ArrayList<IBHashedIdentity> ids = new ArrayList<IBHashedIdentity>(); JSONObject json = obj.getJson(); if (json == null) { return ids; } JSONArray array; try { array = json.getJSONArray(IDENTITIES); } catch (JSONException e) { return ids; } for(int i = 0; i < array.length(); ++i) { JSONObject identity; try { identity = array.getJSONObject(i); } catch (JSONException e) { Log.e(TAG, "identity entry in introduction access error", e); continue; } int authority = -1; String principalHashString = null; try { authority = identity.getInt(ID_AUTHORITY); principalHashString = identity.getString(ID_PRINCIPAL_HASH); } catch (JSONException e) { Log.e(TAG, "identity entry in introduction missing key fields", e); continue; } String principal = null; try { principal = identity.getString(ID_PRINCIPAL); } catch (JSONException e) { } String name = null; try { name = identity.getString(ID_NAME); } catch (JSONException e) { } if(name == null && principal == null) { //not much of an introduction continue; } byte[] principalHash = Base64.decode(principalHashString, Base64.DEFAULT); ids.add(new IBHashedIdentity(Authority.values()[authority], principalHash, 0)); } return ids; } OnItemClickListener mIdentityClickListener = new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> av, View v, int pos, long id) { long identityId = (Long)v.getTag(); Uri identityUri = MusubiContentProvider.uriForItem(Provided.IDENTITIES_ID, identityId); String identityType = MusubiContentProvider.getType(Provided.IDENTITIES_ID); Intent view = new Intent(Intent.ACTION_VIEW); view.setDataAndType(identityUri, identityType); v.getContext().startActivity(view); } }; @Override public void getSummaryText(Context context, TextView view, FeedSummary summary) { view.setTypeface(null, Typeface.ITALIC); view.setText(summary.getSender() + " introduced new people to the feed."); } }