/* * 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.ui.fragments; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import mobisocial.metrics.MusubiMetrics; import mobisocial.musubi.App; import mobisocial.musubi.Helpers; import mobisocial.musubi.R; import mobisocial.musubi.feed.iface.FeedRenderer; import mobisocial.musubi.model.MFeed; import mobisocial.musubi.model.MFeedMember; import mobisocial.musubi.model.MIdentity; import mobisocial.musubi.model.MObject; import mobisocial.musubi.model.helpers.DatabaseManager; import mobisocial.musubi.model.helpers.EncodedMessageManager; import mobisocial.musubi.model.helpers.FeedManager; import mobisocial.musubi.model.helpers.IdentitiesManager; import mobisocial.musubi.model.helpers.MyAccountManager; import mobisocial.musubi.model.helpers.ObjectManager; import mobisocial.musubi.obj.ObjHelpers; import mobisocial.musubi.objects.IntroductionObj; import mobisocial.musubi.provider.MusubiContentProvider; import mobisocial.musubi.provider.MusubiContentProvider.Provided; import mobisocial.musubi.service.MusubiService; import mobisocial.musubi.ui.FeedDetailsActivity; import mobisocial.musubi.ui.FeedListActivity; import mobisocial.musubi.ui.MusubiBaseActivity; import mobisocial.musubi.ui.NearbyActivity; import mobisocial.musubi.ui.util.EmojiSpannableFactory; import mobisocial.musubi.ui.util.FeedHTML; import mobisocial.musubi.ui.util.UiUtil; import mobisocial.musubi.ui.util.UiUtil.PeopleDetails; import mobisocial.musubi.ui.widget.CompositeImageView; import mobisocial.musubi.ui.widget.MultiIdentitySelector; import mobisocial.musubi.ui.widget.MultiIdentitySelector.OnIdentitiesUpdatedListener; import mobisocial.musubi.ui.widget.MultiIdentitySelector.OnRequestAddIdentityListener; import mobisocial.musubi.util.IdentityCache; import mobisocial.musubi.util.IdentityCache.CachedIdentity; import mobisocial.musubi.util.RelativeDate; import mobisocial.socialkit.Obj; import org.json.JSONException; import org.json.JSONObject; import org.mobisocial.corral.ContentCorral; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.database.ContentObserver; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Typeface; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.provider.ContactsContract; import android.provider.ContactsContract.Intents.Insert; import android.support.v4.app.DialogFragment; import android.support.v4.app.ListFragment; import android.support.v4.app.LoaderManager; import android.support.v4.app.SupportActivity; import android.support.v4.content.AsyncTaskLoader; import android.support.v4.content.Loader; import android.support.v4.util.LruCache; import android.support.v4.view.Menu; import android.support.v4.view.MenuItem; import android.text.Editable; import android.text.Spannable; import android.text.TextWatcher; import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.android.contacts.widget.SingleTopPinnedHeaderListAdapter; /** * Displays a list of all user-accessible threads (feeds). * */ public class FeedListFragment extends ListFragment implements LoaderManager.LoaderCallbacks<Cursor> { private static final String TAG = "FeedListFragment"; private static final boolean DBG = MusubiBaseActivity.DBG; private static final int REQUEST_ADD_CONTACT = 1; private FeedListAdapter mFeeds; public static final String ARG_IDENTITY_ID = "identity_id"; public static final String DUAL_PANE = "dual_pane"; private OnFeedSelectedListener mFeedSelectedListener; private MultiIdentitySelector mPeople; private SQLiteOpenHelper mDatabaseSource; private ContentObserver mObserver; private Activity mActivity; public static final int DAYS_TO_SHOW = 7; public static int ONE_DAY = 1000*60*60*24; static final String sFeedSortOrder = MFeed.COL_LATEST_RENDERABLE_OBJ_TIME + " desc"; public FeedListFragment() { if (DBG) Log.d(TAG, "Instantiating new FeedListFragment"); } public interface OnFeedSelectedListener { public void onFeedSelected(Uri feedUri); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (DBG) Log.d(TAG, "Creating new FeedListFragment"); mFeeds = new FeedListAdapter(mActivity); for (int i = 0; i < DAYS_TO_SHOW+1; i++) { mFeeds.addPartition(false, true); } setListAdapter(mFeeds); mObserver = new ContentObserver(new Handler(mActivity.getMainLooper())) { @Override public void onChange(boolean arg0) { if(mFeeds.isEmpty() || !isAdded()) { return; } initLoaders(true); } }; } @Override public void onAttach(SupportActivity activity) { super.onAttach(activity); mActivity = activity.asActivity(); if (DBG) Log.d(TAG, "Attaching FeedListFragment."); mFeedSelectedListener = (OnFeedSelectedListener) activity; mDatabaseSource = App.getDatabaseSource(mActivity); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_feed_list, container, false); Bundle args = getArguments(); if(args != null && args.containsKey("no_nearby") && args.getBoolean("no_nearby")) v.findViewById(R.id.nearby).setVisibility(View.GONE); return v; } @Override public void onPause() { super.onPause(); mActivity.getContentResolver().unregisterContentObserver(mObserver); } @Override public void onResume() { super.onResume(); IdentitiesManager identitiesManager = new IdentitiesManager(mDatabaseSource); if (!identitiesManager.hasConnectedAccounts()) { mActivity.findViewById(R.id.go).setOnClickListener(mNoAccountsListener); mActivity.findViewById(R.id.people).setOnClickListener(mNoAccountsListener); mActivity.findViewById(R.id.nearby).setOnClickListener(mNoAccountsListener); } else { mActivity.findViewById(R.id.go).setOnClickListener(mStartListener); mActivity.findViewById(R.id.people).setOnClickListener(null); mActivity.findViewById(R.id.nearby).setOnClickListener(mJoinNearbyListener); } /*if (!MusubiBaseActivity.isDeveloperModeEnabled(mActivity)) { getActivity().findViewById(R.id.nearby).setVisibility(View.GONE); }*/ mActivity.getContentResolver().registerContentObserver(MusubiService.PRIMARY_CONTENT_CHANGED, false, mObserver); mObserver.dispatchChange(false); } public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); if (null != mActivity.findViewById(R.id.feed_view)) { getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE); } ListView lv = (ListView)getView().findViewById(android.R.id.list); registerForContextMenu(lv); /** Prepare the autocompleting dropdown **/ // TODO: background mPeople = (MultiIdentitySelector) mActivity.findViewById(R.id.people); mPeople.setOnRequestAddIdentityListener(mOnRequestAddIdentityListener); mPeople.setOnIdentitiesUpdatedListener(mIdentitiesUpdatedListener); mPeople.addTextChangedListener(new TextWatcher() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void afterTextChanged(Editable s) { if (mPeople.getSelectedIdentities().size() == 0) { initLoaders(true); } else { // defer to OnIdentitiesUpdatedListener } } }); mActivity.findViewById(R.id.go).setOnClickListener(mStartListener); /** Load the latest feeds in the background **/ initLoaders(false); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if(requestCode == REQUEST_ADD_CONTACT) { if(resultCode == Activity.RESULT_OK) { UiUtil.addedContact(mActivity, data, mPeople); } } } private OnRequestAddIdentityListener mOnRequestAddIdentityListener = new OnRequestAddIdentityListener() { @Override public void onRequestAddIdentity(String enteredText) { Intent i = new Intent(Intent.ACTION_INSERT_OR_EDIT); i.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE); if(enteredText != null) { Pattern emailPattern = Pattern.compile("\\b[A-Z0-9._%-]+@[A-Z0-9.-]+\\.[A-Z]{2,4}\\b", Pattern.CASE_INSENSITIVE); if (emailPattern.matcher(enteredText).matches()) { i.putExtra(Insert.EMAIL, enteredText); } else { i.putExtra(Insert.NAME, enteredText); } } startActivityForResult(i, REQUEST_ADD_CONTACT); } }; private OnIdentitiesUpdatedListener mIdentitiesUpdatedListener = new OnIdentitiesUpdatedListener() { @Override public void onIdentitiesUpdated() { initLoaders(true); } }; static class ViewHolder { PeopleDetails peopleDetails; FeedSummary feedSummary; CompositeImageView icon; TextView feedLabel; TextView time; TextView text; TextView unreadCount; } public static class FeedListAdapter extends SingleTopPinnedHeaderListAdapter { private final DatabaseManager mmDatabaseManager; private final Context mmContext; private final LayoutInflater mmLayoutInflater; private final IdentityCache mmIdentityCache; private final FeedIconCache mmFeedIconCache; private EmojiSpannableFactory mEmojiSpannableFactory; public FeedListAdapter (Context context) { super(context); setPinnedPartitionHeadersEnabled(true); mmContext = context; mEmojiSpannableFactory = EmojiSpannableFactory.getInstance(mmContext); mmLayoutInflater = LayoutInflater.from(context); SQLiteOpenHelper helper = App.getDatabaseSource(context); mmDatabaseManager = new DatabaseManager(helper); mmIdentityCache = App.getContactCache(context); mmFeedIconCache = new FeedIconCache(context, mmDatabaseManager, 30, 160); } @Override protected View newHeaderView(Context context, int partition, Cursor cursor, ViewGroup parent) { TextView tv = new TextView(context); tv.setPadding(6, 2, 2, 2); tv.setBackgroundColor(Color.rgb(101, 159, 229)); tv.setLayoutParams(new AbsListView.LayoutParams(AbsListView.LayoutParams.FILL_PARENT, AbsListView.LayoutParams.WRAP_CONTENT)); tv.setTextAppearance(context, android.R.style.TextAppearance_Medium_Inverse); return tv; } final Map<Integer, String> mHeaderLabels = new HashMap<Integer, String>(10); @Override protected void bindHeaderView(View view, int partition, Cursor cursor) { TextView tv = (TextView)view; String text; if (partition == 0) { text = "Today"; } else if (partition == 1) { text = "Yesterday"; } else if (partition < DAYS_TO_SHOW) { if (mHeaderLabels.containsKey(partition)) { text = mHeaderLabels.get(partition); } else { text = partition + " days ago"; mHeaderLabels.put(partition, text); } } else { text = "Older Conversations"; } tv.setText(text); } @Override public View newView(Context context, int partition, Cursor c, int position, ViewGroup parent) { View v = mmLayoutInflater.inflate(R.layout.feed_entry, parent, false); ViewHolder holder = new ViewHolder(); holder.peopleDetails = new PeopleDetails(); holder.feedSummary = new FeedSummary(mmContext); holder.icon = (CompositeImageView)v.findViewById(R.id.image); holder.feedLabel = ((TextView)v.findViewById(R.id.feed_label)); holder.time = ((TextView)v.findViewById(R.id.time_text)); holder.text = ((TextView)v.findViewById(R.id.text)); holder.unreadCount = (TextView)v.findViewById(R.id.unread_count); //holder.icon.setOnClickListener(mmOnIconClickListener); v.setTag(R.id.holder, holder); return v; } @Override public void bindView(final View v, int partition, final Cursor c, int position) { ViewHolder holder = (ViewHolder)v.getTag(R.id.holder); PeopleDetails details = holder.peopleDetails; FeedSummary feedSummary = holder.feedSummary; feedSummary.populate(c); String timeString = RelativeDate.getRelativeDate(feedSummary.timestamp); long[] identityIds = mmDatabaseManager.getFeedManager().getFeedMembers(feedSummary.feedId); UiUtil.populatePeopleDetails(mmContext, mmDatabaseManager.getIdentitiesManager(), identityIds, mmIdentityCache, details); if (feedSummary.feedName == null) feedSummary.feedName = details.name; List<Bitmap> images = null; if (feedSummary.hasThumbnail) { Bitmap bm = mmFeedIconCache.get(feedSummary.feedId); if (bm != null) { images = new ArrayList<Bitmap>(1); images.add(bm); } } if (images == null) { images = details.images; } if (images.size() == 0) { images.add(BitmapFactory.decodeResource(mmContext.getResources(), R.drawable.ic_contact_picture)); } // TODO: thumbnail view for all feed types v.setTag(feedSummary.feedId); holder.icon.setImageBitmaps(images); Spannable span = mEmojiSpannableFactory.newSpannable(feedSummary.feedName); holder.feedLabel.setText(span); holder.time.setText(timeString); /*if (holder.text.getText() != null) { mSpannableFactory.recycleSpans(holder.text.getText()); }*/ holder.text.setTypeface(null, Typeface.NORMAL); FeedRenderer renderer = ObjHelpers.getFeedRenderer(feedSummary.objType); renderer.getSummaryText(mmContext, holder.text, holder.feedSummary); if (feedSummary.numUnread == 0) { holder.unreadCount.setVisibility(View.GONE); } else { holder.unreadCount.setText("(" + feedSummary.numUnread + " new)"); holder.unreadCount.setVisibility(View.VISIBLE); } } View.OnClickListener mmOnIconClickListener = new View.OnClickListener() { @Override public void onClick(View v) { long feedId = (Long)((View)v.getParent()).getTag(); Intent i = new Intent(v.getContext(), FeedDetailsActivity.class); i.setData(MusubiContentProvider.uriForItem(Provided.FEEDS_ID, feedId)); v.getContext().startActivity(i); } }; } static class FeedIconCache extends LruCache<Long, Bitmap> { final int mImageSize; final DatabaseManager mDatabaseManager; final Matrix mScaleMatrix; public FeedIconCache(Context context, DatabaseManager databaseManager, int maxCount, int imageSize) { super(maxCount); mImageSize = imageSize; mDatabaseManager = databaseManager; mScaleMatrix = new Matrix(); } @Override protected Bitmap create(Long feedId) { byte[] thumbnailBytes = mDatabaseManager.getFeedManager().getFeedThumbnailForId(feedId); if (thumbnailBytes != null) { Bitmap bm = UiUtil.decodeSampledBitmapFromByteArray(thumbnailBytes, mImageSize, mImageSize); int bw = bm.getWidth(); int bh = bm.getHeight(); float dx = 0, dy = 0; if (bw > mImageSize || bh > mImageSize) { float scale; if (bw > bh) { scale = (float) mImageSize / (float) bw; dx = (mImageSize - bw * scale) * 0.5f; } else { scale = (float) mImageSize / (float) bh; dy = (mImageSize - bh * scale) * 0.5f; } mScaleMatrix.reset(); mScaleMatrix.setScale(scale, scale); mScaleMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f)); return Bitmap.createBitmap(bm, 0, 0, bw, bh, mScaleMatrix, true); } } return null; } } @Override public void onListItemClick(ListView l, View v, int position, long id) { Long feedId = (Long)v.getTag(); Uri feedUri = MusubiContentProvider.uriForItem(Provided.FEEDS, feedId); selectFeed(position, feedUri); mPeople.clearSelectedIdentities(); } class DeleteFeedAndContent extends AsyncTask<Void, Void, Void> { long mFeedId; private ProgressDialog mProgressDialog; private boolean mCanceled = false; public DeleteFeedAndContent(long feedId) { mFeedId = feedId; mProgressDialog = new ProgressDialog(mActivity); mProgressDialog.setTitle("Deleting Feed"); mProgressDialog.setMessage("Feed is being deleted. You will still receive new messages sent to the group."); mProgressDialog.setCancelable(true); mProgressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { mCanceled = true; } }); } @Override protected void onPreExecute() { mProgressDialog.show(); } @Override protected Void doInBackground(Void... params) { SQLiteDatabase db = mDatabaseSource.getWritableDatabase(); db.beginTransaction(); FeedManager feedManager = new FeedManager(mDatabaseSource); ObjectManager objectManager = new ObjectManager(mDatabaseSource); EncodedMessageManager encodedMessageManager = new EncodedMessageManager(mDatabaseSource); Cursor c = objectManager.getIdCursorForFeed(mFeedId); try { while(c.moveToNext() && !mCanceled) { long id = c.getLong(0); MObject object = objectManager.getObjectForId(id); if(object.encodedId_ != null) { encodedMessageManager.delete(object.encodedId_); objectManager.delete(id); } } } finally { c.close(); } MFeed feed = feedManager.lookupFeed(mFeedId); feedManager.deleteFeedAndMembers(feed); if(!mCanceled) db.setTransactionSuccessful(); db.endTransaction(); return null; } @Override protected void onPostExecute(Void result) { initLoaders(true); mProgressDialog.dismiss(); } } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { if (v.getId() == android.R.id.list) { menu.setHeaderTitle("Feed..."); menu.add(Menu.NONE, 0, 0, "Delete"); menu.add(Menu.NONE, 1, 0, "Send HTML"); } } @Override public boolean onContextItemSelected(MenuItem item) { AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo)item.getMenuInfo(); int menuItemIndex = item.getItemId(); Cursor cursor = (Cursor)mFeeds.getItem(info.position); switch(menuItemIndex) { case 0: //pass the feed id in handleDelete(cursor.getLong(0)); break; case 1: //pass the feed id in handleExport(cursor.getLong(0)); break; } return true; } public void handleDelete(final long feedId) { if(feedId == MFeed.WIZ_FEED_ID) return; new AlertDialog.Builder(mActivity) .setIcon(android.R.drawable.ic_dialog_alert) .setTitle(R.string.delete_feed) .setMessage(R.string.delete_feed_message) .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { FeedManager feedManager = new FeedManager(mDatabaseSource); MFeed feed = feedManager.lookupFeed(feedId); if(feed != null) new DeleteFeedAndContent(feedId).execute(); } }) .setNegativeButton(R.string.no, null) .show(); } public void handleExport(final long feedId) { FeedManager feedManager = new FeedManager(mDatabaseSource); MFeed feed = feedManager.lookupFeed(feedId); if(feed != null) new ExportFeedContent(feedId).execute(); } class ExportFeedContent extends AsyncTask<Void, Void, Void> { long mFeedId; private ProgressDialog mProgressDialog; private boolean mCanceled = false; private String mFilename; private String mName; public ExportFeedContent(long feedId) { mFeedId = feedId; mProgressDialog = new ProgressDialog(mActivity); mProgressDialog.setTitle("Send Feed as HTML"); mProgressDialog.setMessage("Converting..."); mProgressDialog.setCancelable(true); mProgressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { mCanceled = true; } }); } @Override protected void onPreExecute() { mProgressDialog.show(); } public String encodeFilename(String s) { return s.replaceAll("[^A-Za-z0-9]+", " "); } @Override protected Void doInBackground(Void... params) { FeedManager feedManager = new FeedManager(mDatabaseSource); IdentitiesManager identitiesManager = new IdentitiesManager(mDatabaseSource); MFeed feed = feedManager.lookupFeed(mFeedId); if(feed == null) return null; ObjectManager objectManager = new ObjectManager(mDatabaseSource); String selection = new StringBuilder(100).append(MObject.COL_RENDERABLE).append(" = 1 AND ") .append(MObject.COL_PARENT_ID).append(" is null AND ") .append(MObject.COL_FEED_ID).append(" =?").toString(); String[] selectionArgs = new String[] { Long.toString(mFeedId) }; Cursor c = getActivity().getContentResolver().query(MusubiContentProvider.uriForDir(Provided.OBJECTS), new String[] { MObject.COL_ID }, selection, selectionArgs, MObject.COL_LAST_MODIFIED_TIMESTAMP + " DESC"); try { File contentDir = new File(Environment.getExternalStorageDirectory(), ContentCorral.HTML_SUBFOLDER); MObject newest_object = null; while(c.moveToNext() && newest_object == null) { long newest_id = c.getLong(0); newest_object = objectManager.getObjectForId(newest_id); } //could only happen if someone delets while we export if(newest_object == null) { return null; } mName = UiUtil.getFeedNameFromMembersList(feedManager, feed); if(!contentDir.exists() && !contentDir.mkdirs()) { Log.e(TAG, "failed to create musubi html directory"); return null; } String filename = contentDir.getAbsolutePath() + "/" + encodeFilename(mName) + "." + newest_object.timestamp_ + ".html"; FileOutputStream fo; try { fo = new FileOutputStream(filename); } catch (FileNotFoundException e) { Log.e(TAG, "failed to open HTML export file", e); return null; } FeedHTML.writeHeader(fo, feedManager, feed); c.moveToPosition(-1); try { while(c.moveToNext() && !mCanceled) { long id = c.getLong(0); MObject object = objectManager.getObjectForId(id); if(object == null) continue; FeedHTML.writeObj(fo, mActivity, identitiesManager, object); } } finally { c.close(); } FeedHTML.writeFooter(fo); try { fo.close(); } catch (IOException e) { Log.e(TAG, "failed to close HTML export file", e); return null; } mFilename = filename; } finally { c.close(); } return null; } @Override protected void onPostExecute(Void result) { if(mFilename != null) { Intent share = new Intent(Intent.ACTION_SEND); share.setType("text/html"); share.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + mFilename)); share.putExtra(Intent.EXTRA_SUBJECT, "Musubi Feed: " + mName); share.putExtra(Intent.EXTRA_TEXT, "Here is a fun group activity that I was a part of using Musubi. http://play.google.com/store/apps/details?id=mobisocial.musubi"); startActivity(Intent.createChooser(share, "Send Feed Snapshot")); } mProgressDialog.dismiss(); } } void initLoaders(boolean restart) { LoaderManager lm = getLoaderManager(); Calendar cal = Calendar.getInstance(); cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0); Bundle args = new Bundle(); args.putLong("start", cal.getTimeInMillis()); if (restart) { lm.restartLoader(0, args, this); } else { lm.initLoader(0, args, this); } cal.add(Calendar.DAY_OF_MONTH, -1); for (int i = 1; i < DAYS_TO_SHOW; i++) { long time = cal.getTimeInMillis(); args = new Bundle(); args.putLong("start", time); args.putLong("end", time + ONE_DAY); if (restart) { lm.restartLoader(i, args, this); } else { lm.initLoader(i, args, this); } cal.add(Calendar.DAY_OF_MONTH, -1); } args = new Bundle(); args.putLong("end", cal.getTimeInMillis() + ONE_DAY); if (restart) { lm.restartLoader(DAYS_TO_SHOW, args, this); } else { lm.initLoader(DAYS_TO_SHOW, args, this); } } @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { Set<MIdentity> ids = mPeople.getSelectedIdentities(); String filterText = mPeople.getText().toString(); StringBuilder constraints = new StringBuilder(100); constraints.append("1=1"); if (args != null) { if (args.containsKey("start")) { constraints.append(" AND ").append(MFeed.COL_LATEST_RENDERABLE_OBJ_TIME) .append(">").append(args.getLong("start")); } if (args.containsKey("end")) { constraints.append(" AND ").append(MFeed.COL_LATEST_RENDERABLE_OBJ_TIME) .append("<=").append(args.getLong("end")); } } if (filterText.length() > 0) { if (ids.size() > 0) { for (MIdentity p : ids) { constraints.append(" AND ").append(MFeed.TABLE).append(".").append(MFeed.COL_ID).append(" in (SELECT ") .append(MFeedMember.COL_FEED_ID).append(" FROM ").append(MFeedMember.TABLE) .append(" WHERE ").append(MFeedMember.COL_IDENTITY_ID).append("=") .append(p.id_).append(")"); } } else { constraints.append(" AND ").append(MFeed.TABLE).append(".").append(MFeed.COL_NAME).append(" LIKE "); DatabaseUtils.appendEscapedSQLString(constraints, "%" + filterText + "%"); } } FeedSummaryLoader cl = new FeedSummaryLoader(getActivity(), constraints.toString()); cl.setUpdateThrottle(1000); return cl; } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { mFeeds.changeCursor(loader.getId(), cursor); /*if (cursor.moveToFirst()) { long feedId = cursor.getLong(cursor.getColumnIndexOrThrow(MFeed.COL_ID)); final Uri feedUri = MusubiContentProvider.uriForItem(Provided.FEEDS, feedId); if (getArguments() != null && getArguments().containsKey(DUAL_PANE) && isAdded()) { new Handler(getActivity().getMainLooper()).post(new Runnable() { @Override public void run() { selectFeed(0, feedUri); } }); } }*/ } @Override public void onLoaderReset(Loader<Cursor> arg0) { } void selectFeed(int position, Uri feedUri) { mFeedSelectedListener.onFeedSelected(feedUri); } private OnClickListener mNoAccountsListener = new OnClickListener() { @Override public void onClick(View v) { getActivity().showDialog(FeedListActivity.DIALOG_PLZ_LINK_ACCCOUNT); } }; private View.OnClickListener mJoinNearbyListener = new View.OnClickListener() { @Override public void onClick(View v) { startActivity(new Intent(mActivity, NearbyActivity.class)); } }; private View.OnClickListener mStartListener = new View.OnClickListener() { @Override public void onClick(View v) { LinkedHashSet<MIdentity> identities = mPeople.getSelectedIdentities(); //mAppSelectFragment.getSelectedApp(); if (identities.size() == 0) { Toast.makeText(mActivity, "You have to add people to start something!", Toast.LENGTH_SHORT).show(); } else { new CreateFeedAsyncTask().execute(identities.toArray(new MIdentity[identities.size()])); } } }; class CreateFeedAsyncTask extends AsyncTask<MIdentity, Void, Uri> { DialogFragment mCreatingFeedDialog; @Override protected void onPreExecute() { mCreatingFeedDialog = new CreateFeedDialogFragment(); ((MusubiBaseActivity)getActivity()).showDialog(mCreatingFeedDialog); } @Override protected Uri doInBackground(MIdentity... identities) { //explicit user control of identity is handled by putting yourself in the feed list FeedManager fm = new FeedManager(mDatabaseSource); MyAccountManager am = new MyAccountManager(mDatabaseSource); MFeed feed = fm.createExpandingFeed(identities); Uri feedUri = MusubiContentProvider.uriForItem(Provided.FEEDS, feed.id_); UiUtil.addToWhitelistsIfNecessary(fm, am, fm.getFeedMembers(feed), true); //introduce your buddies so they have names for each other Obj invitedObj = IntroductionObj.from(Arrays.asList(identities), true); Helpers.sendToFeed(mActivity, invitedObj, feedUri); App.getUsageMetrics(mActivity).report(MusubiMetrics.FEED_CREATED_EXPANDING); Long objId = fm.getCachedLatestRenderable(feed.id_); while (objId == null) { try { Thread.sleep(50); } catch (InterruptedException e) {} objId = fm.getCachedLatestRenderable(feed.id_); } return feedUri; } @Override protected void onPostExecute(Uri result) { mPeople.clearSelectedIdentities(); mCreatingFeedDialog.dismiss(); mFeedSelectedListener.onFeedSelected(result); } } public static class CreateFeedDialogFragment extends DialogFragment { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { ProgressDialog d = new ProgressDialog(getActivity()); d.setTitle("Just a Moment"); d.setMessage("Starting conversation..."); d.setIndeterminate(true); return d; } } /** * Static library support version of the framework's {@link android.content.CursorLoader}. * Used to write apps that run on platforms prior to Android 3.0. When running * on Android 3.0 or above, this implementation is still used; it does not try * to switch to the framework's implementation. See the framework SDK * documentation for a class overview. */ public static class FeedSummaryLoader extends AsyncTaskLoader<Cursor> { static final String TAG = "FeedObjectsCursorLoader"; final ForceLoadContentObserver mObserver; final SQLiteDatabase mDb; final String mConstraints; Cursor mCursor; /* Runs on a worker thread */ @Override public Cursor loadInBackground() { Cursor cursor = initCursor(); if (cursor != null) { // Ensure the cursor window is filled cursor.getCount(); registerContentObserver(cursor, mObserver); } return cursor; } /** * Registers an observer to get notifications from the content provider * when the cursor needs to be refreshed. */ void registerContentObserver(Cursor cursor, ContentObserver observer) { cursor.registerContentObserver(observer); } /* Runs on the UI thread */ @Override public void deliverResult(Cursor cursor) { if (isReset()) { // An async query came in while the loader is stopped if (cursor != null) { cursor.close(); } return; } Cursor oldCursor = mCursor; mCursor = cursor; if (isStarted()) { super.deliverResult(cursor); } if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) { oldCursor.close(); } } /** * Creates an empty unspecified CursorLoader. You must follow this with * calls to {@link #setUri(Uri)}, {@link #setSelection(String)}, etc * to specify the query to perform. */ public FeedSummaryLoader(Context context, String constraints) { super(context); mConstraints = constraints; mDb = App.getDatabaseSource(context).getReadableDatabase(); mObserver = new ForceLoadContentObserver(); } /** * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks * will be called on the UI thread. If a previous load has been completed and is still valid * the result may be passed to the callbacks immediately. * * Must be called from the UI thread */ @Override protected void onStartLoading() { if (mCursor != null) { deliverResult(mCursor); } if (takeContentChanged() || mCursor == null) { forceLoad(); } } /** * Must be called from the UI thread */ @Override protected void onStopLoading() { // Attempt to cancel the current load task if possible. cancelLoad(); } @Override public void onCanceled(Cursor cursor) { if (cursor != null && !cursor.isClosed()) { cursor.close(); } } @Override protected void onReset() { super.onReset(); // Ensure the loader is stopped onStopLoading(); if (mCursor != null && !mCursor.isClosed()) { mCursor.close(); } mCursor = null; } Cursor initCursor() { Cursor c = mDb.rawQuery(getFeedSummariesQuery(), null); c.setNotificationUri(getContext().getContentResolver(), MusubiContentProvider.uriForDir(Provided.FEEDS)); return c; } String getFeedSummariesQuery() { StringBuilder sql = new StringBuilder(100); sql.append("SELECT ") .append(MFeed.TABLE).append(".").append(MFeed.COL_ID).append(",") .append(MFeed.TABLE).append(".").append(MFeed.COL_NAME).append(",") .append(MFeed.TABLE).append(".").append(MFeed.COL_NUM_UNREAD).append(",") .append(MFeed.TABLE).append(".").append(MFeed.COL_LATEST_RENDERABLE_OBJ_TIME).append(",") .append(MObject.TABLE).append(".").append(MObject.COL_TYPE).append(",") .append(MObject.TABLE).append(".").append(MObject.COL_JSON).append(",") .append(MObject.TABLE).append(".").append(MObject.COL_IDENTITY_ID).append(",") .append(MFeed.TABLE).append(".").append(MFeed.COL_THUMBNAIL).append(" IS NOT NULL AS feed_thumbnail") .append(" FROM ").append(MFeed.TABLE) .append(" LEFT JOIN ").append(MObject.TABLE) .append(" ON ").append(MFeed.TABLE).append(".").append(MFeed.COL_LATEST_RENDERABLE_OBJ_ID) .append("=").append(MObject.TABLE).append(".").append(MObject.COL_ID) //.append(" LEFT JOIN ").append(MIdentity.TABLE) // .append(" ON ").append(MObject.TABLE).append(".").append(MObject.COL_IDENTITY_ID) // .append("=").append(MIdentity.TABLE).append(".").append(MIdentity.COL_ID) .append(" WHERE ").append(FeedManager.VISIBLE_FEED_SELECTION); if (mConstraints != null && mConstraints.length() > 0) { sql.append(" AND ").append(mConstraints); } sql.append(" ORDER BY ").append(sFeedSortOrder); return sql.toString(); } } public static class FeedSummary { final Context mContext; public long feedId; public String feedName; public int numUnread; public long timestamp; public String objType; public String objJsonSrc; public long identityId; public boolean hasThumbnail; private JSONObject mJson; public FeedSummary(Context context) { mContext = context; } public void populate(Cursor c) { feedId = c.getLong(0); feedName = (c.isNull(1)) ? null : c.getString(1); numUnread = c.getInt(2); timestamp = c.getLong(3); objType = c.getString(4); objJsonSrc = (c.isNull(5)) ? null : c.getString(5); identityId = c.getLong(6); hasThumbnail = c.getInt(7) != 0; mJson = null; } public JSONObject getJson() { if (mJson == null && objJsonSrc != null) { try { mJson = new JSONObject(objJsonSrc); } catch (JSONException e) {} } return mJson; } public CachedIdentity getSender() { return App.getContactCache(mContext).get(identityId); } } }