/*
* 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;
import java.io.IOException;
import java.io.InputStream;
import mobisocial.musubi.model.MObject;
import mobisocial.musubi.model.helpers.IdentitiesManager;
import mobisocial.musubi.model.helpers.ObjectManager;
import mobisocial.musubi.obj.action.EditPhotoAction;
import mobisocial.musubi.obj.action.SharePhotoAction;
import mobisocial.musubi.objects.PictureObj;
import mobisocial.musubi.ui.MusubiBaseActivity;
import mobisocial.musubi.util.CommonLayouts;
import mobisocial.musubi.util.InstrumentedActivity;
import mobisocial.musubi.util.PhotoTaker;
import mobisocial.musubi.util.SimpleCursorLoader;
import mobisocial.musubi.util.SlowGallery;
import mobisocial.socialkit.musubi.DbObj;
import mobisocial.socialkit.musubi.Musubi;
import org.json.JSONException;
import org.json.JSONObject;
import org.mobisocial.corral.CorralDownloadClient;
import org.mobisocial.corral.CorralDownloadHandler;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteOpenHelper;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
import android.support.v4.view.MenuItem;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.CursorAdapter;
import android.widget.Gallery;
import android.widget.ImageView;
/**
* A gallery for viewing all photos in a feed.
*
*/
//TODO: if someone deletes a picture from the feed while it is being shown, weird
//things happen
public class ImageGalleryActivity extends MusubiBaseActivity implements LoaderCallbacks<Cursor>,
InstrumentedActivity, OnItemSelectedListener {
public static final String EXTRA_DEFAULT_OBJECT_ID = "objectId";
static final String EXTRA_SELECTION = "selection";
private static final String TAG = "imageGallery";
private static final boolean DBG = false;
private Gallery mGallery;
private ImageGalleryAdapter mAdapter;
private Uri mFeedUri;
private long mInitialObjId;
private int mInitialSelection = -1;
private CorralDownloadClient mCorralClient;
private CorralHandler mCorralHandler;
private Musubi mMusubi;
int mScreenWidth;
int mScreenHeight;
long mCurrentObjId;
SQLiteOpenHelper mDatabaseSource;
ObjectManager mObjectManager;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
if (getSupportActionBar() != null) {
getSupportActionBar().hide();
}
DisplayMetrics dm = getResources().getDisplayMetrics();
mScreenWidth = dm.widthPixels;
mScreenHeight = dm.heightPixels;
mCorralClient = CorralDownloadClient.getInstance(this);
mFeedUri = getIntent().getData();
mInitialObjId = getIntent().getLongExtra(EXTRA_DEFAULT_OBJECT_ID, -1);
mGallery = new SlowGallery(this);
mGallery.setBackgroundColor(Color.BLACK);
mGallery.setOnItemSelectedListener(this);
addContentView(mGallery, CommonLayouts.FULL_SCREEN);
if (savedInstanceState != null) {
mInitialSelection = savedInstanceState.getInt(EXTRA_SELECTION);
}
mMusubi = App.getMusubi(this);
mDatabaseSource = App.getDatabaseSource(this);
mObjectManager = new ObjectManager(mDatabaseSource);
getSupportLoaderManager().initLoader(0, null, this);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(EXTRA_SELECTION, mGallery.getSelectedItemPosition());
}
@Override
protected void onNewIntent(Intent intent) {
if (MusubiBaseActivity.isTVModeEnabled(this)) {
mGallery.setSelection(0);
}
}
// Cursor must be ordered ASC.
// The sort order and search order are opposite!
private static int binarySearch(Cursor c, long id, int colId) {
long test;
int first = 0;
int max = c.getCount();
while (first < max) {
int mid = (first + max) / 2;
c.moveToPosition(mid);
test = c.getLong(colId);
if (id < test) {
max = mid;
} else if (id > test) {
first = mid + 1;
} else {
return mid;
}
}
return 0;
}
@Override
protected void onResume() {
super.onResume();
mCorralHandler = new CorralHandler();
mCorralHandler.start();
}
@Override
protected void onPause() {
super.onPause();
cleanCorralImage();
shutdownCorralThread();
}
@Override
protected void onDestroy() {
super.onDestroy();
}
private void shutdownCorralThread() {
if(mCorralHandler != null) {
Message msg = mCorralHandler.obtainQuitMessage();
mCorralHandler.mHandler.sendMessage(msg);
mCorralHandler = null;
}
}
private class ImageGalleryAdapter extends CursorAdapter {
private final Context mContext;
private final int mInitialSelection;
private final int COL_ID;
public int getInitialSelection() {
return mInitialSelection;
}
private ImageGalleryAdapter(Context context, Cursor c, int init) {
super(context, c);
mContext = context;
mInitialSelection = init;
COL_ID = c.getColumnIndexOrThrow(MObject.COL_ID);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
DbObj obj = mMusubi.objForId(cursor.getLong(0));
//shouldn't happen unless someone deletes a picture.
if(obj == null)
return;
ImageView im = (ImageView)view;
im.setTag(cursor.getLong(COL_ID));
byte[] bytes = obj.getRaw();
if (bytes == null) {
Log.e(TAG, "Null image bytes for " + im.getTag(), new Throwable());
return;
}
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPurgeable = true;
options.inInputShareable = true;
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
im.setImageBitmap(bitmap);
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
ImageView im = new ImageView(mContext);
im.setLayoutParams(new Gallery.LayoutParams(
Gallery.LayoutParams.MATCH_PARENT,
Gallery.LayoutParams.MATCH_PARENT));
im.setScaleType(ImageView.ScaleType.FIT_CENTER);
im.setBackgroundColor(Color.BLACK);
return im;
}
}
private static final int MENU_EDIT = 3;
private static final int MENU_SHARE = 4;
private static final int MENU_SET_PROFILE = 5;
@Override
public boolean onCreateOptionsMenu(android.support.v4.view.Menu menu) {
menu.add(0, MENU_EDIT, 1, "Edit");
menu.add(0, MENU_SHARE, 2, "Share");
menu.add(0, MENU_SET_PROFILE, 3, "Set as Profile"); // XXX Bug prevents last menu entry
// from showing up on phones without a hardware menu button.
return true;
}
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case MENU_SET_PROFILE: {
new Thread() {
public void run() {
long objId = (Long)mGallery.getSelectedView().getTag();
DbObj obj = mMusubi.objForId(objId);
byte[] picBytes = obj.getRaw();
IdentitiesManager idMan = new IdentitiesManager(
App.getDatabaseSource(ImageGalleryActivity.this));
idMan.updateMyProfileThumbnail(ImageGalleryActivity.this, picBytes, true);
toast("Set profile picture.");
};
}.start();
return true;
}
case MENU_SHARE: {
long objId = (Long)mGallery.getSelectedView().getTag();
DbObj obj = mMusubi.objForId(objId);
new SharePhotoAction().actOn(this, new PictureObj(), obj);
return true;
}
case MENU_EDIT: {
long objId = (Long)mGallery.getSelectedView().getTag();
DbObj obj = mMusubi.objForId(objId);
doActivityForResult(new EditPhotoAction.EditCallout(this, obj));
return true;
}
default:
return false;
}
}
Uri loadFromCorral(CorralHandler handler, final DbObj obj) {
if (MusubiBaseActivity.isDeveloperModeEnabled(this)) {
try {
// Fetch via remote
return CorralDownloadHandler.startOrFetchDownload(this, obj).getResult();
} catch (InterruptedException e) {
Log.i(TAG, "Failed to get hd content", e);
}
}
// Local-only
return mCorralClient.getAvailableContentUri(obj);
}
private void cleanCorralImage() {
if(mCorralImage != null && mCorralImage.getBitmap() != null) {
mCorralView.setImageDrawable(mOriginalImage);
mCorralImage.getBitmap().recycle();
mCorralImage = null;
mOriginalImage = null;
mCorralView = null;
}
}
void injectImage(Uri fileUri, final ImageView imageView, final DbObj mObj) {
try {
if ((Long)imageView.getTag() == mObj.getLocalId()) {
if (DBG) Log.d(TAG, "Opening HD file " + fileUri);
runOnUiThread(new Runnable() {
@Override
public void run() {
if ((Long)imageView.getTag() == mObj.getLocalId()) {
cleanCorralImage();
}
}
});
final BitmapDrawable image = getBitmap(fileUri);
if(image == null) {
return;
}
runOnUiThread(new Runnable() {
@Override
public void run() {
if ((Long)imageView.getTag() == mObj.getLocalId()) {
mCorralImage = image;
mOriginalImage = (BitmapDrawable)imageView.getDrawable();
mCorralView = imageView;
imageView.setImageDrawable(image);
}
}
});
}
} catch (OutOfMemoryError e) {
App.getUsageMetrics(this).report(e);
Log.e(TAG, "error loading data from corral", e);
}
}
class RotatedBitmapDrawable extends BitmapDrawable {
float mRotation;
public RotatedBitmapDrawable(Bitmap b, float rotation) {
super(b);
mRotation = rotation;
}
@Override
public int getIntrinsicHeight() {
if(mRotation > 89 && mRotation < 91 || mRotation > 269 && mRotation < 271) {
return super.getIntrinsicWidth();
} else {
return super.getIntrinsicHeight();
}
}
@Override
public int getIntrinsicWidth() {
if(mRotation > 89 && mRotation < 91 || mRotation > 269 && mRotation < 271) {
return super.getIntrinsicHeight();
} else {
return super.getIntrinsicWidth();
}
}
@Override
public void draw(Canvas canvas) {
int saveCount = canvas.save();
Rect bounds = super.getBounds();
canvas.rotate(mRotation, bounds.centerX(), bounds.centerY());
if(mRotation > 89 && mRotation < 91 || mRotation > 269 && mRotation < 271) {
canvas.scale((float)getIntrinsicHeight() / getIntrinsicWidth(), (float)getIntrinsicWidth() / getIntrinsicHeight(), bounds.centerX(), bounds.centerY());
}
super.draw(canvas);
canvas.restoreToCount(saveCount);
}
}
//get rotated bitmap allowing the data to be shared
BitmapDrawable getBitmap(Uri uri) {
InputStream in = null;
try {
in = getContentResolver().openInputStream(uri);
// Decode image size
BitmapFactory.Options o = new BitmapFactory.Options();
o.inJustDecodeBounds = true;
BitmapFactory.decodeStream(in, null, o);
in.close();
in = getContentResolver().openInputStream(uri);
float rotation = PhotoTaker.rotationForImage(this, uri);
DisplayMetrics dm = getResources().getDisplayMetrics();
int screen_width = dm.widthPixels;
int screen_height = dm.heightPixels;
int scale = 1;
if(rotation > 89 && rotation < 91 || rotation > 269 && rotation < 271) {
int t = screen_width;
screen_width = screen_height;
screen_height = t;
}
while (o.outWidth / (scale + 1) >= screen_width && o.outHeight / (scale + 1) >= screen_height) {
scale++;
}
o = new BitmapFactory.Options();
o.inPurgeable = true;
o.inInputShareable = true;
o.inSampleSize = scale;
Bitmap b = BitmapFactory.decodeStream(in, null, o);
return new RotatedBitmapDrawable(b, rotation);
} catch (IOException e) {
Log.e(TAG, e.getMessage(), e);
return null;
}
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new SimpleCursorLoader(this) {
@Override
public Cursor loadInBackground() {
long feedId = Long.parseLong(mFeedUri.getLastPathSegment());
return mObjectManager.getTypedIdCursorForFeed(PictureObj.TYPE, feedId);
}
};
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
if (mAdapter == null) {
int init = binarySearch(cursor, mInitialObjId, 0);
cursor.moveToPosition(-1);
mAdapter = new ImageGalleryAdapter(this, cursor, init);
mGallery.setAdapter(mAdapter);
mGallery.setSelection((mInitialSelection == -1)
? mAdapter.getInitialSelection() : mInitialSelection);
} else {
mAdapter.changeCursor(cursor);
}
}
@Override
public void onLoaderReset(Loader<Cursor> arg0) {
}
BitmapDrawable mCorralImage;
BitmapDrawable mOriginalImage;
ImageView mCorralView;
@Override
public void onItemSelected(AdapterView<?> adapter, View view, int position, long id) {
if (view.getTag() == null) {
return;
}
mCurrentObjId = (Long)view.getTag();
DbObj obj = mMusubi.objForId(mCurrentObjId);
//should happen unless someone deletes an obj from the feed.
if(obj == null) {
return;
}
if (obj.getJson() != null) {
//this can get called after onPause so we have to check that there is a corral handler still
if (obj.getJson().has(CorralDownloadClient.OBJ_LOCAL_URI) && mCorralHandler != null) {
Message msg = mCorralHandler.obtainLoadImageMessage();
msg.obj = new CorralArg(mCurrentObjId, (ImageView)view);
mCorralHandler.mHandler.sendMessage(msg);
}
}
}
@Override
public void onNothingSelected(AdapterView<?> arg0) {
}
class CorralHandler extends Thread {
final int LOAD_IMAGE = 0;
final int QUIT = 1;
public Handler mHandler;
boolean mLoaded = false;
boolean mQuit = false;
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
Looper.prepare();
mHandler = new Handler() {
public void handleMessage(Message msg) {
if(hasMessages(QUIT)) {
//don't handle other buffered loads if possible
mQuit = true;
getLooper().quit();
return;
}
switch (msg.what) {
case LOAD_IMAGE:
CorralArg arg = (CorralArg)msg.obj;
if (arg.objId != mCurrentObjId) {
return;
}
DbObj obj = mMusubi.objForId(arg.objId);
if (obj == null) {
Log.w(TAG, "null object: " + arg.objId);
return;
}
Uri fileUri = loadFromCorral(CorralHandler.this, obj);
if(fileUri == null)
break;
if(hasMessages(LOAD_IMAGE) || hasMessages(QUIT))
break;
injectImage(fileUri, arg.imageView, obj);
arg.imageView = null;
arg.objId = -1;
break;
case QUIT:
mQuit = true;
getLooper().quit();
break;
}
}
};
mLoaded = true;
Looper.loop();
}
public Message obtainQuitMessage() {
prepareHandler();
Message msg = mHandler.obtainMessage();
msg.what = QUIT;
return msg;
}
public Message obtainLoadImageMessage() {
prepareHandler();
Message msg = mHandler.obtainMessage();
msg.what = LOAD_IMAGE;
return msg;
}
void prepareHandler() {
while (!mLoaded) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {}
}
}
}
class CorralArg {
long objId;
ImageView imageView;
public CorralArg(long objId, ImageView image) {
this.objId = objId;
this.imageView = image;
}
}
}