6

My app is using images from HTTPS source, Solar Dynamics Observatory, and these images are loading just fine on Android versions with API 21 (5.0) (I think that API 20 would work as well) or higher, but I can't load these images on Android versions from API 16 (min. API that I set for my app) to API 19. Here's the code that's loading the images, it's pretty basic -

Picasso.with(this)
                .load("https://sdo.gsfc.nasa.gov/assets/img/latest/latest_2048_HMIIC.jpg")
                .placeholder(R.drawable.placeholdernew)
                .error(R.drawable.errornew)
                .into(mImageView, new com.squareup.picasso.Callback() {
                    @Override
                    public void onSuccess() {
                        mAttacher = new PhotoViewAttacher(mImageView); //this adds the zoom function after the picture is loaded successfully, so the user couldn't zoom the placeholder or error picture :)
                    }

                    @Override
                    public void onError() {
                        Log.d("Pic Error", "Loading Error");
                    }
                });

As you can see, there are no errors in my code (I think :P). I checked the SDO site with SSLLabs site, and this site said that this SDO server is not accepting TLS handshake on Android devices older than API 20. Is there any way to enable TLS 1.2 in Picasso on older Android versions? Help would be appreciated! Thanks in advance :)

  • 2
    If you add OkHttp, you can try to configure OkHttp to support TLS 1.2 as outlined in [some of these answers](http://stackoverflow.com/q/28943660/115145), then have Picasso use your `OkHttpClient`. I'll try to work out some code for this early next week. Many thanks for including a real URL, as it will help to test this. – CommonsWare Feb 10 '17 at 19:59
  • Thank you for this. If you want to test the images, just click on any sun image (I'm using 2048 images and 512 images in my app if it does matter). I'm waiting for the code now :) –  Feb 10 '17 at 21:44

1 Answers1

10

Well, this works, but it's ugly.

In your project's dependencies, add:

compile 'com.squareup.picasso:picasso:2.5.2'
compile 'com.squareup.okhttp3:okhttp:3.6.0'
compile 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.0.2'

(you may already have the picasso line — I am just making sure that you are on the latest version)

Next, add this class to your project (based on this answer):

  public static class TLSSocketFactory extends SSLSocketFactory {

    private SSLSocketFactory internalSSLSocketFactory;

    public TLSSocketFactory(SSLSocketFactory delegate) throws
      KeyManagementException, NoSuchAlgorithmException {
      internalSSLSocketFactory = delegate;
    }

    @Override
    public String[] getDefaultCipherSuites() {
      return internalSSLSocketFactory.getDefaultCipherSuites();
    }

    @Override
    public String[] getSupportedCipherSuites() {
      return internalSSLSocketFactory.getSupportedCipherSuites();
    }

    @Override
    public Socket createSocket(Socket s, String host, int port, boolean autoClose)
      throws IOException {
      return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose));
    }

    @Override
    public Socket createSocket(String host, int port) throws IOException {
      return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
    }

    @Override
    public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
      return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort));
    }

    @Override
    public Socket createSocket(InetAddress host, int port) throws IOException {
      return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
    }

    @Override
    public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
      return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort));
    }

    /*
     * Utility methods
     */

    private static Socket enableTLSOnSocket(Socket socket) {
      if (socket != null && (socket instanceof SSLSocket)
        && isTLSServerEnabled((SSLSocket) socket)) { // skip the fix if server doesn't provide there TLS version
        ((SSLSocket) socket).setEnabledProtocols(new String[]{"TLSv1.1", "TLSv1.2"});
      }
      return socket;
    }

    private static boolean isTLSServerEnabled(SSLSocket sslSocket) {
      System.out.println("__prova__ :: " + sslSocket.getSupportedProtocols().toString());
      for (String protocol : sslSocket.getSupportedProtocols()) {
        if (protocol.equals("TLSv1.1") || protocol.equals("TLSv1.2")) {
          return true;
        }
      }
      return false;
    }
  }

