/*
* 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.action;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import mobisocial.musubi.App;
import mobisocial.musubi.Helpers;
import mobisocial.musubi.R;
import mobisocial.musubi.feed.iface.DbEntryHandler;
import mobisocial.musubi.model.DbRelation;
import mobisocial.musubi.model.MApp;
import mobisocial.musubi.model.helpers.AppManager;
import mobisocial.musubi.obj.ObjHelpers;
import mobisocial.musubi.obj.iface.ObjAction;
import mobisocial.musubi.objects.AppObj;
import mobisocial.musubi.objects.PictureObj;
import mobisocial.musubi.ui.fragments.AppSelectDialog;
import mobisocial.musubi.ui.fragments.AppSelectDialog.MusubiWebApp;
import mobisocial.musubi.ui.util.IntentProxyActivity;
import mobisocial.musubi.util.ActivityCallout;
import mobisocial.musubi.util.InstrumentedActivity;
import mobisocial.musubi.util.PhotoTaker;
import mobisocial.socialkit.musubi.DbObj;
import mobisocial.socialkit.musubi.Musubi;
import mobisocial.socialkit.obj.MemObj;
import org.json.JSONException;
import org.json.JSONObject;
import org.mobisocial.corral.ContentCorral;
import org.mobisocial.corral.CorralDownloadClient;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.LabeledIntent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.net.Uri;
import android.os.Environment;
import android.util.Log;
import android.widget.Toast;
/**
* Edits a picture object using the standard Android "EDIT" intent.
*
*/
public class EditPhotoAction extends ObjAction {
private static final String TAG = "EditPhotoAction";
public static final String CATEGORY_IN_PLACE = "mobisocial.intent.category.IN_PLACE";
static final String PICSAY_PACKAGE_PREFIX = "com.shinycore.picsay";
@Override
public void onAct(Context context, DbEntryHandler objType, DbObj obj) {
((InstrumentedActivity)context).doActivityForResult(
new EditCallout((Activity)context, obj));
}
@Override
public String getLabel(Context context) {
return "Edit";
}
@Override
public boolean isActive(Context context, DbEntryHandler objType, DbObj obj) {
return (objType instanceof PictureObj);
}
static File getTempImagePath() {
return new File(Environment.getExternalStorageDirectory().getAbsolutePath() +
"/musubi_edit.png");
}
public static class EditCallout implements ActivityCallout {
final JSONObject mJson;
final byte[] mRaw;
final Activity mContext;
final Uri mObjUri;
final Uri mFeedUri;
final Uri mHdUri;
final String mHash;
public EditCallout(Activity context, DbObj obj) {
mObjUri = obj.getUri();
mHash = obj.getUniversalHashString();
mJson = obj.getJson();
mRaw = obj.getRaw();
mContext = context;
mFeedUri = obj.getContainingFeed().getUri();
Uri hd = null;
CorralDownloadClient client = CorralDownloadClient.getInstance(context);
if (client.fileAvailableLocally(obj)) {
hd = client.getAvailableContentUri(obj);
}
mHdUri = hd;
}
@Override
public Intent getStartIntent() {
Uri contentUri;
File file;
if (mHdUri != null) {
// Don't edit in-place to avoid edited images showing up in
// places like the camera reel.
FileOutputStream out = null;
try {
file = getTempImagePath();
out = new FileOutputStream(file);
InputStream is = mContext.getContentResolver().openInputStream(mHdUri);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPurgeable = true;
options.inSampleSize = 4;
Bitmap bitmap = BitmapFactory.decodeStream(is, null, options);
Matrix matrix = new Matrix();
float rotation = PhotoTaker.rotationForImage(mContext, mHdUri);
if (rotation != 0f) {
matrix.preRotate(rotation);
int width = bitmap.getWidth();
int height = bitmap.getHeight();
bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true);
}
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
} catch (IOException e) {
Toast.makeText(mContext, "Could not edit photo.", Toast.LENGTH_SHORT).show();
Log.e(TAG, "Error editing photo", e);
return null;
} finally {
try {
if(out != null) out.close();
} catch (IOException e) {
Log.e(TAG, "failed to close output stream for picture", e);
}
}
} else {
OutputStream out = null;
try {
file = new File(Environment.getExternalStorageDirectory().getAbsolutePath() +
"/musubi_edit.png");
out = new FileOutputStream(file);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPurgeable = true;
options.inInputShareable = true;
Bitmap bitmap = BitmapFactory.decodeByteArray(mRaw, 0, mRaw.length, options);
if(bitmap == null)
return null;
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
out.flush();
out.close();
bitmap.recycle();
bitmap = null;
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
try {
if(out != null) out.close();
} catch (IOException e) {
Log.e(TAG, "failed to close output stream for picture", e);
}
}
}
contentUri = Uri.fromFile(file);
Log.w(TAG, "uri=" + contentUri);
return getEditorChooserIntent(mContext, contentUri, "image/png", mFeedUri);
}
@Override
public void handleResult(int resultCode, final Intent data) {
if (resultCode == Activity.RESULT_OK) {
new Thread() {
@Override
public void run() {
try {
ComponentName cn = data.getParcelableExtra(IntentProxyActivity.EXTRA_RESOLVED_COMPONENT);
Uri result;
if (cn.getPackageName().startsWith(PICSAY_PACKAGE_PREFIX)) {
result = data.getData();
} else {
// IN_PLACE
result = Uri.fromFile(getTempImagePath());
}
boolean reference = true;
Uri stored = ContentCorral.storeContent(mContext, result);
if (stored == null) {
Log.w(TAG, "Error storing content in corral");
stored = result;
reference = false;
} else {
new File(result.getPath()).delete();
}
MemObj outboundObj = PictureObj.from(mContext, stored, reference);
try {
JSONObject json = outboundObj.getJson();
json.put(ObjHelpers.TARGET_HASH, mHash);
json.put(ObjHelpers.TARGET_RELATION, DbRelation.RELATION_EDIT);
json.put(AppObj.ANDROID_PACKAGE_NAME, cn.getPackageName());
json.put(AppObj.CLAIMED_APP_ID, cn.getPackageName());
try {
ActivityInfo info = mContext.getPackageManager()
.getActivityInfo(cn, 0);
json.put(AppObj.APP_NAME, info.loadLabel(
mContext.getPackageManager()));
} catch (NameNotFoundException e) {
}
} catch (JSONException e) {}
Helpers.sendToFeed(mContext, outboundObj, mFeedUri);
} catch (IOException e) {
Log.e(TAG, "Error reading photo data.", e);
toast("Error reading photo data.");
}
}
}.start();
}
}
private final void toast(final String text) {
mContext.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(mContext, text, Toast.LENGTH_SHORT).show();
}
});
}
/**
* Returns a chooser intent for editing a photo with results. Includes apps
* that support the RETURN_RESULT category as well as PicSay, which is known
* to return a result.
*/
Intent getEditorChooserIntent(Context context, Uri contentUri, String mimeType, Uri feedUri) {
List<Intent> targetedShareIntents = new ArrayList<Intent>();
Intent editIntent = new Intent(android.content.Intent.ACTION_EDIT);
editIntent.addCategory(CATEGORY_IN_PLACE);
editIntent.setDataAndType(contentUri, mimeType);
String title = "Edit with...";
Intent picsayIntent = getPicsayIntent(context, contentUri, mimeType);
targetedShareIntents.addAll(getBundledEditorIntents(context, mObjUri, feedUri));
assert(targetedShareIntents.size() != 0);
List<ResolveInfo> resInfo = context.getPackageManager().queryIntentActivities(
editIntent, PackageManager.MATCH_DEFAULT_ONLY);
if (!resInfo.isEmpty()){
for (ResolveInfo resolveInfo : resInfo) {
String packageName = resolveInfo.activityInfo.packageName;
Intent targetedShareIntent = new Intent(Intent.ACTION_EDIT);
targetedShareIntent.setDataAndType(contentUri, mimeType);
targetedShareIntent.putExtra(Musubi.EXTRA_FEED_URI, feedUri);
targetedShareIntent.addCategory(CATEGORY_IN_PLACE);
targetedShareIntent.setPackage(packageName);
targetedShareIntents.add(targetedShareIntent);
}
if (picsayIntent != null) {
targetedShareIntents.add(picsayIntent);
}
} else {
if (picsayIntent != null) {
targetedShareIntents.add(picsayIntent);
} else {
LabeledIntent picsayMarket = new LabeledIntent(context.getPackageName(), "PicSay", R.drawable.picsay_icon);
picsayMarket.setAction(Intent.ACTION_VIEW);
picsayMarket.setData(Uri.parse("market://details?id=com.shinycore.picsayfree"));
targetedShareIntents.add(picsayMarket);
}
}
targetedShareIntents.addAll(getWebEditorIntents(context, mObjUri, feedUri));
// XXX First intent must not be a LabeledIntent.
// See getBundledEditorIntents().
// Sketch doens't have to be proxied because it doesn't return a result.
Intent first = targetedShareIntents.remove(0);
Intent[] later = new Intent[targetedShareIntents.size()];
int i = 0;
for (Intent intent : targetedShareIntents) {
later[i++] = IntentProxyActivity.getProxyIntent(context, intent);
}
Intent chooserIntent = Intent.createChooser(first, title);
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, later);
return chooserIntent;
}
Intent getPicsayIntent(Context context, Uri contentUri, String mimeType) {
Intent edit = new Intent(Intent.ACTION_EDIT);
edit.setDataAndType(contentUri, mimeType);
List<ResolveInfo> resInfo = context.getPackageManager().queryIntentActivities(edit, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo i : resInfo) {
if (i.activityInfo.packageName.startsWith(PICSAY_PACKAGE_PREFIX)) {
Intent picsay = new Intent();
picsay.setAction(Intent.ACTION_EDIT);
picsay.setPackage(i.activityInfo.packageName);
picsay.setDataAndType(contentUri, mimeType);
return picsay;
}
}
return null;
}
/**
* XXX This is largely a workaround for a nasty bug somewhere between here
* and Android's ResolverActivity. You cannot send a LabeledIntent as the
* first intent in Intent.createChooser(), or bad things happen. Here we
* force the first intent to be an Intent serviced by our application.
*/
Set<Intent> getBundledEditorIntents(Context context, Uri objUri, Uri feedUri) {
Set<Intent> editors = new HashSet<Intent>();
// Sketch
MApp sketch = new AppManager(App.getDatabaseSource(context))
.lookupAppByAppId(AppSelectDialog.SKETCH_APP_ID);
if (sketch != null) {
MusubiWebApp app = new MusubiWebApp(context, sketch.name_, sketch.appId_,
sketch.webAppUrl_, R.drawable.sketch);
Intent intent = app.getLaunchIntent(context, feedUri);
Intent wrapper = new Intent("musubi.intent.action.SKETCH");
wrapper.setData(objUri);
wrapper.putExtras(intent.getExtras());
editors.add(wrapper);
}
return editors;
}
/**
* This is a poor approximation of an edit intent over a SocialDB object.
* The generalization is an app that supports the EDIT intent for objects
* of type "picture" (ObjHelper.mimeTypeOf("picture") == "vnd.musubi.obj/picture")
*/
Set<Intent> getWebEditorIntents(Context context, Uri objUri, Uri feedUri) {
Set<Intent> editors = new HashSet<Intent>();
List<MApp> apps = new AppManager(App.getDatabaseSource(context))
.lookupAppForAction(PictureObj.TYPE, "edit");
for(MApp app : apps) {
MusubiWebApp web_app = new MusubiWebApp(context, app.name_, app.appId_,
app.webAppUrl_, R.drawable.ic_menu_globe);
Intent intent = web_app.getLaunchIntent(context, feedUri);
intent.setData(objUri);
editors.add(intent);
}
return editors;
}
}
}