chore(format): Android project formatting (#433)

Added an extremely simple `.editorconfig` I inferred from the main part of the project, then reformatted the codebase according to it. 🙂
This commit is contained in:
Stanisław Chmiela 2019-03-19 15:42:47 +01:00 committed by Thibault Malbranche
parent 0d3b1df2e3
commit e697dff1d0
13 changed files with 529 additions and 538 deletions

6
android/.editorconfig Normal file
View File

@ -0,0 +1,6 @@
[*]
charset=utf-8
end_of_line=lf
insert_final_newline=false
indent_style=space
indent_size=2

View File

@ -1,13 +1,15 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.reactnativecommunity.webview">
<application>
<provider
android:name=".RNCWebViewFileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider_paths" />
</provider>
</application>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.reactnativecommunity.webview">
<application>
<provider
android:name=".RNCWebViewFileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider_paths" />
</provider>
</application>
</manifest>

View File

@ -4,7 +4,7 @@ import android.support.v4.content.FileProvider;
/**
* Providing a custom {@code FileProvider} prevents manifest {@code <provider>} name collisions.
*
* <p>
* See https://developer.android.com/guide/topics/manifest/provider-element.html for details.
*/
public class RNCWebViewFileProvider extends FileProvider {

View File

@ -4,24 +4,6 @@ import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.DownloadManager;
import android.content.Context;
import com.facebook.react.uimanager.UIManagerModule;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
@ -43,7 +25,6 @@ import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReactContext;
@ -52,67 +33,62 @@ import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.build.ReactBuildConfig;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.events.ContentSizeChangeEvent;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.reactnativecommunity.webview.events.TopLoadingErrorEvent;
import com.reactnativecommunity.webview.events.TopLoadingFinishEvent;
import com.reactnativecommunity.webview.events.TopLoadingProgressEvent;
import com.reactnativecommunity.webview.events.TopLoadingStartEvent;
import com.reactnativecommunity.webview.events.TopMessageEvent;
import com.reactnativecommunity.webview.events.TopLoadingProgressEvent;
import com.reactnativecommunity.webview.events.TopShouldStartLoadWithRequestEvent;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import javax.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import javax.annotation.Nullable;
/**
* Manages instances of {@link WebView}
*
* <p>
* Can accept following commands:
* - GO_BACK
* - GO_FORWARD
* - RELOAD
* - LOAD_URL
*
* - GO_BACK
* - GO_FORWARD
* - RELOAD
* - LOAD_URL
* <p>
* {@link WebView} instances could emit following direct events:
* - topLoadingFinish
* - topLoadingStart
* - topLoadingStart
* - topLoadingProgress
* - topShouldStartLoadWithRequest
*
* - topLoadingFinish
* - topLoadingStart
* - topLoadingStart
* - topLoadingProgress
* - topShouldStartLoadWithRequest
* <p>
* Each event will carry the following properties:
* - target - view's react tag
* - url - url set for the webview
* - loading - whether webview is in a loading state
* - title - title of the current page
* - canGoBack - boolean, whether there is anything on a history stack to go back
* - canGoForward - boolean, whether it is possible to request GO_FORWARD command
* - target - view's react tag
* - url - url set for the webview
* - loading - whether webview is in a loading state
* - title - title of the current page
* - canGoBack - boolean, whether there is anything on a history stack to go back
* - canGoForward - boolean, whether it is possible to request GO_FORWARD command
*/
@ReactModule(name = RNCWebViewManager.REACT_CLASS)
public class RNCWebViewManager extends SimpleViewManager<WebView> {
protected static final String REACT_CLASS = "RNCWebView";
private RNCWebViewPackage aPackage;
protected static final String HTML_ENCODING = "UTF-8";
protected static final String HTML_MIME_TYPE = "text/html";
protected static final String JAVASCRIPT_INTERFACE = "ReactNativeWebView";
protected static final String HTTP_METHOD_POST = "POST";
public static final int COMMAND_GO_BACK = 1;
public static final int COMMAND_GO_FORWARD = 2;
public static final int COMMAND_RELOAD = 3;
@ -120,246 +96,16 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
public static final int COMMAND_POST_MESSAGE = 5;
public static final int COMMAND_INJECT_JAVASCRIPT = 6;
public static final int COMMAND_LOAD_URL = 7;
protected static final String REACT_CLASS = "RNCWebView";
protected static final String HTML_ENCODING = "UTF-8";
protected static final String HTML_MIME_TYPE = "text/html";
protected static final String JAVASCRIPT_INTERFACE = "ReactNativeWebView";
protected static final String HTTP_METHOD_POST = "POST";
// Use `webView.loadUrl("about:blank")` to reliably reset the view
// state and release page resources (including any running JavaScript).
protected static final String BLANK_URL = "about:blank";
protected WebViewConfig mWebViewConfig;
protected static class RNCWebViewClient extends WebViewClient {
protected boolean mLastLoadFailed = false;
protected @Nullable ReadableArray mUrlPrefixesForDefaultIntent;
@Override
public void onPageFinished(WebView webView, String url) {
super.onPageFinished(webView, url);
if (!mLastLoadFailed) {
RNCWebView reactWebView = (RNCWebView) webView;
reactWebView.callInjectedJavaScript();
emitFinishEvent(webView, url);
}
}
@Override
public void onPageStarted(WebView webView, String url, Bitmap favicon) {
super.onPageStarted(webView, url, favicon);
mLastLoadFailed = false;
dispatchEvent(
webView,
new TopLoadingStartEvent(
webView.getId(),
createWebViewEvent(webView, url)));
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
dispatchEvent(
view,
new TopShouldStartLoadWithRequestEvent(
view.getId(),
createWebViewEvent(view, url)));
return true;
}
@TargetApi(Build.VERSION_CODES.N)
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
final String url = request.getUrl().toString();
return this.shouldOverrideUrlLoading(view, url);
}
@Override
public void onReceivedError(
WebView webView,
int errorCode,
String description,
String failingUrl) {
super.onReceivedError(webView, errorCode, description, failingUrl);
mLastLoadFailed = true;
// In case of an error JS side expect to get a finish event first, and then get an error event
// Android WebView does it in the opposite way, so we need to simulate that behavior
emitFinishEvent(webView, failingUrl);
WritableMap eventData = createWebViewEvent(webView, failingUrl);
eventData.putDouble("code", errorCode);
eventData.putString("description", description);
dispatchEvent(
webView,
new TopLoadingErrorEvent(webView.getId(), eventData));
}
protected void emitFinishEvent(WebView webView, String url) {
dispatchEvent(
webView,
new TopLoadingFinishEvent(
webView.getId(),
createWebViewEvent(webView, url)));
}
protected WritableMap createWebViewEvent(WebView webView, String url) {
WritableMap event = Arguments.createMap();
event.putDouble("target", webView.getId());
// Don't use webView.getUrl() here, the URL isn't updated to the new value yet in callbacks
// like onPageFinished
event.putString("url", url);
event.putBoolean("loading", !mLastLoadFailed && webView.getProgress() != 100);
event.putString("title", webView.getTitle());
event.putBoolean("canGoBack", webView.canGoBack());
event.putBoolean("canGoForward", webView.canGoForward());
return event;
}
public void setUrlPrefixesForDefaultIntent(ReadableArray specialUrls) {
mUrlPrefixesForDefaultIntent = specialUrls;
}
}
/**
* Subclass of {@link WebView} that implements {@link LifecycleEventListener} interface in order
* to call {@link WebView#destroy} on activity destroy event and also to clear the client
*/
protected static class RNCWebView extends WebView implements LifecycleEventListener {
protected @Nullable String injectedJS;
protected boolean messagingEnabled = false;
protected @Nullable RNCWebViewClient mRNCWebViewClient;
protected boolean sendContentSizeChangeEvents = false;
public void setSendContentSizeChangeEvents(boolean sendContentSizeChangeEvents) {
this.sendContentSizeChangeEvents = sendContentSizeChangeEvents;
}
protected class RNCWebViewBridge {
RNCWebView mContext;
RNCWebViewBridge(RNCWebView c) {
mContext = c;
}
/**
* This method is called whenever JavaScript running within the web view calls:
* - window[JAVASCRIPT_INTERFACE].postMessage
*/
@JavascriptInterface
public void postMessage(String message) {
mContext.onMessage(message);
}
}
/**
* WebView must be created with an context of the current activity
*
* Activity Context is required for creation of dialogs internally by WebView
* Reactive Native needed for access to ReactNative internal system functionality
*
*/
public RNCWebView(ThemedReactContext reactContext) {
super(reactContext);
}
@Override
public void onHostResume() {
// do nothing
}
@Override
public void onHostPause() {
// do nothing
}
@Override
public void onHostDestroy() {
cleanupCallbacksAndDestroy();
}
@Override
protected void onSizeChanged(int w, int h, int ow, int oh) {
super.onSizeChanged(w, h, ow, oh);
if (sendContentSizeChangeEvents) {
dispatchEvent(
this,
new ContentSizeChangeEvent(
this.getId(),
w,
h
)
);
}
}
@Override
public void setWebViewClient(WebViewClient client) {
super.setWebViewClient(client);
mRNCWebViewClient = (RNCWebViewClient)client;
}
public @Nullable RNCWebViewClient getRNCWebViewClient() {
return mRNCWebViewClient;
}
public void setInjectedJavaScript(@Nullable String js) {
injectedJS = js;
}
protected RNCWebViewBridge createRNCWebViewBridge(RNCWebView webView) {
return new RNCWebViewBridge(webView);
}
@SuppressLint("AddJavascriptInterface")
public void setMessagingEnabled(boolean enabled) {
if (messagingEnabled == enabled) {
return;
}
messagingEnabled = enabled;
if (enabled) {
addJavascriptInterface(createRNCWebViewBridge(this), JAVASCRIPT_INTERFACE);
} else {
removeJavascriptInterface(JAVASCRIPT_INTERFACE);
}
}
protected void evaluateJavascriptWithFallback(String script) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
evaluateJavascript(script, null);
return;
}
try {
loadUrl("javascript:" + URLEncoder.encode(script, "UTF-8"));
} catch (UnsupportedEncodingException e) {
// UTF-8 should always be supported
throw new RuntimeException(e);
}
}
public void callInjectedJavaScript() {
if (getSettings().getJavaScriptEnabled() &&
injectedJS != null &&
!TextUtils.isEmpty(injectedJS)) {
evaluateJavascriptWithFallback("(function() {\n" + injectedJS + ";\n})();");
}
}
public void onMessage(String message) {
dispatchEvent(this, new TopMessageEvent(this.getId(), message));
}
protected void cleanupCallbacksAndDestroy() {
setWebViewClient(null);
destroy();
}
}
private RNCWebViewPackage aPackage;
public RNCWebViewManager() {
mWebViewConfig = new WebViewConfig() {
@ -372,6 +118,13 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
mWebViewConfig = webViewConfig;
}
protected static void dispatchEvent(WebView webView, Event event) {
ReactContext reactContext = (ReactContext) webView.getContext();
EventDispatcher eventDispatcher =
reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
eventDispatcher.dispatchEvent(event);
}
@Override
public String getName() {
return REACT_CLASS;
@ -396,21 +149,21 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
}
@Override
public void onProgressChanged(WebView webView, int newProgress) {
@Override
public void onProgressChanged(WebView webView, int newProgress) {
super.onProgressChanged(webView, newProgress);
WritableMap event = Arguments.createMap();
event.putDouble("target", webView.getId());
event.putString("title", webView.getTitle());
event.putBoolean("canGoBack", webView.canGoBack());
event.putBoolean("canGoForward", webView.canGoForward());
event.putDouble("progress", (float)newProgress/100);
event.putDouble("progress", (float) newProgress / 100);
dispatchEvent(
webView,
new TopLoadingProgressEvent(
webView.getId(),
event));
}
webView,
new TopLoadingProgressEvent(
webView.getId(),
event));
}
@Override
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
@ -420,9 +173,11 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
protected void openFileChooser(ValueCallback<Uri> filePathCallback, String acceptType) {
getModule().startPhotoPickerIntent(filePathCallback, acceptType);
}
protected void openFileChooser(ValueCallback<Uri> filePathCallback) {
getModule().startPhotoPickerIntent(filePathCallback, "");
}
protected void openFileChooser(ValueCallback<Uri> filePathCallback, String acceptType, String capture) {
getModule().startPhotoPickerIntent(filePathCallback, acceptType);
}
@ -453,8 +208,8 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
// Fixes broken full-screen modals/galleries due to body height being 0.
webView.setLayoutParams(
new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT));
new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT));
setGeolocationEnabled(webView, false);
if (ReactBuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
@ -516,7 +271,7 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
public void setShowsVerticalScrollIndicator(WebView view, boolean enabled) {
view.setVerticalScrollBarEnabled(enabled);
}
@ReactProp(name = "cacheEnabled")
public void setCacheEnabled(WebView view, boolean enabled) {
if (enabled) {
@ -616,7 +371,7 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
String html = source.getString("html");
if (source.hasKey("baseUrl")) {
view.loadDataWithBaseURL(
source.getString("baseUrl"), html, HTML_MIME_TYPE, HTML_ENCODING, null);
source.getString("baseUrl"), html, HTML_MIME_TYPE, HTML_ENCODING, null);
} else {
view.loadData(html, HTML_MIME_TYPE + "; charset=" + HTML_ENCODING, null);
}
@ -689,8 +444,8 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
@ReactProp(name = "urlPrefixesForDefaultIntent")
public void setUrlPrefixesForDefaultIntent(
WebView view,
@Nullable ReadableArray urlPrefixesForDefaultIntent) {
WebView view,
@Nullable ReadableArray urlPrefixesForDefaultIntent) {
RNCWebViewClient client = ((RNCWebView) view).getRNCWebViewClient();
if (client != null && urlPrefixesForDefaultIntent != null) {
client.setUrlPrefixesForDefaultIntent(urlPrefixesForDefaultIntent);
@ -729,16 +484,17 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
}
@Override
public @Nullable Map<String, Integer> getCommandsMap() {
public @Nullable
Map<String, Integer> getCommandsMap() {
return MapBuilder.of(
"goBack", COMMAND_GO_BACK,
"goForward", COMMAND_GO_FORWARD,
"reload", COMMAND_RELOAD,
"stopLoading", COMMAND_STOP_LOADING,
"postMessage", COMMAND_POST_MESSAGE,
"injectJavaScript", COMMAND_INJECT_JAVASCRIPT,
"loadUrl", COMMAND_LOAD_URL
);
"goBack", COMMAND_GO_BACK,
"goForward", COMMAND_GO_FORWARD,
"reload", COMMAND_RELOAD,
"stopLoading", COMMAND_STOP_LOADING,
"postMessage", COMMAND_POST_MESSAGE,
"injectJavaScript", COMMAND_INJECT_JAVASCRIPT,
"loadUrl", COMMAND_LOAD_URL
);
}
@Override
@ -765,13 +521,13 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
"var event;" +
"var data = " + eventInitDict.toString() + ";" +
"try {" +
"event = new MessageEvent('message', data);" +
"event = new MessageEvent('message', data);" +
"} catch (e) {" +
"event = document.createEvent('MessageEvent');" +
"event.initMessageEvent('message', true, true, data.data, data.origin, data.lastEventId, data.source);" +
"event = document.createEvent('MessageEvent');" +
"event.initMessageEvent('message', true, true, data.data, data.origin, data.lastEventId, data.source);" +
"}" +
"document.dispatchEvent(event);" +
"})();");
"})();");
} catch (JSONException e) {
throw new RuntimeException(e);
}
@ -796,13 +552,6 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
((RNCWebView) webView).cleanupCallbacksAndDestroy();
}
protected static void dispatchEvent(WebView webView, Event event) {
ReactContext reactContext = (ReactContext) webView.getContext();
EventDispatcher eventDispatcher =
reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
eventDispatcher.dispatchEvent(event);
}
public RNCWebViewPackage getPackage() {
return this.aPackage;
}
@ -814,4 +563,241 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
public RNCWebViewModule getModule() {
return this.aPackage.getModule();
}
protected static class RNCWebViewClient extends WebViewClient {
protected boolean mLastLoadFailed = false;
protected @Nullable
ReadableArray mUrlPrefixesForDefaultIntent;
@Override
public void onPageFinished(WebView webView, String url) {
super.onPageFinished(webView, url);
if (!mLastLoadFailed) {
RNCWebView reactWebView = (RNCWebView) webView;
reactWebView.callInjectedJavaScript();
emitFinishEvent(webView, url);
}
}
@Override
public void onPageStarted(WebView webView, String url, Bitmap favicon) {
super.onPageStarted(webView, url, favicon);
mLastLoadFailed = false;
dispatchEvent(
webView,
new TopLoadingStartEvent(
webView.getId(),
createWebViewEvent(webView, url)));
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
dispatchEvent(
view,
new TopShouldStartLoadWithRequestEvent(
view.getId(),
createWebViewEvent(view, url)));
return true;
}
@TargetApi(Build.VERSION_CODES.N)
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
final String url = request.getUrl().toString();
return this.shouldOverrideUrlLoading(view, url);
}
@Override
public void onReceivedError(
WebView webView,
int errorCode,
String description,
String failingUrl) {
super.onReceivedError(webView, errorCode, description, failingUrl);
mLastLoadFailed = true;
// In case of an error JS side expect to get a finish event first, and then get an error event
// Android WebView does it in the opposite way, so we need to simulate that behavior
emitFinishEvent(webView, failingUrl);
WritableMap eventData = createWebViewEvent(webView, failingUrl);
eventData.putDouble("code", errorCode);
eventData.putString("description", description);
dispatchEvent(
webView,
new TopLoadingErrorEvent(webView.getId(), eventData));
}
protected void emitFinishEvent(WebView webView, String url) {
dispatchEvent(
webView,
new TopLoadingFinishEvent(
webView.getId(),
createWebViewEvent(webView, url)));
}
protected WritableMap createWebViewEvent(WebView webView, String url) {
WritableMap event = Arguments.createMap();
event.putDouble("target", webView.getId());
// Don't use webView.getUrl() here, the URL isn't updated to the new value yet in callbacks
// like onPageFinished
event.putString("url", url);
event.putBoolean("loading", !mLastLoadFailed && webView.getProgress() != 100);
event.putString("title", webView.getTitle());
event.putBoolean("canGoBack", webView.canGoBack());
event.putBoolean("canGoForward", webView.canGoForward());
return event;
}
public void setUrlPrefixesForDefaultIntent(ReadableArray specialUrls) {
mUrlPrefixesForDefaultIntent = specialUrls;
}
}
/**
* Subclass of {@link WebView} that implements {@link LifecycleEventListener} interface in order
* to call {@link WebView#destroy} on activity destroy event and also to clear the client
*/
protected static class RNCWebView extends WebView implements LifecycleEventListener {
protected @Nullable
String injectedJS;
protected boolean messagingEnabled = false;
protected @Nullable
RNCWebViewClient mRNCWebViewClient;
protected boolean sendContentSizeChangeEvents = false;
/**
* WebView must be created with an context of the current activity
* <p>
* Activity Context is required for creation of dialogs internally by WebView
* Reactive Native needed for access to ReactNative internal system functionality
*/
public RNCWebView(ThemedReactContext reactContext) {
super(reactContext);
}
public void setSendContentSizeChangeEvents(boolean sendContentSizeChangeEvents) {
this.sendContentSizeChangeEvents = sendContentSizeChangeEvents;
}
@Override
public void onHostResume() {
// do nothing
}
@Override
public void onHostPause() {
// do nothing
}
@Override
public void onHostDestroy() {
cleanupCallbacksAndDestroy();
}
@Override
protected void onSizeChanged(int w, int h, int ow, int oh) {
super.onSizeChanged(w, h, ow, oh);
if (sendContentSizeChangeEvents) {
dispatchEvent(
this,
new ContentSizeChangeEvent(
this.getId(),
w,
h
)
);
}
}
@Override
public void setWebViewClient(WebViewClient client) {
super.setWebViewClient(client);
mRNCWebViewClient = (RNCWebViewClient) client;
}
public @Nullable
RNCWebViewClient getRNCWebViewClient() {
return mRNCWebViewClient;
}
public void setInjectedJavaScript(@Nullable String js) {
injectedJS = js;
}
protected RNCWebViewBridge createRNCWebViewBridge(RNCWebView webView) {
return new RNCWebViewBridge(webView);
}
@SuppressLint("AddJavascriptInterface")
public void setMessagingEnabled(boolean enabled) {
if (messagingEnabled == enabled) {
return;
}
messagingEnabled = enabled;
if (enabled) {
addJavascriptInterface(createRNCWebViewBridge(this), JAVASCRIPT_INTERFACE);
} else {
removeJavascriptInterface(JAVASCRIPT_INTERFACE);
}
}
protected void evaluateJavascriptWithFallback(String script) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
evaluateJavascript(script, null);
return;
}
try {
loadUrl("javascript:" + URLEncoder.encode(script, "UTF-8"));
} catch (UnsupportedEncodingException e) {
// UTF-8 should always be supported
throw new RuntimeException(e);
}
}
public void callInjectedJavaScript() {
if (getSettings().getJavaScriptEnabled() &&
injectedJS != null &&
!TextUtils.isEmpty(injectedJS)) {
evaluateJavascriptWithFallback("(function() {\n" + injectedJS + ";\n})();");
}
}
public void onMessage(String message) {
dispatchEvent(this, new TopMessageEvent(this.getId(), message));
}
protected void cleanupCallbacksAndDestroy() {
setWebViewClient(null);
destroy();
}
protected class RNCWebViewBridge {
RNCWebView mContext;
RNCWebViewBridge(RNCWebView c) {
mContext = c;
}
/**
* This method is called whenever JavaScript running within the web view calls:
* - window[JAVASCRIPT_INTERFACE].postMessage
*/
@JavascriptInterface
public void postMessage(String message) {
mContext.onMessage(message);
}
}
}
}