(that class is public static, and so is designed to be a nested class inside something else — just get rid of the static if you want it to be a standalone class)

Then, in your class that is using Picasso, add this method, based on this issue comment:

  public X509TrustManager provideX509TrustManager() {
    try {
      TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
      factory.init((KeyStore) null);
      TrustManager[] trustManagers = factory.getTrustManagers();
      return (X509TrustManager) trustManagers[0];
    }
    catch (NoSuchAlgorithmException exception) {
      Log.e(getClass().getSimpleName(), "not trust manager available", exception);
    }
    catch (KeyStoreException exception) {
      Log.e(getClass().getSimpleName(), "not trust manager available", exception);
    }

    return null;
  }

Finally, this code should successfully download your image:

    SSLContext sslContext=SSLContext.getInstance("TLS");
    sslContext.init(null, null, null);
    SSLSocketFactory noSSLv3Factory;

    if (Build.VERSION.SDK_INT<=Build.VERSION_CODES.KITKAT) {
      noSSLv3Factory=new TLSSocketFactory(sslContext.getSocketFactory());
    }
    else {
      noSSLv3Factory=sslContext.getSocketFactory();
    }

    OkHttpClient.Builder okb=new OkHttpClient.Builder()
      .sslSocketFactory(noSSLv3Factory, provideX509TrustManager());
    OkHttpClient ok=okb.build();

    Picasso p=new Picasso.Builder(getActivity())
      .downloader(new OkHttp3Downloader(ok))
      .build();

    p.load(
      "https://sdo.gsfc.nasa.gov/assets/img/latest/latest_2048_HMIIC.jpg")
      .fit().centerCrop()
      .placeholder(R.drawable.owner_placeholder)
      .error(R.drawable.owner_error).into(icon);

(where you would replace my fit() and subsequent calls with the right ones for your project)

If you happen to know the people maintaining that NASA server... they really ought to upgrade their SSL support. Just sayin'.

Community
  • 1
  • 1
CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • Wow, thank you very much! I'll test it tomorrow, and let you know if it's working :) –  Feb 12 '17 at 00:31
  • Thank you again, image loading is actually working on old devices! :) –  Feb 12 '17 at 13:32
  • I'm getting `NoClassDefFoundError okio.Buffer` on `OkHttpClient.Builder okb = new OkHttpClient.Builder()`. I have included all three libraries: `picasso 2.5.2`, `okhttp3.10.0` and `picasso2-okhttp3-downloader-1.0.1`. Not sure if it mattersI'm running my project on eclipse and included the libraries in the libs folder. – Abbas May 10 '18 at 09:27
  • @Abbas: Eclipse development is no longer supported by Google. You are going to need to track down manually each of the dependencies used by those libraries, and add those libraries as well. One is `okio`, which is the source of the `NoClassDefError` that you are seeing. – CommonsWare May 10 '18 at 10:33
  • Thanks this resolved the crash, but the images still didn't load. However I was able to make it work with the same code. Earlier I tried holding Picasso object in a static class and use that but when that didn't work I put the same code in a separate file with its own static picasso object (A singleton class) and surprisingly that worked. Any idea why would a singleton work but not otherwise. P.S. Earlier the code was in main activity's `onCreate()` could that mess it up? – Abbas May 10 '18 at 12:24
  • I realize not much info can be posted in comments. But I thought may be you'd think up a possible reason for it. – Abbas May 10 '18 at 12:26
  • @Abbas: If you have not done so already, set up an error listener on Picasso, so you can get the `Exception` if there is a problem and log it, as that may give you more clues. Beyond that, you might want to ask a separate Stack Overflow question, one with a [mcve], showing what you have done and explaining in detail what your symptoms are, such as what "didn't load" means. – CommonsWare May 10 '18 at 14:14
  • For some reason and I don't know why, but this does not work with Picasso 2.71828 – Daniel Wilson Oct 11 '18 at 17:19
  • You are the God! – Homayoon Ahmadi Feb 19 '19 at 21:38