I have build a hybrid-app using flutter's web_view_plugin(webview).
One of our paying methods require to open a 3rd party app(in this case kakaotalk). But the flutter webview plugin does not provide this function and returned net::ERR_UNKNOWN_URL_SCHEME. I did some research and I understand that the problem lies in the url. If the url does not start with http or https, it will cause this error.
So, to solve this problem I had to change native java code. Now I have no experience at all with java and android, so fixing the native code was very difficult. I understand that I have to modify shouldOverrideUrlLoading part, in order to allow the url that starts with intent:// and also I have to put in some validation to check if the app is installed or not.(If not installed the user should be redirected to playstore)
The code which I added is in shouldOverrideUrlLoading.
I also did three imports. The rest is code, generated by flutter
package com.flutter_webview_plugin;
import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.os.Build;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.content.Intent; //added import
import android.net.Uri; //added import
import android.content.ActivityNotFoundException; //added import
/**
* Created by lejard_h on 20/12/2017.
*/
public class BrowserClient extends WebViewClient {
private Pattern invalidUrlPattern = null;
public BrowserClient() {
this(null);
}
public BrowserClient(String invalidUrlRegex) {
super();
if (invalidUrlRegex != null) {
invalidUrlPattern = Pattern.compile(invalidUrlRegex);
}
}
public void updateInvalidUrlRegex(String invalidUrlRegex) {
if (invalidUrlRegex != null) {
invalidUrlPattern = Pattern.compile(invalidUrlRegex);
} else {
invalidUrlPattern = null;
}
}
#Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
Map<String, Object> data = new HashMap<>();
data.put("url", url);
data.put("type", "startLoad");
FlutterWebviewPlugin.channel.invokeMethod("onState", data);
}
#Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
Map<String, Object> data = new HashMap<>();
data.put("url", url);
FlutterWebviewPlugin.channel.invokeMethod("onUrlChanged", data);
data.put("type", "finishLoad");
FlutterWebviewPlugin.channel.invokeMethod("onState", data);
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
#Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
// returning true causes the current WebView to abort loading the URL,
// while returning false causes the WebView to continue loading the URL as usual.
String url = request.getUrl().toString();
boolean isInvalid = checkInvalidUrl(url);
Map<String, Object> data = new HashMap<>();
data.put("url", url);
data.put("type", isInvalid ? "abortLoad" : "shouldStart");
FlutterWebviewPlugin.channel.invokeMethod("onState", data);
return isInvalid;
}
#Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
// returning true causes the current WebView to abort loading the URL,
// while returning false causes the WebView to continue loading the URL as usual.
if (url.startsWith(INTENT_PROTOCOL_START)) {
final int customUrlStartIndex = INTENT_PROTOCOL_START.length();
final int customUrlEndIndex = url.indexOf(INTENT_PROTOCOL_INTENT);
if (customUrlEndIndex < 0) {
return false;
} else {
final String customUrl = url.substring(customUrlStartIndex, customUrlEndIndex);
try {
view.getContext().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(customUrl)));
} catch (ActivityNotFoundException e) {
final int packageStartIndex = customUrlEndIndex + INTENT_PROTOCOL_INTENT.length();
final int packageEndIndex = url.indexOf(INTENT_PROTOCOL_END);
final String packageName = url.substring(packageStartIndex, packageEndIndex < 0 ? url.length() : packageEndIndex);
view.getContext().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(GOOGLE_PLAY_STORE_PREFIX + packageName)));
}
return true;
}
} else {
return false;
}
// boolean isInvalid = checkInvalidUrl(url);
// Map<String, Object> data = new HashMap<>();
// data.put("url", url);
// data.put("type", isInvalid ? "abortLoad" : "shouldStart");
// FlutterWebviewPlugin.channel.invokeMethod("onState", data);
// return isInvalid;
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
#Override
public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
super.onReceivedHttpError(view, request, errorResponse);
Map<String, Object> data = new HashMap<>();
data.put("url", request.getUrl().toString());
data.put("code", Integer.toString(errorResponse.getStatusCode()));
FlutterWebviewPlugin.channel.invokeMethod("onHttpError", data);
}
#Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);
Map<String, Object> data = new HashMap<>();
data.put("url", failingUrl);
data.put("code", errorCode);
FlutterWebviewPlugin.channel.invokeMethod("onHttpError", data);
}
private boolean checkInvalidUrl(String url) {
if (invalidUrlPattern == null) {
return false;
} else {
Matcher matcher = invalidUrlPattern.matcher(url);
return matcher.lookingAt();
}
}
}
The code compiles, but it still returns the same error net::ERR_UNKNOWN_URL_SCHEME when I try to pay with the "3rd party app(kakaotalk)"
I encountered a similar error on Android before when Firebase Dynamic Links are being forced to be loaded in a WebView. In my case, FDL is expected to be handled by Google Play Services in Android. But since the WebView doesn't know what to do with the link it's forced to display, the WebView returns "net::ERR_UNKNOWN_URL_SCHEME" error. I'm unsure if this is the same case as yours since I'm unable to verify the link that you're trying to load apart from "intent://kakaopay..."
You can try opening the link externally by using url_launcher. Use RegEx to filter intent URLs and check if the URL can be launched and be handled externally (outside the app).
var yourURL = "URL goes here";
// Check if URL contains "intent"
yourURL.contains(RegExp('^intent://.*\$')){
// Check if the URL can be launched
if (await canLaunch(yourURL)) {
await launch(yourURL);
} else {
print('Could not launch $yourURL');
}
}
Also, the plugin (web_view_plugin) that you're using seems to be outdated, and I can't find it here https://pub.dev/packages?q=web_view_plugin. Flutter has its official WebView plugin (webview_flutter) that has been released and I suggest checking it out if it fits your use case.
Listen, In some cases using #omatt's answer might not work, especially for webview_flutter. I struggled to find a solution so I did this:
_launchURL(url) async {
var link = "https://hiddenwords.page.link/deposit";
if (await canLaunch(link)) {
await launch(link,
forceWebView: false, enableJavaScript: true, forceSafariVC:
false);
} else {
throw 'Could not launch $link';
}
}
I manually put the url/link I wanted it to open in the _launch function... Dont mind the url in the _launch parenthesis.
I also added this to the Webview widget:
navigationDelegate: (NavigationRequest request) {
if (request.url.contains(RegExp('^intent://.*\$'))) {
_launchURL(request.url);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
Hope this works for you. It works for me...
1.Use your APP open other app in flutter with parameter(in your dynamic link);
2.use: url_launcher: ^6.1.6;
First, their app must support dynamic link;
Second, they provide you with dynamic links for their deals;
In this way, we can click the dynamic link in your APP and jump to the specified page of their APP.
Code:
final Uri toLaunch = Uri(scheme: 'https', host: 'link.fitstop.com', path: 'link/qbvQ/');
//https://link.fitstop.com/link/qbvQ is dynamic link
Future<void>? _launched;
ElevatedButton(
onPressed: () => setState(() {
_launched = _launchInBrowser(toLaunch);
}),
child: Text(
'url_launcher',
),
)
Future<void> _launchInBrowser(Uri url) async {
if (!await launchUrl(
url,
mode: LaunchMode.externalApplication,
)) {
throw 'Could not launch $url';
}
}
Related
I have saved a persistent anchor (for 365 days) on the cloud. Now, I want to retrieve it. I can do it just fine using the code Google provided in one of its sample projects. However, I want to use Sceneform since I want to do some manipulations afterwards (drawing 3D shapes), that are much easier to do in Sceneform. However, I can't seem to resolve the persistent cloud anchors. All the examples I find online, don't deal with persistent cloud anchors and they only deal with the normal 24 hour cloud anchors.
#RequiresApi(api = VERSION_CODES.N)
protected void onUpdateFrame(FrameTime frameTime) {
Frame frame = arFragment.getArSceneView().getArFrame();
// If there is no frame, just return.
if (frame == null) {
return;
}
if (session == null) {
Log.d(TAG, "setup a session once");
session = arFragment.getArSceneView().getSession();
cloudAnchorManager = new CloudAnchorManager(session);
}
if (resolveListener == null && session != null) {
Log.d(TAG, "setup a resolveListener once");
resolveListener = new MemexViewingActivity.ResolveListener();
// Encourage the user to look at a previously mapped area.
if (cloudAnchorId != null && !gotGoodAnchor && cloudAnchorManager != null) {
Log.d(TAG, "put resolveListener on cloud manager once");
userMessageText.setText(R.string.resolving_processing);
cloudAnchorManager.resolveCloudAnchor(cloudAnchorId, resolveListener);
}
}
if (cloudAnchorManager != null && session != null) {
try {
Frame dummy = session.update();
cloudAnchorManager.onUpdate();
} catch (CameraNotAvailableException e) {
e.printStackTrace();
}
}
}
Is there anything wrong in the above update function that I have written? The CloudAnchorManager class is the same one Google uses in its Persistent Cloud Anchor example. Here, I will put its code too:
package com.memex.eu.helpers;
import android.util.Log;
import com.google.ar.core.Anchor;
import com.google.ar.core.Anchor.CloudAnchorState;
import com.google.ar.core.Session;
import com.google.common.base.Preconditions;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* A helper class to handle all the Cloud Anchors logic, and add a callback-like mechanism on top of
* the existing ARCore API.
*/
public class CloudAnchorManager {
/** Listener for the results of a host operation. */
public interface CloudAnchorListener {
/** This method is invoked when the results of a Cloud Anchor operation are available. */
void onComplete(Anchor anchor);
}
private final Session session;
private final Map<Anchor, CloudAnchorListener> pendingAnchors = new HashMap<>();
public CloudAnchorManager(Session session) {
this.session = Preconditions.checkNotNull(session);
}
/** Hosts an anchor. The {#code listener} will be invoked when the results are available. */
public synchronized void hostCloudAnchor(Anchor anchor, CloudAnchorListener listener) {
Preconditions.checkNotNull(listener, "The listener cannot be null.");
// This is configurable up to 365 days.
Anchor newAnchor = session.hostCloudAnchorWithTtl(anchor, /* ttlDays= */ 365);
pendingAnchors.put(newAnchor, listener);
}
/** Resolves an anchor. The {#code listener} will be invoked when the results are available. */
public synchronized void resolveCloudAnchor(String anchorId, CloudAnchorListener listener) {
Preconditions.checkNotNull(listener, "The listener cannot be null.");
Anchor newAnchor = session.resolveCloudAnchor(anchorId);
pendingAnchors.put(newAnchor, listener);
}
/** Should be called after a {#link Session#update()} call. */
public synchronized void onUpdate() {
Preconditions.checkNotNull(session, "The session cannot be null.");
for (Iterator<Map.Entry<Anchor, CloudAnchorListener>> it = pendingAnchors.entrySet().iterator();
it.hasNext(); ) {
Map.Entry<Anchor, CloudAnchorListener> entry = it.next();
Anchor anchor = entry.getKey();
if (isReturnableState(anchor.getCloudAnchorState())) {
CloudAnchorListener listener = entry.getValue();
listener.onComplete(anchor);
it.remove();
}
}
}
/** Clears any currently registered listeners, so they won't be called again. */
synchronized void clearListeners() {
pendingAnchors.clear();
}
private static boolean isReturnableState(CloudAnchorState cloudState) {
switch (cloudState) {
case NONE:
case TASK_IN_PROGRESS:
return false;
default:
return true;
}
}
}
Also, here is another class I am using (this is also from the Google example project):
/* Listens for a resolved anchor. */
private final class ResolveListener implements CloudAnchorManager.CloudAnchorListener {
#Override
public void onComplete(Anchor resolvedAnchor) {
runOnUiThread(
() -> {
Anchor.CloudAnchorState state = resolvedAnchor.getCloudAnchorState();
if (state.isError()) {
Log.e(TAG, "Error resolving a cloud anchor, state " + state);
userMessageText.setText(getString(R.string.resolving_error, state));
return;
}
Log.e(TAG, "cloud anchor successfully resolved, state " + state);
anchor = resolvedAnchor;
userMessageText.setText(getString(R.string.resolving_success));
gotGoodAnchor = true;
});
}
}
when I run my app, I point the phone's camera at the physical space where I previously put an object but the anchor is never resolved. I think the problem might be in the update function but I can't seem to figure out what.
I guess I wasn't looking at the object properly. Now, it's working. This code is correct.
As I know that the ViewModel should be secluded from the UI/View and contains only the logic that observes the data that's coming from the server or database
In my App, I used REST API "retrofit" and blogger API and I tried to migrate/upgrade the current code to MVVM but there are a few problems, let's go to the code
BloggerAPI Class
public class BloggerAPI {
private static final String BASE_URL =
"https://www.googleapis.com/blogger/v3/blogs/4294497614198718393/posts/";
private static final String KEY = "the Key";
private PostInterFace postInterFace;
private static BloggerAPI INSTANCE;
public BloggerAPI() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
postInterFace = retrofit.create(PostInterFace.class);
}
public static String getBaseUrl() {
return BASE_URL;
}
public static String getKEY() {
return KEY;
}
public static BloggerAPI getINSTANCE() {
if(INSTANCE == null){
INSTANCE = new BloggerAPI();
}
return INSTANCE;
}
public interface PostInterFace {
#GET
Call<PostList> getPostList(#Url String url);
}
public Call<PostList>getPosts(String url){
return postInterFace.getPostList(url);
}
}
this getData method I used in the Mainctivity to retrieve blog posts
public void getData() {
if (getItemsByLabelCalled) return;
progressBar.setVisibility(View.VISIBLE);
String url = BloggerAPI.getBaseUrl() + "?key=" + BloggerAPI.getKEY();
if (token != "") {
url = url + "&pageToken=" + token;
}
if (token == null) {
return;
}
final Call<PostList> postList = BloggerAPI.getINSTANCE().getPosts(url);
postList.enqueue(new Callback<PostList>() {
#Override
public void onResponse(#NonNull Call<PostList> call, #NonNull Response<PostList> response) {
if (response.isSuccessful()) {
progressBar.setVisibility(View.GONE);
PostList list = response.body();
Log.d(TAG, "onResponse: " + response.body());
if (list != null) {
token = list.getNextPageToken();
items.addAll(list.getItems());
adapter.notifyDataSetChanged();
for (int i = 0; i < items.size(); i++) {
items.get(i).setReDefinedID(i);
}
if (sqLiteItemsDBHelper == null || sqLiteItemsDBHelper.getAllItems().isEmpty()) {
SaveInDatabase task = new SaveInDatabase();
Item[] listArr = items.toArray(new Item[0]);
task.execute(listArr);
}
}
} else {
progressBar.setVisibility(View.GONE);
recyclerView.setVisibility(View.GONE);
emptyView.setVisibility(View.VISIBLE);
int sc = response.code();
switch (sc) {
case 400:
Log.e("Error 400", "Bad Request");
break;
case 404:
Log.e("Error 404", "Not Found");
break;
default:
Log.e("Error", "Generic Error");
}
}
}
#Override
public void onFailure(#NonNull Call<PostList> call, #NonNull Throwable t) {
Toast.makeText(MainActivity.this, "getData error occured", Toast.LENGTH_LONG).show();
Log.e(TAG, "onFailure: " + t.toString());
Log.e(TAG, "onFailure: " + t.getCause());
progressBar.setVisibility(View.GONE);
recyclerView.setVisibility(View.GONE);
emptyView.setVisibility(View.VISIBLE);
}
});
}
I created the PostsViewModel to trying to think practically how to migrate the current code to use MVVM
public class PostsViewModel extends ViewModel {
public MutableLiveData<PostList> postListMutableLiveData = new MutableLiveData<>();
public void getData() {
String token = "";
// if (getItemsByLabelCalled) return;
// progressBar.setVisibility(View.VISIBLE);
String url = BloggerAPI.getBaseUrl() + "?key=" + BloggerAPI.getKEY();
if (token != "") {
url = url + "&pageToken=" + token;
}
if (token == null) {
return;
}
BloggerAPI.getINSTANCE().getPosts(url).enqueue(new Callback<PostList>() {
#Override
public void onResponse(Call<PostList> call, Response<PostList> response) {
postListMutableLiveData.setValue(response.body());
}
#Override
public void onFailure(Call<PostList> call, Throwable t) {
}
});
}
}
and it's used thus in MainActivity
postsViewModel = ViewModelProviders.of(this).get(PostsViewModel.class);
postsViewModel.postListMutableLiveData.observe(this, postList -> {
items.addAll(postList.getItems());
adapter.notifyDataSetChanged();
});
now there are two problems using this way of MVVM "ViewModel"
first in the current getData method in the MainActivity it's contains some components that should work only in the View layer like the items list, the recyclerView needs to set View.GONE in case of response unsuccessful, progressBar, emptyView TextView, the adapter that needs to notify if there are changes in the list, and finally I need the context to used the create the Toast messages.
To solve this issue I think to add the UI components and other things into the ViewModel Class and create a constructor like this
public class PostsViewModel extends ViewModel {
Context context;
List<Item> itemList;
PostAdapter postAdapter;
ProgressBar progressBar;
TextView textView;
public PostsViewModel(Context context, List<Item> itemList, PostAdapter postAdapter, ProgressBar progressBar, TextView textView) {
this.context = context;
this.itemList = itemList;
this.postAdapter = postAdapter;
this.progressBar = progressBar;
this.textView = textView;
}
but this is not logically with MVVM arch and for sure cause memory leaking also I will not be able to create the instance of ViewModel with regular way like this
postsViewModel = ViewModelProviders.of(this).get(PostsViewModel.class);
postsViewModel.postListMutableLiveData.observe(this, postList -> {
items.addAll(postList.getItems());
adapter.notifyDataSetChanged();
});
and must be used like this
postsViewModel = new PostsViewModel(this,items,adapter,progressBar,emptyView);
so the first question is How to bind these UI components with the ViewModel?
second in the current getata I used the SaveInDatabase class use the AsyncTask way to save all items in the SQLite database the second question is How to move this class to work with ViewModel? but it also needs to work in the View layer to avoid leaking
the SaveInDatabase Class
static class SaveInDatabase extends AsyncTask<Item, Void, Void> {
#Override
protected Void doInBackground(Item... items) {
List<Item> itemsList = Arrays.asList(items);
// runtimeExceptionDaoItems.create(itemsList);
for (int i = 0 ; i< itemsList.size();i++) {
sqLiteItemsDBHelper.addItem(itemsList.get(i));
Log.e(TAG, "Size :" + sqLiteItemsDBHelper.getAllItems().size());
}
return null;
}
#Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
}
}
Actually the question is too broad to answer because there are many ways to implement for this case. First of all, never pass view objects to viewModel. ViewModel is used to notify changes to ui layer with LiveData or rxJava without retaining the view instance. You may try this way.
class PostViewModel extends ViewModel {
private final MutableLiveData<PostList> postListLiveData = new MutableLiveData<PostList>();
private final MutableLiveData<Boolean> loadingStateLiveData = new MutableLiveData<Boolean>();
private String token = "";
public void getData() {
loadingStateLiveData.postValue(true);
// if (getItemsByLabelCalled) return;
// progressBar.setVisibility(View.VISIBLE);
String url = BloggerAPI.getBaseUrl() + "?key=" + BloggerAPI.getKEY();
if (token != "") {
url = url + "&pageToken=" + token;
}
if (token == null) {
return;
}
BloggerAPI.getINSTANCE().getPosts(url).enqueue(new Callback<PostList>() {
#Override
public void onResponse(Call<PostList> call, Response<PostList> response) {
loadingStateLiveData.postValue(false);
postListLiveData.setValue(response.body());
token = response.body().getNextPageToken(); //===> the token
}
#Override
public void onFailure(Call<PostList> call, Throwable t) {
loadingStateLiveData.postValue(false);
}
});
}
public LiveData<PostList> getPostListLiveData(){
return postListLiveData;
}
public LiveData<Boolean> getLoadingStateLiveData(){
return loadingStateLiveData;
}
}
and you may observe the changes from your activity like this.
postsViewModel = ViewModelProviders.of(this).get(PostsViewModel.class);
postsViewModel.getPostListLiveData().observe(this,postList->{
if(isYourPostListEmpty(postlist)) {
recyclerView.setVisibility(View.GONE);
emptyView.setVisibility(View.VISIBLE);
items.addAll(postList.getItems());
adapter.notifyDataSetChanged();
}else {
recyclerView.setVisibility(View.VISIBLE);
emptyView.setVisibility(View.GONE);
}
});
postsViewModel.getLoadingStateLiveData().observe(this,isLoading->{
if(isLoading) {
progressBar.setVisibility(View.VISIBLE);
}else {
progressBar.setVisibility(View.GONE);
}
});
For my personal prefer, I like using Enum for error handling, but I can't post here as it will make the answer very long. For your second question, use Room from google. It will make you life a lot easier. It work very well with mvvm and it natively support liveData. You can try CodeLab from google to practise using room.
Bonus: You don't need to edit the url like this:
String url = BloggerAPI.getBaseUrl() + "?key=" + BloggerAPI.getKEY();
if (token != "") {
url = url + "&pageToken=" + token;
}
You can use #Path or #query based on your requirements.
As your question is bit broad , I am not giving any source code for the same, Rather mentioning samples which clearly resolves issues mentioned with MVVM.
Clean Code Architecture can be followed which will clearly separate the responsibilities of each layer.
First of all application architecture needs to be restructured so that each layer has designated role in MVVM. You can follow the following pattern for the same.
Only View Model will have access to UI layer
View model will connect with Use Case layer
Use case layer will connect with Data Layer
No layer will have cyclic reference to other components.
So now for Database, Repository will decide, from which section the data needs to be fetched
This can be either from Network or from DataBase.
All these points (except Database part) are covered over Medium Article, were each step is covered with actual API's .
Along with that unit test is also covered.
Libraries used are in this project are
Coroutines
Retrofit
Koin (Dependency Injection) Can be replaced with dagger2 is required
MockWebServer (Testing)
Language: Kotlin
Full Source code can be found over Github
Edit
Kotlin is the official supported language for Android Development now. I suggest you should lean and migrate your java android projects to Kotlin.
Still for converting Kotlin to Java, Go to Menu > Tools > Kotlin > Decompile Kotlin to Java Option
After reading MANY posts that seem similar, these were all about JSON requests, not StringRequests.
I am using volley API for my Android application, and I am following a tutorial on interaction between my app using volley and my server which is handled with php.
For some reason however, my data is not sent to the php part, because when I try to access the data on the webserver, it states that the variables are empty.
Here is my project. First off is my Singleton class which sets up ONE requestqueue:
import android.content.Context;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.toolbox.Volley;
public class Server_singleton
{
private static Server_singleton anInstance;
private RequestQueue requestQueue;
private static Context aCtx;
private Server_singleton(Context context)
{
aCtx = context;
requestQueue = getRequestQueue();
}
public static synchronized Server_singleton getInstance(Context context)
{
if(anInstance == null)
{
anInstance = new Server_singleton(context);
}
return anInstance;
}
public RequestQueue getRequestQueue()
{
if(requestQueue == null)
{
requestQueue = Volley.newRequestQueue(aCtx.getApplicationContext());
}
return requestQueue;
}
public <T> void addToRequestQueue(Request<T> request)
{
requestQueue.add(request);
}
}
This above class should be all nice and fine I believe(99% certain), since I follow a general design approach recommended by Android/Google using volley.
Secondly, the next file which uses Server_singleton. Here is where the magic happens, and most likely the mistake is in here some place:
import android.content.Context;
import android.util.Log;
import com.android.volley.AuthFailureError;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.StringRequest;
import com.android.volley.toolbox.Volley;
import java.util.HashMap;
import java.util.Map;
/**
*
*
* This class handles requests to web server by using Google Volley API
* Google Volley API is very powerful and abstracts many low-level details when establishing
* connection with a web server.
* Volley API does not run on the main thread, which is the correct way of doing it in android.
* If it was not doing work in a background thread, the main thread would be blocked(perhaps).
* This is all done in an asynchronous way, which means that methods may behave somewhat
* different than you would expect. A method which returns a string for example
* may return a null object, before it is actually done waiting on the response from server
* This means that we have to introduce callback methods with for instance interfaces.
*/
public class Server_interaction
{
String server_url = "http://hiddenfromyou/update_location.php"; //correct ip in my code, but hidden here
String response_string;
RequestQueue queue;
Context context;
public Server_interaction(Context context)
{
this.context = context;
queue = Server_singleton.getInstance(context).getRequestQueue();
}
public static final String TAG = Server_interaction.class.getSimpleName();
public void post_request(final VolleyCallback callback)
{
StringRequest stringRequest = new StringRequest(Request.Method.POST, server_url,
new Response.Listener<String>()
{
#Override
public void onResponse(String response)
{
response_string = response;
callback.onSuccess(response_string);
//requestQueue.stop();
Log.i(TAG, "the response is: "+ response_string);
}
}
, new Response.ErrorListener() {
#Override
public void onErrorResponse(VolleyError error){
response_string = "Something went wrong";
//error.printstacktrace()
//requestQueue.stop();
Log.i(TAG, "something went wrong. Is the server up and running?");
}
})
{
#Override
protected Map<String, String> getParams() throws AuthFailureError {
String the_name = "olaf";
String the_mail = "lalalal";
String the_country = "Norway";
String the_latitude = "33";
String the_longitude = "99";
Map<String, String> params = new HashMap<String, String>();
params.put("name", the_name);
params.put("email", the_mail);
params.put("country", the_country);
params.put("latitude", String.valueOf(the_latitude));
params.put("longitude", String.valueOf(the_longitude));
Log.i(TAG, "inside getparams : "+params);
return params;
}
};//stringrequest parameter end
//add request to requestqueue
Log.i(TAG, "the stringrequest: "+ stringRequest);
Server_singleton.getInstance(context).addToRequestQueue(stringRequest);
Log.i(TAG, "the response again:: "+ response_string);
}
}
The above code WORKS. But it should POST country, latitute etc to my webserver...
Here is my PHP script:
<?php
$email = isset($_POST["email"]) ? $_POST["email"] : print("received nothing!"); //receive from android app
$phonenumber = $_POST["phonenumber"]; //receive from android app
$country = $_POST["country"]; //receive from android app
$latitude = $_POST["latitude"]; //receive from android app
$longitude = $_POST["longitude"];
$username_for_localhost = "root";
$password_for_localhost = "";
$host = "localhost";
$db_name = "exigentia_location_db";
$con = mysqli_connect($host, $username_for_localhost, $password_for_localhost, $db_name);
if($con)
{
echo "Connection succeded";
}
else
{
echo "Connection failed";
}
$sql = "insert into person values('".$email."', '".$phonenumber."', '".$country."', '".$location."', '".$latitude."', '".$longitude."');";
if(mysqli_query($con, $sql))
{
echo "data insertion succeeded";
}
else
{
echo "data insertion failed";
}
mysqli_close($con);
?>
I check only the first var if its set, and else print out. It prints out the text, which means it is not set...Also the other ones give me index errors, since they obviously are empty...
What am I doing wrong? I have been fiddling with this problem for days, and I cannot figure out where I am wrong.
Finally a pic of what happens when I refresh my page with the php script after running the app:
try this:
Server_singleton.getInstance().addToRequestQueue(request, method);
where method is your tag: like "Register", "login" ..etc.You can use without Tag method also.
Now in your Server_singleton write this code:
public class Server_singleton extends Application {
public static final String TAG = Server_singleton.class.getSimpleName();
private RequestQueue mRequestQueue;
private static Server_singleton mInstance;
#Override
public void onCreate() {
super.onCreate();
mInstance = this;
}
public static synchronized Server_singleton getInstance() {
return mInstance;
}
public RequestQueue getRequestQueue() {
if (mRequestQueue == null) {
mRequestQueue = Volley.newRequestQueue(getApplicationContext());
}
return mRequestQueue;
}
public <T> void addToRequestQueue(Request<T> req, String tag) {
req.setTag(TextUtils.isEmpty(tag) ? TAG : tag);
getRequestQueue().add(req);
}
public <T> void addToRequestQueue(Request<T> req) {
req.setTag(TAG);
getRequestQueue().add(req);
}
public void cancelPendingRequests(Object tag) {
if (mRequestQueue != null) {
mRequestQueue.cancelAll(tag);
}
}
}
Make sure you set the permission in manifest:
<uses-permission android:name="android.permission.INTERNET" />
And in build.gradle use:
compile 'com.mcxiaoke.volley:library-aar:1.0.0'
put("Content-Type", "application/x-www-form-urlencoded")
http://www.itworld.com/article/2702452/development/how-to-send-a-post-request-with-google-volley-on-android.html
You need to also override the getBodyContentType() like getParams() and put the following code in it.
#Override
public String getBodyContentType() {
return "application/x-www-form-urlencoded; charset=UTF-8";
}
The picture you have shown depicts that there may be errors in your PHP script. The $ sign may not be present with the variables used in the script, or any other scripting errors cause such UI to appear as a result of running php script.
com.android.volley.NoConnectionError: java.net.ProtocolException: Unknown method 'PATCH'; must be one of [OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE]
StringRequest putRequest = new StringRequest(Request.Method.PATCH, url,
new Response.Listener<String>()
{
#Override
public void onResponse(String response)
{
Log.d("Response", response);
}
},
new Response.ErrorListener()
{
#Override
public void onErrorResponse(VolleyError error)
{
Log.d("Error.Response", error.toString());
}
}
) {
#Override
protected Map<String, String> getParams()
{
Map<String, String> params = new HashMap<String, String> ();
params.put("name", "My Name");
params.put("age", "11");
return params;
}
};
Are you sure you are using correct version of Volley Library? I just tried your code in Lollipop and it is working OK.
If you are using Volley library as external project, check the Method interface of Request class in com.android.volley package. It should have a PATCH variable in it.
public interface Method {
int DEPRECATED_GET_OR_POST = -1;
int GET = 0;
int POST = 1;
int PUT = 2;
int DELETE = 3;
int HEAD = 4;
int OPTIONS = 5;
int TRACE = 6;
int PATCH = 7;
}
If not, use the latest version of Volley library.
UPDATE:
You are correct, it is showing this error in Kitkat, but not in Lollipop. I guess the main problem is that HTTPUrlConnection of Java does not support PATCH.
(I guess it works in Lollipop because it is using Java 7 and HTTPUrlConnection of Java 7 supports PATCH method?)
Anyhow, You can use the OkHttp Library to correct this problem. The okhttp-urlconnection module implements the java.net.HttpURLConnection
Add the following jar to your libs folder:
okhttp-2.2.0.jar
okhttp-urlconnection-2.2.0.jar
okio-1.2.0.jar
Create a OkHttpStack class:
package com.example.temp;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import com.android.volley.toolbox.HurlStack;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.OkUrlFactory;
public class OkHttpStack extends HurlStack {
private final OkUrlFactory mFactory;
public OkHttpStack() {
this(new OkHttpClient());
}
public OkHttpStack(OkHttpClient client) {
if (client == null) {
throw new NullPointerException("Client must not be null.");
}
mFactory = new OkUrlFactory(client);
}
#Override protected HttpURLConnection createConnection(URL url) throws IOException {
return mFactory.open(url);
}
}
Use the following constructor to create a Volley RequestQueue:
Volley.newRequestQueue(getApplicationContext(),new OkHttpStack()).add(putRequest);
It is working for me on Kitkat now.
While sending request use POST.
In headers just override http method to PATCH.
For me now its working in volley even in kitkat version.
header.put("X-HTTP-Method-Override", "PATCH");
i already found out how to post something to a wall with the graph api on behalf of the facebook user. But now i want to post something in the name of my application.
Here is how i'm trying to do this:
protected void btn_submit_Click(object sender, EventArgs e)
{
Dictionary<string, string> data = new Dictionary<string, string>();
data.Add("message", "Testing");
// i'll add more data later here (picture, link, ...)
data.Add("access_token", FbGraphApi.getAppToken());
FbGraphApi.postOnWall(ConfigSettings.getFbPageId(), data);
}
FbGraphApi.getAppToken()
// ...
private static string graphUrl = "https://graph.facebook.com";
//...
public static string getAppToken() {
MyWebRequest req = new MyWebRequest(graphUrl + "/" + "oauth/access_token?type=client_cred&client_id=" + ConfigSettings.getAppID() + "&client_secret=" + ConfigSettings.getAppSecret(), "GET");
return req.GetResponse().Split('=')[1];
}
FbGraphApi.postOnWall()
public static void postOnWall(string id, Dictionary<string,string> args)
{
call(id, "feed", args);
}
FbGraphApi.call()
private static void call(string id, string method, Dictionary<string,string> args )
{
string data = "";
foreach (KeyValuePair<string, string> arg in args)
{
data += arg.Key + "=" + arg.Value + "&";
}
MyWebRequest req = new MyWebRequest(graphUrl +"/" + id + "/" + method, "POST", data.Substring(0, data.Length - 1));
req.GetResponse(); // here i get: "The remote server returned an error: (403) Forbidden."
}
Does anyone see where this i going wrong? I'm really stuck on this.
Thanks!
You need to obtain the Auth Token for your application to post as that application.
The Auth_Token defines the security context you are posting as.
You would need to request the following Graph API URL, for the current user, to find the access token for your application.
https://graph.facebook.com/me/accounts?access_token=XXXXXXXX
This should give you an output similar to the following:
{
"data": [
{
"name": "My App",
"category": "Application",
"id": "10258853",
"access_token": "xxxxxxxxxxxxxxxx"
}
]
}
Be sure you have the manage_pages permission before calling that API or your will not get the access token back.
Once you have the Access Token you publish to the wall like you would any other user. Note that the ID used in the URL matches the ID of the application. This will post to the Application's wall as the Application.
https://graph.facebook.com/10258853/feed?access_token=XXXXXXX
Be sure you have the publish_stream permission as well before posting to the wall.
Recently I had worked With FB api's.
I had Done every thing in javascript.
Here is what i used to post to a users wall.
I hope this helps you.
Include the javascript library provided by FB and add your app id to it.
<div id="fb-root"></div>
<script>
window.fbAsyncInit = function() {
FB.init({appId: 'your app id', status: true, cookie: true,
xfbml: true});
};
(function() {
var e = document.createElement('script');
e.type = 'text/javascript';
e.src = document.location.protocol +
'//connect.facebook.net/en_US/all.js';
e.async = true;
document.getElementById('fb-root').appendChild(e);
}());
</script>
For login , i used a button with "fb_login" as id and then i used jquery as follows:
$("#fb_login").click(function(){
FB.login(function(response) {
if (response.session)
{
if (response.perms)
{
// alert("Logged in and permission granted for posting");
}
else
{
// alert("Logged in but permission not granted for posting");
}
}
else
{
//alert("Not Logged In");
}
}, {perms:'publish_stream'});
Note that You have to add {perms:'publish_stream'} as done above which will obtain you the rights to post to the users wall.
A button with id="stream_publish" and then the following jquery:
$("#stream_publish").click(function(){
FB.getLoginStatus(function(response){
if(response.session)
{
publishPost(response.session);
}
});
});
function publishPost(session)
{
var publish = {
method: 'stream.publish',
message: 'Your Message',
picture : 'Image to be displayed',
link : 'The link that will be the part of the post, which can point to either your app page or your personal page or any other page',
name: 'Name or title of the post',
caption: 'Caption of the Post',
description: 'It is fun to write Facebook App!',
actions : { name : 'Start Learning', link : 'link to the app'}
};
FB.api('/me/feed', 'POST', publish, function(response) {
document.getElementById('confirmMsg').innerHTML =
'A post had just been published into the stream on your wall.';
});
};
private class FbWebViewClient extends WebViewClient {
boolean started=false;
#Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Log.d("Facebook-WebView", "Redirect URL: " + url);
if (url.startsWith(Facebook.REDIRECT_URI)) {
Bundle values = Util.parseUrl(url);
String error = values.getString("error");
if (error == null) {
error = values.getString("error_type");
}
if (error == null) {
mListener.onComplete(values);
} else if (error.equals("access_denied")
|| error.equals("OAuthAccessDeniedException")) {
mListener.onCancel();
} else {
mListener.onFacebookError(new FacebookError(error));
}
FbDialog.this.dismiss();
return true;
} else if (url.startsWith(Facebook.CANCEL_URI)) {
mListener.onCancel();
FbDialog.this.dismiss();
return true;
} else if (url.contains(DISPLAY_STRING)) {
return false;
}
// launch non-dialog URLs in a full browser
getContext().startActivity(
new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
return true;
}
#Override
public void onReceivedError(WebView view, int errorCode,
String description, String failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);
mListener.onError(new DialogError(description, errorCode,
failingUrl));
FbDialog.this.dismiss();
}
public Map<String, String> getUrlParameters(String url)
throws UnsupportedEncodingException {
Map<String, String> params = new HashMap<String, String>();
String[] urlParts = url.split("\\?");
if (urlParts.length > 1) {
String query = urlParts[1];
for (String param : query.split("&")) {
String pair[] = param.split("=");
String key = URLDecoder.decode(pair[0], "UTF-8");
String value = "";
if (pair.length > 1) {
value = URLDecoder.decode(pair[1], "UTF-8");
}
params.put(key, value);
}
}
return params;
}
#Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
Log.d("Facebook-WebView", "Webview loading URL: " + url);
String newUrl="http://www.facebook.com/dialog/feed?_path=feed&app_id=";
if (url.contains("touch") && started==false) {
started=true;
ChildTabBibleLessonActivity.fbMaterial=ChildTabBibleLessonActivity.fbMaterial.replace(" ", "+");
url=url+"&picture=http://www.minibiblecollege.org/mbclandingpage/images/icmlogo-small.jpg&description="+ChildTabBibleLessonActivity.fbMaterial;
/* Map<String,String> param;
try {
param = getUrlParameters(url);
newUrl=newUrl+param.get("app_id")+"&redirect_uri="+"https://deep-rain-6015.herokuapp.com"+"&display=page&picture=http://www.minibiblecollege.org/mbclandingpage/images/icmlogo-small.jpg"+"&name=MiniBible&description=heregoesMyMessage";
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
*/
view.loadUrl(url);
//super.onPageStarted(view, url, favicon);
}
else
{
super.onPageStarted(view, url, favicon);
}
mSpinner.show();
}
#Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
mSpinner.dismiss();
/*
* Once webview is fully loaded, set the mContent background to be
* transparent and make visible the 'x' image.
*/
mContent.setBackgroundColor(Color.TRANSPARENT);
mWebView.setVisibility(View.VISIBLE);
mCrossImage.setVisibility(View.VISIBLE);
}
}