View File

@ -1,4 +1,3 @@
package com.reactnativecommunity.webview;
import android.Manifest;
@ -37,20 +36,35 @@ import static android.app.Activity.RESULT_OK;
public class RNCWebViewModule extends ReactContextBaseJavaModule implements ActivityEventListener {
private final ReactApplicationContext reactContext;
private RNCWebViewPackage aPackage;
private static final int PICKER = 1;
private static final int PICKER_LEGACY = 3;
private static final int FILE_DOWNLOAD_PERMISSION_REQUEST = 1;
final String DEFAULT_MIME_TYPES = "*/*";
private final ReactApplicationContext reactContext;
private RNCWebViewPackage aPackage;
private ValueCallback<Uri> filePathCallbackLegacy;
private ValueCallback<Uri[]> filePathCallback;
private Uri outputFileUri;
private DownloadManager.Request downloadRequest;
private static final int FILE_DOWNLOAD_PERMISSION_REQUEST = 1;
final String DEFAULT_MIME_TYPES = "*/*";
private PermissionListener webviewFileDownloaderPermissionListener = new PermissionListener() {
@Override
public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case FILE_DOWNLOAD_PERMISSION_REQUEST: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (downloadRequest != null) {
downloadFile();
}
} else {
Toast.makeText(getCurrentActivity().getApplicationContext(), "Cannot download files as permission was denied. Please provide permission to write to storage, in order to download files.", Toast.LENGTH_LONG).show();
}
return true;
}
}
return false;
}
};
public RNCWebViewModule(ReactApplicationContext reactContext) {
super(reactContext);
@ -65,49 +79,49 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
@ReactMethod
public void isFileUploadSupported(final Promise promise) {
Boolean result = false;
int current = Build.VERSION.SDK_INT;
if (current >= Build.VERSION_CODES.LOLLIPOP) {
result = true;
}
if (current >= Build.VERSION_CODES.JELLY_BEAN && current <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
result = true;
}
promise.resolve(result);
Boolean result = false;
int current = Build.VERSION.SDK_INT;
if (current >= Build.VERSION_CODES.LOLLIPOP) {
result = true;
}
if (current >= Build.VERSION_CODES.JELLY_BEAN && current <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
result = true;
}
promise.resolve(result);
}
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
if (filePathCallback == null && filePathCallbackLegacy == null) {
return;
return;
}
// based off of which button was pressed, we get an activity result and a file
// the camera activity doesn't properly return the filename* (I think?) so we use
// this filename instead
switch (requestCode) {
case PICKER:
case PICKER:
if (resultCode != RESULT_OK) {
if (filePathCallback != null) {
filePathCallback.onReceiveValue(null);
}
if (filePathCallback != null) {
filePathCallback.onReceiveValue(null);
}
} else {
Uri result[] = this.getSelectedFiles(data, resultCode);
if (result != null) {
filePathCallback.onReceiveValue(result);
} else {
filePathCallback.onReceiveValue(new Uri[] { outputFileUri });
}
Uri result[] = this.getSelectedFiles(data, resultCode);
if (result != null) {
filePathCallback.onReceiveValue(result);
} else {
filePathCallback.onReceiveValue(new Uri[]{outputFileUri});
}
}
break;
case PICKER_LEGACY:
case PICKER_LEGACY:
Uri result = resultCode != Activity.RESULT_OK ? null : data == null ? outputFileUri : data.getData();
filePathCallbackLegacy.onReceiveValue(result);
break;
}
filePathCallback = null;
filePathCallbackLegacy= null;
filePathCallbackLegacy = null;
outputFileUri = null;
}
@ -116,50 +130,50 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
private Uri[] getSelectedFiles(Intent data, int resultCode) {
if (data == null) {
return null;
return null;
}
// we have one file selected
if (data.getData() != null) {
if (resultCode == RESULT_OK && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return WebChromeClient.FileChooserParams.parseResult(resultCode, data);
} else {
return null;
}
if (resultCode == RESULT_OK && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return WebChromeClient.FileChooserParams.parseResult(resultCode, data);
} else {
return null;
}
}
// we have multiple files selected
if (data.getClipData() != null) {
final int numSelectedFiles = data.getClipData().getItemCount();
Uri[] result = new Uri[numSelectedFiles];
for (int i = 0; i < numSelectedFiles; i++) {
result[i] = data.getClipData().getItemAt(i).getUri();
}
return result;
final int numSelectedFiles = data.getClipData().getItemCount();
Uri[] result = new Uri[numSelectedFiles];
for (int i = 0; i < numSelectedFiles; i++) {
result[i] = data.getClipData().getItemAt(i).getUri();
}
return result;
}
return null;
}
public void startPhotoPickerIntent(ValueCallback<Uri> filePathCallback, String acceptType) {
filePathCallbackLegacy = filePathCallback;
filePathCallbackLegacy = filePathCallback;
Intent fileChooserIntent = getFileChooserIntent(acceptType);
Intent chooserIntent = Intent.createChooser(fileChooserIntent, "");
Intent fileChooserIntent = getFileChooserIntent(acceptType);
Intent chooserIntent = Intent.createChooser(fileChooserIntent, "");
ArrayList<Parcelable> extraIntents = new ArrayList<>();
if (acceptsImages(acceptType)) {
extraIntents.add(getPhotoIntent());
}
if (acceptsVideo(acceptType)) {
extraIntents.add(getVideoIntent());
}
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{}));
ArrayList<Parcelable> extraIntents = new ArrayList<>();
if (acceptsImages(acceptType)) {
extraIntents.add(getPhotoIntent());
}
if (acceptsVideo(acceptType)) {
extraIntents.add(getVideoIntent());
}
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{}));
if (chooserIntent.resolveActivity(getCurrentActivity().getPackageManager()) != null) {
getCurrentActivity().startActivityForResult(chooserIntent, PICKER_LEGACY);
} else {
Log.w("RNCWebViewModule", "there is no Activity to handle this Intent");
}
if (chooserIntent.resolveActivity(getCurrentActivity().getPackageManager()) != null) {
getCurrentActivity().startActivityForResult(chooserIntent, PICKER_LEGACY);
} else {
Log.w("RNCWebViewModule", "there is no Activity to handle this Intent");
}
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@ -181,9 +195,9 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{}));
if (chooserIntent.resolveActivity(getCurrentActivity().getPackageManager()) != null) {
getCurrentActivity().startActivityForResult(chooserIntent, PICKER);
getCurrentActivity().startActivityForResult(chooserIntent, PICKER);
} else {
Log.w("RNCWebViewModule", "there is no Activity to handle this Intent");
Log.w("RNCWebViewModule", "there is no Activity to handle this Intent");
}
return true;
@ -214,7 +228,7 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
if (!result) {
PermissionAwareActivity activity = getPermissionAwareActivity();
activity.requestPermissions(new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE }, FILE_DOWNLOAD_PERMISSION_REQUEST, webviewFileDownloaderPermissionListener);
activity.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, FILE_DOWNLOAD_PERMISSION_REQUEST, webviewFileDownloaderPermissionListener);
}
return result;
@ -270,10 +284,11 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
private Boolean acceptsImages(String types) {
String mimeType = types;
if (types.matches("\\.\\w+")) {
mimeType = getMimeTypeFromExtension(types.replace(".", ""));
mimeType = getMimeTypeFromExtension(types.replace(".", ""));
}
return mimeType.isEmpty() || mimeType.toLowerCase().contains("image");
}
private Boolean acceptsImages(String[] types) {
String[] mimeTypes = getAcceptedMimeType(types);
return isArrayEmpty(mimeTypes) || arrayContainsString(mimeTypes, "image");
@ -282,38 +297,39 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
private Boolean acceptsVideo(String types) {
String mimeType = types;
if (types.matches("\\.\\w+")) {
mimeType = getMimeTypeFromExtension(types.replace(".", ""));
mimeType = getMimeTypeFromExtension(types.replace(".", ""));
}
return mimeType.isEmpty() || mimeType.toLowerCase().contains("video");
}
private Boolean acceptsVideo(String[] types) {
String[] mimeTypes = getAcceptedMimeType(types);
return isArrayEmpty(mimeTypes) || arrayContainsString(mimeTypes, "video");
}
private Boolean arrayContainsString(String[] array, String pattern){
for(String content : array){
if(content.contains(pattern)){
return true;
}
private Boolean arrayContainsString(String[] array, String pattern) {
for (String content : array) {
if (content.contains(pattern)) {
return true;
}
}
return false;
}
private String[] getAcceptedMimeType(String[] types) {
if (isArrayEmpty(types)) {
return new String[]{DEFAULT_MIME_TYPES};
return new String[]{DEFAULT_MIME_TYPES};
}
String[] mimeTypes = new String[types.length];
for (int i = 0; i < types.length; i++) {
String t = types[i];
// convert file extensions to mime types
if (t.matches("\\.\\w+")) {
String mimeType = getMimeTypeFromExtension(t.replace(".", ""));
mimeTypes[i] = mimeType;
} else {
mimeTypes[i] = t;
}
String t = types[i];
// convert file extensions to mime types
if (t.matches("\\.\\w+")) {
String mimeType = getMimeTypeFromExtension(t.replace(".", ""));
mimeTypes[i] = mimeType;
} else {
mimeTypes[i] = t;
}
}
return mimeTypes;
}
@ -321,7 +337,7 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
private String getMimeTypeFromExtension(String extension) {
String type = null;
if (extension != null) {
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
}
return type;
}
@ -329,20 +345,20 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
private Uri getOutputUri(String intentType) {
File capturedFile = null;
try {
capturedFile = getCapturedFile(intentType);
capturedFile = getCapturedFile(intentType);
} catch (IOException e) {
Log.e("CREATE FILE", "Error occurred while creating the File", e);
e.printStackTrace();
Log.e("CREATE FILE", "Error occurred while creating the File", e);
e.printStackTrace();
}
// for versions below 6.0 (23) we use the old File creation & permissions model
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return Uri.fromFile(capturedFile);
return Uri.fromFile(capturedFile);
}
// for versions 6.0+ (23) we use the FileProvider to avoid runtime permissions
String packageName = getReactApplicationContext().getPackageName();
return FileProvider.getUriForFile(getReactApplicationContext(), packageName+".fileprovider", capturedFile);
return FileProvider.getUriForFile(getReactApplicationContext(), packageName + ".fileprovider", capturedFile);
}
private File getCapturedFile(String intentType) throws IOException {
@ -365,10 +381,10 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
// for versions below 6.0 (23) we use the old File creation & permissions model
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
// only this Directory works on all tested Android versions
// ctx.getExternalFilesDir(dir) was failing on Android 5.0 (sdk 21)
File storageDir = Environment.getExternalStoragePublicDirectory(dir);
return new File(storageDir, filename);
// only this Directory works on all tested Android versions
// ctx.getExternalFilesDir(dir) was failing on Android 5.0 (sdk 21)
File storageDir = Environment.getExternalStoragePublicDirectory(dir);
return new File(storageDir, filename);
}
File storageDir = getReactApplicationContext().getExternalFilesDir(null);
@ -385,30 +401,10 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
private PermissionAwareActivity getPermissionAwareActivity() {
Activity activity = getCurrentActivity();
if (activity == null) {
throw new IllegalStateException("Tried to use permissions API while not attached to an Activity.");
throw new IllegalStateException("Tried to use permissions API while not attached to an Activity.");
} else if (!(activity instanceof PermissionAwareActivity)) {
throw new IllegalStateException("Tried to use permissions API but the host Activity doesn't implement PermissionAwareActivity.");
throw new IllegalStateException("Tried to use permissions API but the host Activity doesn't implement PermissionAwareActivity.");
}
return (PermissionAwareActivity) activity;
}
private PermissionListener webviewFileDownloaderPermissionListener = new PermissionListener() {
@Override
public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case FILE_DOWNLOAD_PERMISSION_REQUEST: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (downloadRequest != null) {
downloadFile();
}
} else {
Toast.makeText(getCurrentActivity().getApplicationContext(), "Cannot download files as permission was denied. Please provide permission to write to storage, in order to download files.", Toast.LENGTH_LONG).show();
}
return true;
}
}
return false;
}
};
}

