diff --git a/android/playground/app/src/main/java/com/alibaba/weex/IndexActivity.java b/android/playground/app/src/main/java/com/alibaba/weex/IndexActivity.java index 8469b26736..866733a773 100644 --- a/android/playground/app/src/main/java/com/alibaba/weex/IndexActivity.java +++ b/android/playground/app/src/main/java/com/alibaba/weex/IndexActivity.java @@ -51,6 +51,7 @@ public class IndexActivity extends AbstractWeexActivity { private static final String TAG = "IndexActivity"; private static final int CAMERA_PERMISSION_REQUEST_CODE = 0x1; + private static final int WRITE_EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE = 0x2; private static final String DEFAULT_IP = "your_current_IP"; private static String sCurrentIp = DEFAULT_IP; // your_current_IP @@ -102,6 +103,18 @@ public void onReceive(Context context, Intent intent) { }; LocalBroadcastManager.getInstance(this).registerReceiver(mReloadReceiver, new IntentFilter(WXSDKEngine.JS_FRAMEWORK_RELOAD)); + + requestWeexPermission(); + } + + private void requestWeexPermission() { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + Toast.makeText(this, "please give me the permission", Toast.LENGTH_SHORT).show(); + } else { + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE); + } + } } @Override @@ -147,6 +160,7 @@ public void onRequestPermissionsResult(int requestCode, String[] permissions, in super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == CAMERA_PERMISSION_REQUEST_CODE && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { startActivity(new Intent(this, CaptureActivity.class)); + } else if (requestCode == WRITE_EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { } else { Toast.makeText(this, "request camara permission fail!", Toast.LENGTH_SHORT).show(); } diff --git a/android/sdk/src/main/java/com/taobao/weex/WXSDKManager.java b/android/sdk/src/main/java/com/taobao/weex/WXSDKManager.java index 0b43fa6fd6..4e144e0c66 100644 --- a/android/sdk/src/main/java/com/taobao/weex/WXSDKManager.java +++ b/android/sdk/src/main/java/com/taobao/weex/WXSDKManager.java @@ -45,6 +45,7 @@ import com.taobao.weex.common.WXRefreshData; import com.taobao.weex.common.WXRuntimeException; import com.taobao.weex.common.WXThread; +import com.taobao.weex.common.WXWorkThreadManager; import com.taobao.weex.dom.WXDomManager; import com.taobao.weex.ui.WXRenderManager; import com.taobao.weex.utils.WXLogUtils; @@ -64,6 +65,7 @@ public class WXSDKManager { private static volatile WXSDKManager sManager; private static AtomicInteger sInstanceId = new AtomicInteger(0); private final WXDomManager mWXDomManager; + private final WXWorkThreadManager mWXWorkThreadManager; private WXBridgeManager mBridgeManager; /** package **/ WXRenderManager mWXRenderManager; @@ -97,6 +99,7 @@ private WXSDKManager(WXRenderManager renderManager) { mWXRenderManager = renderManager; mWXDomManager = new WXDomManager(mWXRenderManager); mBridgeManager = WXBridgeManager.getInstance(); + mWXWorkThreadManager = new WXWorkThreadManager(); } /** @@ -195,6 +198,10 @@ public WXRenderManager getWXRenderManager() { return mWXRenderManager; } + public WXWorkThreadManager getWXWorkThreadManager() { + return mWXWorkThreadManager; + } + public @Nullable WXSDKInstance getSDKInstance(String instanceId) { return instanceId == null? null : mWXRenderManager.getWXSDKInstance(instanceId); } @@ -207,6 +214,9 @@ public void destroy() { if (mWXDomManager != null) { mWXDomManager.destroy(); } + if (mWXWorkThreadManager != null) { + mWXWorkThreadManager.destroy(); + } } @Deprecated diff --git a/android/sdk/src/main/java/com/taobao/weex/common/WXWorkThreadManager.java b/android/sdk/src/main/java/com/taobao/weex/common/WXWorkThreadManager.java new file mode 100644 index 0000000000..3510d54332 --- /dev/null +++ b/android/sdk/src/main/java/com/taobao/weex/common/WXWorkThreadManager.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 com.taobao.weex.common; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Class for managing work thread + */ +public final class WXWorkThreadManager { + + private ExecutorService singleThreadExecutor; + + public WXWorkThreadManager() { + singleThreadExecutor = Executors.newSingleThreadExecutor(); + } + + public void post(Runnable task) { + if (singleThreadExecutor != null) + singleThreadExecutor.execute(task); + } + + /** + * Destroy current instance + */ + public void destroy() { + if (singleThreadExecutor != null) + singleThreadExecutor.shutdown(); + singleThreadExecutor = null; + } +} diff --git a/android/sdk/src/main/java/com/taobao/weex/ui/component/WXImage.java b/android/sdk/src/main/java/com/taobao/weex/ui/component/WXImage.java index 89ceedcd6c..db58d9911e 100644 --- a/android/sdk/src/main/java/com/taobao/weex/ui/component/WXImage.java +++ b/android/sdk/src/main/java/com/taobao/weex/ui/component/WXImage.java @@ -18,13 +18,16 @@ */ package com.taobao.weex.ui.component; +import android.Manifest; import android.content.Context; +import android.content.pm.PackageManager; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; import android.text.TextUtils; import android.widget.ImageView; import android.widget.ImageView.ScaleType; @@ -34,6 +37,8 @@ import com.taobao.weex.adapter.IWXImgLoaderAdapter; import com.taobao.weex.adapter.URIAdapter; import com.taobao.weex.annotation.Component; +import com.taobao.weex.annotation.JSMethod; +import com.taobao.weex.bridge.JSCallback; import com.taobao.weex.common.Constants; import com.taobao.weex.common.WXImageSharpen; import com.taobao.weex.common.WXImageStrategy; @@ -46,6 +51,7 @@ import com.taobao.weex.utils.ImageDrawable; import com.taobao.weex.utils.ImgURIUtil; import com.taobao.weex.utils.SingleFunctionParser; +import com.taobao.weex.utils.WXViewToImageUtil; import com.taobao.weex.utils.WXDomUtils; import com.taobao.weex.utils.WXLogUtils; import com.taobao.weex.utils.WXUtils; @@ -62,6 +68,10 @@ */ @Component(lazyload = false) public class WXImage extends WXComponent { + + public static final String SUCCEED = "success"; + public static final String ERRORDESC = "errorDesc"; + private String mSrc; private int mBlurRadius; @@ -332,6 +342,64 @@ public void updateProperties(Map props) { } } + /** + * Need permission {android.permission.WRITE_EXTERNAL_STORAGE} + */ + @JSMethod(uiThread = false) + public void save(final JSCallback saveStatuCallback) { + + if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + if (saveStatuCallback != null) { + Map result = new HashMap<>(); + result.put(SUCCEED, false); + result.put(ERRORDESC,"Permission denied: android.permission.WRITE_EXTERNAL_STORAGE"); + saveStatuCallback.invoke(result); + } + return; + } + + if (mHost == null) { + if (saveStatuCallback != null) { + Map result = new HashMap<>(); + result.put(SUCCEED, false); + result.put(ERRORDESC,"Image component not initialized"); + saveStatuCallback.invoke(result); + } + return; + } + + if (mSrc == null || mSrc.equals("")) { + if (saveStatuCallback != null) { + Map result = new HashMap<>(); + result.put(SUCCEED, false); + result.put(ERRORDESC,"Image does not have the correct src"); + saveStatuCallback.invoke(result); + } + return; + } + + WXViewToImageUtil.generateImage(mHost, 0, 0xfff8f8f8, new WXViewToImageUtil.OnImageSavedCallback() { + @Override + public void onSaveSucceed(String path) { + if (saveStatuCallback != null) { + Map result = new HashMap<>(); + result.put(SUCCEED, true); + saveStatuCallback.invoke(result); + } + } + + @Override + public void onSaveFailed(String errorDesc) { + if (saveStatuCallback != null) { + Map result = new HashMap<>(); + result.put(SUCCEED, false); + result.put(ERRORDESC,errorDesc); + saveStatuCallback.invoke(result); + } + } + }); + } + public interface Measurable { int getNaturalWidth(); int getNaturalHeight(); diff --git a/android/sdk/src/main/java/com/taobao/weex/utils/WXViewToImageUtil.java b/android/sdk/src/main/java/com/taobao/weex/utils/WXViewToImageUtil.java new file mode 100755 index 0000000000..b486fb524c --- /dev/null +++ b/android/sdk/src/main/java/com/taobao/weex/utils/WXViewToImageUtil.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 com.taobao.weex.utils; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.net.Uri; +import android.os.Environment; +import android.os.Handler; +import android.os.Looper; +import android.provider.MediaStore; +import android.support.annotation.ColorInt; +import android.view.View; + +import com.taobao.weex.WXSDKManager; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Created by miomin on 7/25/17. + */ +public class WXViewToImageUtil { + + public static int mBackgroundColor = Color.TRANSPARENT; + + /** + * Generate image and return path via callback + */ + public static void generateImage(final View imageView, final int width, + @ColorInt final int backgroundColor, final OnImageSavedCallback mOnImageSavedCallback) { + + mBackgroundColor = backgroundColor; + + // Only one save image task occurs at the same time + WXSDKManager.getInstance().getWXWorkThreadManager().post(new Thread(new Runnable() { + @Override + public void run() { + // Generate bitmap from ImageView + Bitmap bitmap = getBitmapFromImageView(imageView, width); + + if (bitmap == null) { + if (mOnImageSavedCallback != null) { + mOnImageSavedCallback.onSaveFailed("Image is empty"); + } + return; + } + + // Sava bitmap to gallery + final String destPath = saveBitmapToGallery(imageView.getContext(), bitmap, mOnImageSavedCallback); + + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (mOnImageSavedCallback != null) { + mOnImageSavedCallback.onSaveSucceed(destPath); + imageView.getContext().sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse(destPath))); + } + } + }); + } + })); + } + + /** + * Save bitmap to gallery + * @return image save path + */ + public static String saveBitmapToGallery(Context context, Bitmap bitmap, final OnImageSavedCallback mOnImageSavedCallback) { + + // Save image + File appDir = new File(Environment.getExternalStorageDirectory(), "Weex"); + if (!appDir.exists()) { + appDir.mkdir(); + } + + String fileName = System.currentTimeMillis() + ".jpg"; + File file = new File(appDir, fileName); + + try { + FileOutputStream fos = new FileOutputStream(file); + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos); + fos.flush(); + fos.close(); + } catch (FileNotFoundException e) { + if (mOnImageSavedCallback != null) + mOnImageSavedCallback.onSaveFailed("Image creation failed due to system reason"); + e.printStackTrace(); + } catch (IOException e) { + if (mOnImageSavedCallback != null) + mOnImageSavedCallback.onSaveFailed("Android IOException"); + e.printStackTrace(); + } + + // Insert the image file into the system gallery + try { + MediaStore.Images.Media.insertImage(context.getContentResolver(), + file.getAbsolutePath(), fileName, null); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + + // Notify the system gallery update + context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse("file://" + appDir.getAbsolutePath() + "/" + fileName))); + + return file.getAbsolutePath(); + } + + /** + * Save state callback + */ + public interface OnImageSavedCallback { + void onSaveSucceed(String path); + void onSaveFailed(String errorDesc); + } + + /** + * Get bitmap from imageview + */ + public static Bitmap getBitmapFromImageView(final View view, int width) { + if (view.getWidth() <= 0 || view.getHeight() <= 0) { + view.measure(View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); + view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); + } + + view.setDrawingCacheEnabled(true); + Bitmap bitmap = view.getDrawingCache(); + return bitmap; + } +}