Android volley Timeout Exception when using Reques

2020-07-03 06:17发布

问题:

In my Fragment, i am trying to use TMDB's open movie DB to get details about "Now Playing" Movies.

If i use RequestFuture.get(time, TimeUnit) method to execute this volley request i always get a timeout error. If i manually test the same Url in Safari, i get the results instantaneously.

What I know:

1.) It is not any JSON Parsing error.(the program doesnt even progress to the parsing steps)

2.) No internet issues with AVD. (Reason explained later).

3.) Not an issue with my volley singleton class or my Request Queue. (Reason explained Later).

So im presuming that i am making someother kind of mistake regarding the usages of volley/Request Future.

Fragment Code below:

public class BoxOffice extends android.support.v4.app.Fragment {
    private VolleySingleton volleySingleton;
    private RequestQueue requestQueue;
    private ImageLoader imageLoader;
    private ArrayList<MyMovie> movieList;
    private MyUriBuilder mBuilder;

    public BoxOffice() {
        // Required empty public constructor
        volleySingleton = VolleySingleton.getInstance();
        requestQueue = volleySingleton.getRequestQueue();
        mBuilder = new MyUriBuilder();
        movieList = new ArrayList<>();
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            mParam1 = getArguments().getString(ARG_PARAM1);
            mParam2 = getArguments().getString(ARG_PARAM2);
        }
        StepA();
    }

    public void StepA() {
        String url = mBuilder.getURL("box");
        Log.d("RT", "StepA initiated - "+ url); // Url is perfect - works when copied in Safari.
        RequestFuture<JSONObject> futureA = RequestFuture.newFuture();
        JsonObjectRequest request = new JsonObjectRequest(Request.Method.GET, url, (String) null, futureA, futureA);
        requestQueue.add(request);

        try {
            JSONObject response = futureA.get(30, TimeUnit.SECONDS);
            Log.d("RT", "StepA - response received"); //Never reaches this step
            parseJsonFeed(response);
        } catch (InterruptedException e) {
            Log.e("RT", "StepA - InterruptedException - " + e);
            e.printStackTrace();
        } catch (ExecutionException e) {
            Log.e("RT", "StepA - ExecutionException - " + e);
            e.printStackTrace();
        } catch (TimeoutException e) {
            Log.e("RT", "StepA - TimeoutException - " + e);
            e.printStackTrace();
        }
        Log.d("RT", "StepA END");
    }

    public void parseJsonFeed(JSONObject response) {
        Log.d("RT", "StepA - parseFeed Begin");
        if (response == null || response.length() == 0) {
            return;
        }
        MyMovie currentMovie = null;
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");

        try {
            if (response.has("results")) {
                Log.d("RT", "StepA - results");
                JSONArray resultList = response.getJSONArray("results");
                for (int i = 0; i < 3; i++) {
                    Log.d("RT", "movie " + i);
                    JSONObject movieElement = resultList.getJSONObject(i);
                    if (movieElement.has("id") && movieElement.has("title")) {
                        currentMovie = new MyMovie();
                        currentMovie.setTmdb_id(movieElement.getString("id"));
                        currentMovie.setTitle(movieElement.getString("title"));
                        if (movieElement.has("release_date")) {
                            currentMovie.setReleaseDate(dateFormat.parse(movieElement.getString("release_date")));
                        } else {
                            currentMovie.setReleaseDate(dateFormat.parse("0000-00-00"));
                        }
                        movieList.add(i, currentMovie);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        Log.d("RT", "StepA - parseFeed END");
    }
}

Logcat with the filter for the tag "RT":

05-30 15:17:51.710  D/RT﹕ TL - Constructor Called
05-30 15:17:51.800  D/RT﹕ StepA initiated - https://api.themoviedb.org/3/movie/now_playing?api_key=##### (link works fine)
05-30 15:18:21.820  E/RT﹕ StepA - TimeoutException - java.util.concurrent.TimeoutException
05-30 15:18:21.820  D/RT﹕ StepA END

Before using the RequestFuture methods, i basically did the same thing implementing my own Response.Listener and Response.ErrorListener in my Fragment oncreate(instead of the StepA();) and it WORKED!!!

Below is the code-snippet for that:

JsonObjectRequest request = new JsonObjectRequest(Request.Method.GET, mBuilder.getURL("box"), (String) null, new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                parseJsonFeed(response);
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Toast.makeText(getActivity(), error.toString(), Toast.LENGTH_LONG).show();
            }
        });
        requestQueue.add(request);

So my question is why doesn't it work when i implement the request future methods?

If you ask me why i want to go for synchronous volley implementation; it is because after this i have to have two more volley requests which depend on this request being fully, successfully completed. And also i'm learning :)

回答1:

Sad that no-one could help answer this question but i managed to solve this issue like below:

The timeout will happen to the RequestFuture.get() if it is on the same thread as the UI thread. I have changed the mechanism of the request so that the request is done on a separate Asynch thread(not UI thread) and the response is also received on a separate thread from the request like below:

private void StepA() {
        Log.d("RT", "StepA initiated");
        final CountDownLatch latchA = new CountDownLatch(1);

        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                Log.d("RT", "Thread t Begins");
                ThreadA threadA = new ThreadA();
                try {
                    JSONObject jsonObject = threadA.execute().get(10, TimeUnit.SECONDS);
                    parseA(jsonObject);
                    latchA.countDown();
                    Log.d("RT", "Thread t Ends");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        try {
            latchA.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Log.d("RT", "StepA END");
    }

Below is the Asynch task code for the request:

protected class ThreadA extends AsyncTask<Void, Void, JSONObject> {
    final String url = mBuilder.getURL("box");

    public ThreadA() {
    }

    @Override
    protected JSONObject doInBackground(Void... params) {
        final RequestFuture<JSONObject> future = RequestFuture.newFuture();
        JsonObjectRequest request = new JsonObjectRequest(Request.Method.GET, url, (String) null, future, future);
        requestQueue.add(request);
        try {
            return future.get(10, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
        return null;
    }
}

I've added countdown latches cause they are awesome and also cause i have few more requests like this in my program that depend on this snippet's response. Hence they help run the program more synchronously.



回答2:

rapidclock's answer is fine. Personally I prefer using an IntentService because they're so wonderful. Also Google recommends it: https://www.youtube.com/watch?v=xHXn3Kg2IQE&t=1852s

here's my IntentService:

// http://stackoverflow.com/questions/30549268/android-volley-timeout-exception-when-using-requestfuture-get
//http://afzaln.com/volley/com/android/volley/toolbox/RequestFuture.html
//http://stackoverflow.com/questions/36735682/android-synchronizing-methods-across-processes/36737001#36737001
// http://stackoverflow.com/questions/16904741/can-i-do-a-synchronous-request-with-volley
package org.peacekeeper.service;

import android.app.IntentService;
import android.content.Intent;
import android.os.*;

import com.android.volley.RequestQueue;

import org.json.JSONObject;
import org.peacekeeper.app.R;
import org.peacekeeper.service.pkRequest.pkURL;
import org.peacekeeper.util.*;
import org.slf4j.*;

import java.util.concurrent.*;


/**
 Asynchronously handles an intent using a worker thread. Receives a ResultReceiver object and a
 location through an intent. Tries to fetch the address for the location using a Geocoder, and
 sends the result to the ResultReceiver.
 */
public class RESTIntentService extends IntentService{
//begin static
//Intent putextra ID's
static public final String 
        RECEIVER   = "RESTIntentServiceRCVR",
        JSONResult = "JSONResult",
        REQUEST    = "RESTIntentServiceRequest";
protected final static pkUtility    mUtility      = pkUtility.getInstance();
protected final static RequestQueue mRequestQueue = mUtility.getRequestQueue();
private final static   long         TIMEOUT       = 5;

//end static
private static final Logger mLog = LoggerFactory.getLogger( RESTIntentService.class );
//The receiver where results are forwarded from this service.
private ResultReceiver mReceiver;

//This constructor is required, and calls the super IntentService(String) constructor with the name for a worker thread.
public RESTIntentService(){ super( "RESTIntentService" ); }


@Override protected void onHandleIntent( Intent intent ){
    String errorMessage = "";

    mReceiver = intent.getParcelableExtra( RECEIVER );

    if ( mReceiver == null ){// Check if receiver was properly registered.
        mLog.error( "No RESTIntentService receiver received. There is nowhere to send the results." );
        return;
    }


    // Get the pkRequest passed to this service through an extra.
    pkRequest.pkURL URL = pkURL.valueOf( intent.getStringExtra( REQUEST ) );
    mLog.debug( "RESTIntentService URL: " + URL.toString() );
    // Make sure that the location data was really sent over through an extra. If it wasn't,
    // send an error message and return.
    if ( URL == null ){
        errorMessage = getString( R.string.no_pkRequest_provided );
        mLog.error( errorMessage );
        deliverResultToReceiver( Constants.FAILURE_RESULT, errorMessage );
        return;
    }


    //Request retval = null;
    JSONObject response = null;

    pkRequest request = new pkRequest( URL );
    mLog.debug( "onHandleIntent:\n" + request.toString() );

    request.submit();

    try{
        //while (!request.mFuture.isDone()) {;}
// TODO THIS BLOCKS the service but not the main UI thread. Consider wrapping in an asynch task:
// see http://stackoverflow.com/questions/30549268/android-volley-timeout-exception-when-using-requestfuture-get
        response = request.mFuture.get( TIMEOUT, TimeUnit.SECONDS );

        mLog.debug( "onHandleIntent:\n" + response.toString() );

    }catch ( InterruptedException | ExecutionException | TimeoutException x ){
        errorMessage = getString( R.string.failed_future_request );
        mLog.error( errorMessage, x );
        x.printStackTrace();
    }

    if ( errorMessage.isEmpty() ){
        deliverResultToReceiver( Constants.SUCCESS_RESULT,
                                 response.toString() );
    }
    else{ deliverResultToReceiver( Constants.FAILURE_RESULT, errorMessage ); }
}//onHandleIntent()

// Sends a resultCode and message to the receiver.
private void deliverResultToReceiver( int resultCode, String message ){
    Bundle bundle = new Bundle();
    bundle.putString( JSONResult, message );
    mReceiver.send( resultCode, bundle );
}
}//class RESTIntentService

The downside to using an IntentService is that IT (but not the main UI thread) will be blocked by the future.get(...). (see comment in code re: future.get block) So if you're blasting REST calls at it then you might consider still using it AND wrapping your calls in an async as recommended by rapidclock.

To use the above IntentService put this in the main UI (or whereever):

protected void startRESTService( final pkRequest.pkURL aURL ){
    // Start the service. If the service isn't already running, it is instantiated and started
    // (creating a process for it if needed); if it is running then it remains running. The
    // service kills itself automatically once all intents are processed.

    startService(
            new Intent( this, RESTIntentService.class )
                    .putExtra( RESTIntentService.RECEIVER, mRESTResultReceiver )
                    .putExtra( RESTIntentService.REQUEST, aURL.name() )
                );
}//startRESTService()

//Receiver for data sent from RESTIntentService.
class RESTResultReceiver extends ResultReceiver{
    public RESTResultReceiver( Handler handler ){ super( handler ); }

    //Receives data sent from RESTIntentService and updates the UI in MainActivity.
    @Override protected void onReceiveResult( int resultCode, Bundle resultData ){
        String snippet = resultData.getString( RESTIntentService.JSONResult );
        mLog.debug( "RESTResultReceiver:\t" + snippet );

    }//onReceiveResult
}//class RESTResultReceiver

oh alright...here's my activity (please don't ding me for being overly detailed...I love love love STACKOVERFLOW but no good deed goes unpunished....):

//http://stackoverflow.com/questions/34582370/how-can-i-show-current-location-on-a-google-map-on-android-marshmallow/34582595#34582595
package org.peacekeeper.app;

import android.Manifest;
import android.content.*;
import android.content.pm.PackageManager;
import android.location.Location;
import android.os.*;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.*;
import android.widget.Toast;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.location.*;
import com.google.android.gms.maps.*;
import com.google.android.gms.maps.GoogleMap.*;
import com.google.android.gms.maps.model.*;
import com.google.android.gms.maps.model.Marker;

import org.json.JSONObject;
import org.peacekeeper.rest.LinkedRequest;
import org.peacekeeper.service.*;
import org.peacekeeper.service.pkRequest.pkURL;
import org.peacekeeper.util.pkUtility;
import org.slf4j.*;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.util.ContextInitializer;
import ch.qos.logback.core.joran.spi.JoranException;

public class actGeocoder extends AppCompatActivity
        implements OnMapReadyCallback,
                   GoogleApiClient.ConnectionCallbacks,
                   GoogleApiClient.OnConnectionFailedListener,
                   LocationListener,
                   OnMapLongClickListener,
                   OnMarkerClickListener{

//begin static
private static final LoggerContext mLoggerContext =
        (LoggerContext) LoggerFactory.getILoggerFactory();
private static final ContextInitializer mContextInitializer =
        new ContextInitializer( mLoggerContext );
private static final Logger mLog = LoggerFactory.getLogger( actGeocoder.class );

private static final int MY_PERMISSIONS_REQUEST_LOCATION = 99;
//end static


private GoogleMap mGoogleMap;
private SupportMapFragment mapFrag;
private LocationRequest mLocationRequest;
private GoogleApiClient mGoogleApiClient;
private MarkerOptions mMarkerOptions;
private Marker mMarker;
private AddressResultReceiver mResultReceiver = new AddressResultReceiver( new Handler() );
private RESTResultReceiver mRESTResultReceiver = new RESTResultReceiver( new Handler() );
private pkUtility mUtility;

public void newPeaceKeeperStatus(){
    startRESTService( pkRequest.pkURL.status );
}




@Override protected void onCreate( Bundle savedInstanceState ){
    super.onCreate( savedInstanceState );
    mUtility = pkUtility.getInstance( this );
    newPeaceKeeperStatus();
    setContentView( R.layout.geocoder );

    getSupportActionBar().setTitle( R.string.RegisterYourLocn );
    buildGoogleApiClient();
    mapFrag = (SupportMapFragment) getSupportFragmentManager().findFragmentById( R.id.geocoder );
    mapFrag.getMapAsync( this );
}//onCreate


@Override public void onResume(){
    super.onResume();
    mGoogleApiClient.connect();
}


@Override protected void onRestart(){
    super.onRestart();
    // Reload Logback log: http://stackoverflow.com/questions/3803184/setting-logback-appender-path-programmatically/3810936#3810936
    mLoggerContext.reset();

    //I prefer autoConfig() over JoranConfigurator.doConfigure() so I don't need to find the file myself.
    try{ mContextInitializer.autoConfig(); }
    catch ( JoranException X ){ X.printStackTrace(); }
}//onRestart()

@Override protected void onStop(){
    mGoogleApiClient.disconnect();
    mLoggerContext.stop();//flush log
    super.onStop();
}

@Override public void onDestroy(){
    mLog.trace( "onDestroy():\t" );
    mLoggerContext.stop();//flush log
    super.onDestroy();
}

@Override public void onRequestPermissionsResult( int requestCode, String permissions[], int[] grantResults ){
    switch ( requestCode ){
    case MY_PERMISSIONS_REQUEST_LOCATION:{
        // If request is cancelled, the result arrays are empty.
        if ( grantResults.length > 0
             && grantResults[ 0 ] == PackageManager.PERMISSION_GRANTED ){

            // permission was granted, yay! Do the location-related task you need to do.
            if ( ContextCompat.checkSelfPermission( this,
                                                    Manifest.permission.ACCESS_FINE_LOCATION )
                 == PackageManager.PERMISSION_GRANTED ){

                if ( mGoogleApiClient == null ){ buildGoogleApiClient(); }
                mGoogleMap.setMyLocationEnabled( true );
            }

        }
            // permission denied. Disable the functionality that depends on this permission.
        else{ Toast.makeText( this, "permission denied", Toast.LENGTH_LONG ).show(); }
        return;
    }

    }//switch
}

protected synchronized void buildGoogleApiClient(){
    mGoogleApiClient = new GoogleApiClient.Builder( this )
            .addConnectionCallbacks( this )
            .addOnConnectionFailedListener( this )
            .addApi( LocationServices.API )
            .build();

    mGoogleApiClient.connect();
}

//http://stackoverflow.com/questions/31328143/android-google-maps-onmapready-store-googlemap
@Override public void onMapReady( GoogleMap googleMap ){
    //Initialize Google Play Services
    if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ){
        if ( ContextCompat.checkSelfPermission( this,
                                                Manifest.permission.ACCESS_FINE_LOCATION )
             != PackageManager.PERMISSION_GRANTED ){
            //Location Permission already granted
            checkLocationPermission();
            return;  //Request Location Permission
        }

    }

    mGoogleMap = googleMap;
    mGoogleMap.setMapType( GoogleMap.MAP_TYPE_NORMAL );

    mGoogleMap.setOnMapLongClickListener( this );
    mGoogleMap.setOnMarkerClickListener(this);

    mMarkerOptions = new MarkerOptions()
            .title( "Tap this marker again to register your location" )
            .icon( BitmapDescriptorFactory.defaultMarker( BitmapDescriptorFactory.HUE_MAGENTA) );
}



private void checkLocationPermission(){
    if ( ContextCompat.checkSelfPermission( this, Manifest.permission.ACCESS_FINE_LOCATION )
         != PackageManager.PERMISSION_GRANTED ){

        // Should we show an explanation?
        if ( ActivityCompat.shouldShowRequestPermissionRationale( this,
                                                                  Manifest.permission.ACCESS_FINE_LOCATION ) ){

// Show an explanation to the user *asynchronously* -- don't block this thread waiting for the user's response!
// After the user sees the explanation, try again to request the permission.
            new AlertDialog.Builder( this )
                    .setTitle( "Location Permission Needed" )
                    .setMessage(
                            "This app needs the Location permission, please accept to use location functionality" )
                    .setPositiveButton( "OK", new DialogInterface.OnClickListener(){
                        @Override public void onClick( DialogInterface dialogInterface, int i ){
                            //Prompt the user once explanation has been shown
                            ActivityCompat.requestPermissions( actGeocoder.this,
                                                               new String[]{ Manifest.permission.ACCESS_FINE_LOCATION },
                                                               MY_PERMISSIONS_REQUEST_LOCATION );
                        }
                    } )
                    .create()
                    .show();        }
        else{ // No explanation needed, we can request the permission.
            ActivityCompat.requestPermissions( this,
                                               new String[]{ Manifest.permission.ACCESS_FINE_LOCATION },
                                               MY_PERMISSIONS_REQUEST_LOCATION );
        }
    }
}

@Override public void onConnected( Bundle bundle ){
    mLocationRequest = new LocationRequest()
            .setInterval( 1000 )
            .setFastestInterval( 1000 )
            .setPriority( LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY );

    if ( ContextCompat.checkSelfPermission( this, Manifest.permission.ACCESS_FINE_LOCATION )
         == PackageManager.PERMISSION_GRANTED ){
                    LocationServices.FusedLocationApi.
                     requestLocationUpdates( mGoogleApiClient, mLocationRequest, this );
    }
}


private final static float ZOOM = 18;
@Override public void onLocationChanged( Location location ){//this is called only once on startup.
    //stop location updates since only current location is needed
    LocationServices.FusedLocationApi
            .removeLocationUpdates( mGoogleApiClient, this );

    LatLng latLng = new LatLng( location.getLatitude(), location.getLongitude() );
    mGoogleMap.moveCamera( CameraUpdateFactory.newLatLngZoom( latLng, ZOOM ) );

    onMapLongClick(latLng);
}


@Override public void onMapLongClick( final LatLng latLng ){
    startIntentService( latLng );

    if ( mMarker != null ) mMarker.remove();

    mMarkerOptions.position( latLng );
    mMarker = mGoogleMap.addMarker( mMarkerOptions );
}//onMapLongClick

@Override public boolean onMarkerClick( Marker marker) {
    startActivity(
            new Intent(this, actRegistration.class)
                    .putExtra( FetchAddressIntentService.LOCATION, marker.getSnippet() )
                    .putExtra( FetchAddressIntentService.LATLNG, marker.getPosition() )


                 );
    return true;
}//onMarkerClick


protected void startIntentService( final LatLng latLng ){
    // Start the service. If the service isn't already running, it is instantiated and started
    // (creating a process for it if needed); if it is running then it remains running. The
    // service kills itself automatically once all intents are processed.
    startService(
            new Intent( this, FetchAddressIntentService.class )
                    .putExtra( FetchAddressIntentService.RECEIVER, mResultReceiver )
                    .putExtra( FetchAddressIntentService.LATLNG, latLng )
                );
}//startIntentService()

protected void startRESTService( final pkRequest.pkURL aURL ){
    // Start the service. If the service isn't already running, it is instantiated and started
    // (creating a process for it if needed); if it is running then it remains running. The
    // service kills itself automatically once all intents are processed.

    startService(
            new Intent( this, RESTIntentService.class )
                    .putExtra( RESTIntentService.RECEIVER, mRESTResultReceiver )
                    .putExtra( RESTIntentService.REQUEST, aURL.name() )
                );
}//startRESTService()



//Receiver for data sent from FetchAddressIntentService.
class AddressResultReceiver extends ResultReceiver{
    public AddressResultReceiver( Handler handler ){ super( handler ); }

    //Receives data sent from FetchAddressIntentService and updates the UI in MainActivity.
    @Override protected void onReceiveResult( int resultCode, Bundle resultData ){
        mMarker.setSnippet( resultData.getString( FetchAddressIntentService.LOCATION ) );
        mMarker.showInfoWindow();
    }//onReceiveResult
}//class AddressResultReceiver

//Receiver for data sent from RESTIntentService.
class RESTResultReceiver extends ResultReceiver{
    public RESTResultReceiver( Handler handler ){ super( handler ); }

    //Receives data sent from RESTIntentService and updates the UI in MainActivity.
    @Override protected void onReceiveResult( int resultCode, Bundle resultData ){
        String snippet = resultData.getString( RESTIntentService.JSONResult );
        mLog.debug( "RESTResultReceiver:\t" + snippet );
    }//onReceiveResult
}//class RESTResultReceiver


@Override public void onConnectionSuspended( int i ){ mLog.info("onConnectionSuspended: " + i  );}
@Override public void onConnectionFailed( ConnectionResult connectionResult ){
    mLog.error( R.string.GoogleApiClientConnFailed + ":\t" + connectionResult.getErrorMessage() );
    Toast.makeText(this, R.string.GoogleApiClientConnFailed, Toast.LENGTH_LONG).show();
}
}//class actGeocoder