View File

@ -1,44 +1,43 @@
package com.reactnativecommunity.webview;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.bridge.JavaScriptModule;
public class RNCWebViewPackage implements ReactPackage {
private RNCWebViewManager manager;
private RNCWebViewModule module;
private RNCWebViewManager manager;
private RNCWebViewModule module;
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modulesList = new ArrayList<>();
module = new RNCWebViewModule(reactContext);
module.setPackage(this);
modulesList.add(module);
return modulesList;
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modulesList = new ArrayList<>();
module = new RNCWebViewModule(reactContext);
module.setPackage(this);
modulesList.add(module);
return modulesList;
}
// Deprecated from RN 0.47
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
// Deprecated from RN 0.47
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
manager = new RNCWebViewManager();
manager.setPackage(this);
return Arrays.<ViewManager>asList(manager);
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
manager = new RNCWebViewManager();
manager.setPackage(this);
return Arrays.<ViewManager>asList(manager);
}
public RNCWebViewModule getModule() {
return module;
}
public RNCWebViewModule getModule() {
return module;
}
}

View File

@ -8,18 +8,18 @@ import com.facebook.react.uimanager.events.RCTEventEmitter
* Event emitted when there is an error in loading.
*/
class TopLoadingErrorEvent(viewId: Int, private val mEventData: WritableMap) :
Event<TopLoadingErrorEvent>(viewId) {
companion object {
const val EVENT_NAME = "topLoadingError"
}
Event<TopLoadingErrorEvent>(viewId) {
companion object {
const val EVENT_NAME = "topLoadingError"
}
override fun getEventName(): String = EVENT_NAME
override fun getEventName(): String = EVENT_NAME
override fun canCoalesce(): Boolean = false
override fun canCoalesce(): Boolean = false
override fun getCoalescingKey(): Short = 0
override fun getCoalescingKey(): Short = 0
override fun dispatch(rctEventEmitter: RCTEventEmitter) =
rctEventEmitter.receiveEvent(viewTag, eventName, mEventData)
override fun dispatch(rctEventEmitter: RCTEventEmitter) =
rctEventEmitter.receiveEvent(viewTag, eventName, mEventData)
}

View File

@ -8,17 +8,17 @@ import com.facebook.react.uimanager.events.RCTEventEmitter
* Event emitted when loading is completed.
*/
class TopLoadingFinishEvent(viewId: Int, private val mEventData: WritableMap) :
Event<TopLoadingFinishEvent>(viewId) {
companion object {
const val EVENT_NAME = "topLoadingFinish"
}
Event<TopLoadingFinishEvent>(viewId) {
companion object {
const val EVENT_NAME = "topLoadingFinish"
}
override fun getEventName(): String = EVENT_NAME
override fun getEventName(): String = EVENT_NAME
override fun canCoalesce(): Boolean = false
override fun canCoalesce(): Boolean = false
override fun getCoalescingKey(): Short = 0
override fun getCoalescingKey(): Short = 0
override fun dispatch(rctEventEmitter: RCTEventEmitter) =
rctEventEmitter.receiveEvent(viewTag, eventName, mEventData)
override fun dispatch(rctEventEmitter: RCTEventEmitter) =
rctEventEmitter.receiveEvent(viewTag, eventName, mEventData)
}

View File

@ -8,17 +8,17 @@ import com.facebook.react.uimanager.events.RCTEventEmitter
* Event emitted when there is a loading progress event.
*/
class TopLoadingProgressEvent(viewId: Int, private val mEventData: WritableMap) :
Event<TopLoadingProgressEvent>(viewId) {
companion object {
const val EVENT_NAME = "topLoadingProgress"
}
Event<TopLoadingProgressEvent>(viewId) {
companion object {
const val EVENT_NAME = "topLoadingProgress"
}
override fun getEventName(): String = EVENT_NAME
override fun getEventName(): String = EVENT_NAME
override fun canCoalesce(): Boolean = false
override fun canCoalesce(): Boolean = false
override fun getCoalescingKey(): Short = 0
override fun getCoalescingKey(): Short = 0
override fun dispatch(rctEventEmitter: RCTEventEmitter) =
rctEventEmitter.receiveEvent(viewTag, eventName, mEventData)
override fun dispatch(rctEventEmitter: RCTEventEmitter) =
rctEventEmitter.receiveEvent(viewTag, eventName, mEventData)
}

View File

@ -8,18 +8,18 @@ import com.facebook.react.uimanager.events.RCTEventEmitter
* Event emitted when loading has started
*/
class TopLoadingStartEvent(viewId: Int, private val mEventData: WritableMap) :
Event<TopLoadingStartEvent>(viewId) {
companion object {
const val EVENT_NAME = "topLoadingStart"
}
Event<TopLoadingStartEvent>(viewId) {
companion object {
const val EVENT_NAME = "topLoadingStart"
}
override fun getEventName(): String = EVENT_NAME
override fun getEventName(): String = EVENT_NAME
override fun canCoalesce(): Boolean = false
override fun canCoalesce(): Boolean = false
override fun getCoalescingKey(): Short = 0
override fun getCoalescingKey(): Short = 0
override fun dispatch(rctEventEmitter: RCTEventEmitter) =
rctEventEmitter.receiveEvent(viewTag, eventName, mEventData)
override fun dispatch(rctEventEmitter: RCTEventEmitter) =
rctEventEmitter.receiveEvent(viewTag, eventName, mEventData)
}

View File

@ -8,19 +8,19 @@ import com.facebook.react.uimanager.events.RCTEventEmitter
* Event emitted when there is an error in loading.
*/
class TopMessageEvent(viewId: Int, private val mData: String) : Event<TopMessageEvent>(viewId) {
companion object {
const val EVENT_NAME = "topMessage"
}
companion object {
const val EVENT_NAME = "topMessage"
}
override fun getEventName(): String = EVENT_NAME
override fun getEventName(): String = EVENT_NAME
override fun canCoalesce(): Boolean = false
override fun canCoalesce(): Boolean = false
override fun getCoalescingKey(): Short = 0
override fun getCoalescingKey(): Short = 0
override fun dispatch(rctEventEmitter: RCTEventEmitter) {
val data = Arguments.createMap()
data.putString("data", mData)
rctEventEmitter.receiveEvent(viewTag, EVENT_NAME, data)
}
override fun dispatch(rctEventEmitter: RCTEventEmitter) {
val data = Arguments.createMap()
data.putString("data", mData)
rctEventEmitter.receiveEvent(viewTag, EVENT_NAME, data)
}
}

View File

@ -8,20 +8,20 @@ import com.facebook.react.uimanager.events.RCTEventEmitter
* Event emitted when shouldOverrideUrlLoading is called
*/
class TopShouldStartLoadWithRequestEvent(viewId: Int, private val mData: WritableMap) : Event<TopShouldStartLoadWithRequestEvent>(viewId) {
companion object {
const val EVENT_NAME = "topShouldStartLoadWithRequest"
}
companion object {
const val EVENT_NAME = "topShouldStartLoadWithRequest"
}
init {
mData.putString("navigationType", "other")
}
init {
mData.putString("navigationType", "other")
}
override fun getEventName(): String = EVENT_NAME
override fun getEventName(): String = EVENT_NAME
override fun canCoalesce(): Boolean = false
override fun canCoalesce(): Boolean = false
override fun getCoalescingKey(): Short = 0
override fun getCoalescingKey(): Short = 0
override fun dispatch(rctEventEmitter: RCTEventEmitter) =
rctEventEmitter.receiveEvent(viewTag, EVENT_NAME, mData)
override fun dispatch(rctEventEmitter: RCTEventEmitter) =
rctEventEmitter.receiveEvent(viewTag, EVENT_NAME, mData)
}

View File

@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="shared" path="." />
<paths>
<external-path
name="shared"
path="." />
</paths>