restructure and unite service android/java to libsignal

This commit is contained in:
Ryan ZHAO
2020-11-26 09:46:52 +11:00
parent 673d35625b
commit 7a66a47520
3790 changed files with 101955 additions and 74 deletions

View File

@@ -0,0 +1,75 @@
package org.thoughtcrime.securesms.glide;
import androidx.annotation.NonNull;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.data.DataFetcher;
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.net.ChunkedDataFetcher;
import org.thoughtcrime.securesms.net.RequestController;
import java.io.InputStream;
import okhttp3.OkHttpClient;
class ChunkedImageUrlFetcher implements DataFetcher<InputStream> {
private static final String TAG = ChunkedImageUrlFetcher.class.getSimpleName();
private final OkHttpClient client;
private final ChunkedImageUrl url;
private RequestController requestController;
ChunkedImageUrlFetcher(@NonNull OkHttpClient client, @NonNull ChunkedImageUrl url) {
this.client = client;
this.url = url;
}
@Override
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
ChunkedDataFetcher fetcher = new ChunkedDataFetcher(client);
requestController = fetcher.fetch(url.getUrl(), url.getSize(), new ChunkedDataFetcher.Callback() {
@Override
public void onSuccess(InputStream stream) {
callback.onDataReady(stream);
}
@Override
public void onFailure(Exception e) {
callback.onLoadFailed(e);
}
});
}
@Override
public void cleanup() {
if (requestController != null) {
requestController.cancel();
}
}
@Override
public void cancel() {
Log.d(TAG, "Canceled.");
if (requestController != null) {
requestController.cancel();
}
}
@NonNull
@Override
public Class<InputStream> getDataClass() {
return InputStream.class;
}
@NonNull
@Override
public DataSource getDataSource() {
return DataSource.REMOTE;
}
}

View File

@@ -0,0 +1,58 @@
package org.thoughtcrime.securesms.glide;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.model.ModelLoader;
import com.bumptech.glide.load.model.ModelLoaderFactory;
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
import org.thoughtcrime.securesms.net.ContentProxySafetyInterceptor;
import org.thoughtcrime.securesms.net.ContentProxySelector;
import java.io.InputStream;
import okhttp3.OkHttpClient;
public class ChunkedImageUrlLoader implements ModelLoader<ChunkedImageUrl, InputStream> {
private final OkHttpClient client;
private ChunkedImageUrlLoader(OkHttpClient client) {
this.client = client;
}
@Override
public @Nullable LoadData<InputStream> buildLoadData(@NonNull ChunkedImageUrl url, int width, int height, @NonNull Options options) {
return new LoadData<>(url, new ChunkedImageUrlFetcher(client, url));
}
@Override
public boolean handles(@NonNull ChunkedImageUrl url) {
return true;
}
public static class Factory implements ModelLoaderFactory<ChunkedImageUrl, InputStream> {
private final OkHttpClient client;
public Factory() {
this.client = new OkHttpClient.Builder()
.proxySelector(new ContentProxySelector())
.cache(null)
.addNetworkInterceptor(new ContentProxySafetyInterceptor())
.addNetworkInterceptor(new PaddedHeadersInterceptor())
.build();
}
@Override
public @NonNull ModelLoader<ChunkedImageUrl, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
return new ChunkedImageUrlLoader(client);
}
@Override
public void teardown() {}
}
}

View File

@@ -0,0 +1,59 @@
package org.thoughtcrime.securesms.glide;
import android.content.Context;
import androidx.annotation.NonNull;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.data.DataFetcher;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import java.io.IOException;
import java.io.InputStream;
class ContactPhotoFetcher implements DataFetcher<InputStream> {
private final Context context;
private final ContactPhoto contactPhoto;
private InputStream inputStream;
ContactPhotoFetcher(@NonNull Context context, @NonNull ContactPhoto contactPhoto) {
this.context = context.getApplicationContext();
this.contactPhoto = contactPhoto;
}
@Override
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
try {
inputStream = contactPhoto.openInputStream(context);
callback.onDataReady(inputStream);
} catch (IOException e) {
callback.onLoadFailed(e);
}
}
@Override
public void cleanup() {
try {
if (inputStream != null) inputStream.close();
} catch (IOException e) {}
}
@Override
public void cancel() {
}
@Override
public @NonNull Class<InputStream> getDataClass() {
return InputStream.class;
}
@Override
public @NonNull DataSource getDataSource() {
return DataSource.LOCAL;
}
}

View File

@@ -0,0 +1,50 @@
package org.thoughtcrime.securesms.glide;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.model.ModelLoader;
import com.bumptech.glide.load.model.ModelLoaderFactory;
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import java.io.InputStream;
public class ContactPhotoLoader implements ModelLoader<ContactPhoto, InputStream> {
private final Context context;
private ContactPhotoLoader(Context context) {
this.context = context;
}
@Override
public @Nullable LoadData<InputStream> buildLoadData(@NonNull ContactPhoto contactPhoto, int width, int height, @NonNull Options options) {
return new LoadData<>(contactPhoto, new ContactPhotoFetcher(context, contactPhoto));
}
@Override
public boolean handles(@NonNull ContactPhoto contactPhoto) {
return true;
}
public static class Factory implements ModelLoaderFactory<ContactPhoto, InputStream> {
private final Context context;
public Factory(Context context) {
this.context = context.getApplicationContext();
}
@Override
public @NonNull ModelLoader<ContactPhoto, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
return new ContactPhotoLoader(context);
}
@Override
public void teardown() {}
}
}

View File

@@ -0,0 +1,94 @@
package org.thoughtcrime.securesms.glide;
import androidx.annotation.NonNull;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.data.DataFetcher;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.util.ContentLengthInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
/**
* Fetches an {@link InputStream} using the okhttp library.
*/
class OkHttpStreamFetcher implements DataFetcher<InputStream> {
private static final String TAG = OkHttpStreamFetcher.class.getSimpleName();
private final OkHttpClient client;
private final GlideUrl url;
private InputStream stream;
private ResponseBody responseBody;
OkHttpStreamFetcher(OkHttpClient client, GlideUrl url) {
this.client = client;
this.url = url;
}
@Override
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
try {
Request.Builder requestBuilder = new Request.Builder()
.url(url.toStringUrl());
for (Map.Entry<String, String> headerEntry : url.getHeaders().entrySet()) {
String key = headerEntry.getKey();
requestBuilder.addHeader(key, headerEntry.getValue());
}
Request request = requestBuilder.build();
Response response = client.newCall(request).execute();
responseBody = response.body();
if (!response.isSuccessful()) {
throw new IOException("Request failed with code: " + response.code());
}
long contentLength = responseBody.contentLength();
stream = ContentLengthInputStream.obtain(responseBody.byteStream(), contentLength);
callback.onDataReady(stream);
} catch (IOException e) {
callback.onLoadFailed(e);
}
}
@Override
public void cleanup() {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
// Ignored
}
}
if (responseBody != null) {
responseBody.close();
}
}
@Override
public void cancel() {
// TODO: call cancel on the client when this method is called on a background thread. See #257
}
@Override
public @NonNull Class<InputStream> getDataClass() {
return InputStream.class;
}
@Override
public @NonNull DataSource getDataSource() {
return DataSource.REMOTE;
}
}

View File

@@ -0,0 +1,74 @@
package org.thoughtcrime.securesms.glide;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.load.model.ModelLoader;
import com.bumptech.glide.load.model.ModelLoaderFactory;
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
import org.thoughtcrime.securesms.net.ContentProxySelector;
import java.io.InputStream;
import okhttp3.OkHttpClient;
/**
* A simple model loader for fetching media over http/https using OkHttp.
*/
public class OkHttpUrlLoader implements ModelLoader<GlideUrl, InputStream> {
private final OkHttpClient client;
private OkHttpUrlLoader(OkHttpClient client) {
this.client = client;
}
@Override
public @Nullable LoadData<InputStream> buildLoadData(@NonNull GlideUrl glideUrl, int width, int height, @NonNull Options options) {
return new LoadData<>(glideUrl, new OkHttpStreamFetcher(client, glideUrl));
}
@Override
public boolean handles(@NonNull GlideUrl glideUrl) {
return true;
}
public static class Factory implements ModelLoaderFactory<GlideUrl, InputStream> {
private static volatile OkHttpClient internalClient;
private OkHttpClient client;
private static OkHttpClient getInternalClient() {
if (internalClient == null) {
synchronized (Factory.class) {
if (internalClient == null) {
internalClient = new OkHttpClient.Builder()
.proxySelector(new ContentProxySelector())
.build();
}
}
}
return internalClient;
}
public Factory() {
this(getInternalClient());
}
private Factory(OkHttpClient client) {
this.client = client;
}
@Override
public @NonNull ModelLoader<GlideUrl, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
return new OkHttpUrlLoader(client);
}
@Override
public void teardown() {
// Do nothing, this instance doesn't own the client.
}
}
}

View File

@@ -0,0 +1,46 @@
package org.thoughtcrime.securesms.glide;
import androidx.annotation.NonNull;
import java.io.IOException;
import java.security.SecureRandom;
import okhttp3.Headers;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
/**
* An interceptor that adds a header with a random amount of bytes to disguise header length.
*/
public class PaddedHeadersInterceptor implements Interceptor {
private static final String PADDING_HEADER = "X-SignalPadding";
private static final int MIN_RANDOM_BYTES = 1;
private static final int MAX_RANDOM_BYTES = 64;
@Override
public @NonNull Response intercept(@NonNull Chain chain) throws IOException {
Request padded = chain.request().newBuilder()
.headers(getPaddedHeaders(chain.request().headers()))
.build();
return chain.proceed(padded);
}
private @NonNull Headers getPaddedHeaders(@NonNull Headers headers) {
return headers.newBuilder()
.add(PADDING_HEADER, getRandomString(new SecureRandom(), MIN_RANDOM_BYTES, MAX_RANDOM_BYTES))
.build();
}
private static @NonNull String getRandomString(@NonNull SecureRandom secureRandom, int minLength, int maxLength) {
char[] buffer = new char[secureRandom.nextInt(maxLength - minLength) + minLength];
for (int i = 0 ; i < buffer.length; i++) {
buffer[i] = (char) (secureRandom.nextInt(74) + 48); // Random char from 0-Z
}
return new String(buffer);
}
}

View File

@@ -0,0 +1,54 @@
package org.thoughtcrime.securesms.glide.cache;
import android.graphics.Bitmap;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.logging.Log;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.ResourceDecoder;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
public class EncryptedBitmapCacheDecoder extends EncryptedCoder implements ResourceDecoder<File, Bitmap> {
private static final String TAG = EncryptedBitmapCacheDecoder.class.getSimpleName();
private final StreamBitmapDecoder streamBitmapDecoder;
private final byte[] secret;
public EncryptedBitmapCacheDecoder(@NonNull byte[] secret, @NonNull StreamBitmapDecoder streamBitmapDecoder) {
this.secret = secret;
this.streamBitmapDecoder = streamBitmapDecoder;
}
@Override
public boolean handles(@NonNull File source, @NonNull Options options)
throws IOException
{
Log.i(TAG, "Checking item for encrypted Bitmap cache decoder: " + source.toString());
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
return streamBitmapDecoder.handles(inputStream, options);
} catch (IOException e) {
Log.w(TAG, e);
return false;
}
}
@Nullable
@Override
public Resource<Bitmap> decode(@NonNull File source, int width, int height, @NonNull Options options)
throws IOException
{
Log.i(TAG, "Encrypted Bitmap cache decoder running: " + source.toString());
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
return streamBitmapDecoder.decode(inputStream, width, height, options);
}
}
}

View File

@@ -0,0 +1,65 @@
package org.thoughtcrime.securesms.glide.cache;
import android.graphics.Bitmap;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.logging.Log;
import com.bumptech.glide.load.EncodeStrategy;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.ResourceEncoder;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.resource.bitmap.BitmapEncoder;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
public class EncryptedBitmapResourceEncoder extends EncryptedCoder implements ResourceEncoder<Bitmap> {
private static final String TAG = EncryptedBitmapResourceEncoder.class.getSimpleName();
private final byte[] secret;
public EncryptedBitmapResourceEncoder(@NonNull byte[] secret) {
this.secret = secret;
}
@Override
public EncodeStrategy getEncodeStrategy(@NonNull Options options) {
return EncodeStrategy.TRANSFORMED;
}
@SuppressWarnings("EmptyCatchBlock")
@Override
public boolean encode(@NonNull Resource<Bitmap> data, @NonNull File file, @NonNull Options options) {
Log.i(TAG, "Encrypted resource encoder running: " + file.toString());
Bitmap bitmap = data.get();
Bitmap.CompressFormat format = getFormat(bitmap, options);
int quality = options.get(BitmapEncoder.COMPRESSION_QUALITY);
try (OutputStream os = createEncryptedOutputStream(secret, file)) {
bitmap.compress(format, quality, os);
os.close();
return true;
} catch (IOException e) {
Log.w(TAG, e);
return false;
}
}
private Bitmap.CompressFormat getFormat(Bitmap bitmap, Options options) {
Bitmap.CompressFormat format = options.get(BitmapEncoder.COMPRESSION_FORMAT);
if (format != null) {
return format;
} else if (bitmap.hasAlpha()) {
return Bitmap.CompressFormat.PNG;
} else {
return Bitmap.CompressFormat.JPEG;
}
}
}

View File

@@ -0,0 +1,53 @@
package org.thoughtcrime.securesms.glide.cache;
import androidx.annotation.NonNull;
import com.bumptech.glide.load.Encoder;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
import org.thoughtcrime.securesms.logging.Log;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class EncryptedCacheEncoder extends EncryptedCoder implements Encoder<InputStream> {
private static final String TAG = EncryptedCacheEncoder.class.getSimpleName();
private final byte[] secret;
private final ArrayPool byteArrayPool;
public EncryptedCacheEncoder(@NonNull byte[] secret, @NonNull ArrayPool byteArrayPool) {
this.secret = secret;
this.byteArrayPool = byteArrayPool;
}
@SuppressWarnings("EmptyCatchBlock")
@Override
public boolean encode(@NonNull InputStream data, @NonNull File file, @NonNull Options options) {
Log.i(TAG, "Encrypted cache encoder running: " + file.toString());
byte[] buffer = byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class);
try (OutputStream outputStream = createEncryptedOutputStream(secret, file)) {
int read;
while ((read = data.read(buffer)) != -1) {
outputStream.write(buffer, 0, read);
}
return true;
} catch (IOException e) {
Log.w(TAG, e);
return false;
} finally {
byteArrayPool.put(buffer);
}
}
}

View File

@@ -0,0 +1,98 @@
package org.thoughtcrime.securesms.glide.cache;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.util.Util;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
class EncryptedCoder {
private static byte[] MAGIC_BYTES = {(byte)0x91, (byte)0x5e, (byte)0x6d, (byte)0xb4,
(byte)0x09, (byte)0xa6, (byte)0x68, (byte)0xbe,
(byte)0xe5, (byte)0xb1, (byte)0x1b, (byte)0xd7,
(byte)0x29, (byte)0xe5, (byte)0x04, (byte)0xcc};
OutputStream createEncryptedOutputStream(@NonNull byte[] masterKey, @NonNull File file)
throws IOException
{
try {
byte[] random = Util.getSecretBytes(32);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(masterKey, "HmacSHA256"));
FileOutputStream fileOutputStream = new FileOutputStream(file);
byte[] iv = new byte[16];
byte[] key = mac.doFinal(random);
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
fileOutputStream.write(MAGIC_BYTES);
fileOutputStream.write(random);
CipherOutputStream outputStream = new CipherOutputStream(fileOutputStream, cipher);
outputStream.write(MAGIC_BYTES);
return outputStream;
} catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
}
}
InputStream createEncryptedInputStream(@NonNull byte[] masterKey, @NonNull File file) throws IOException {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(masterKey, "HmacSHA256"));
FileInputStream fileInputStream = new FileInputStream(file);
byte[] theirMagic = new byte[MAGIC_BYTES.length];
byte[] theirRandom = new byte[32];
byte[] theirEncryptedMagic = new byte[MAGIC_BYTES.length];
Util.readFully(fileInputStream, theirMagic);
Util.readFully(fileInputStream, theirRandom);
if (!MessageDigest.isEqual(theirMagic, MAGIC_BYTES)) {
throw new IOException("Not an encrypted cache file!");
}
byte[] iv = new byte[16];
byte[] key = mac.doFinal(theirRandom);
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
CipherInputStream inputStream = new CipherInputStream(fileInputStream, cipher);
Util.readFully(inputStream, theirEncryptedMagic);
if (!MessageDigest.isEqual(theirEncryptedMagic, MAGIC_BYTES)) {
throw new IOException("Key change on encrypted cache file!");
}
return inputStream;
} catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -0,0 +1,51 @@
package org.thoughtcrime.securesms.glide.cache;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.logging.Log;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.ResourceDecoder;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.resource.gif.GifDrawable;
import com.bumptech.glide.load.resource.gif.StreamGifDecoder;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
public class EncryptedGifCacheDecoder extends EncryptedCoder implements ResourceDecoder<File, GifDrawable> {
private static final String TAG = EncryptedGifCacheDecoder.class.getSimpleName();
private final byte[] secret;
private final StreamGifDecoder gifDecoder;
public EncryptedGifCacheDecoder(@NonNull byte[] secret, @NonNull StreamGifDecoder gifDecoder) {
this.secret = secret;
this.gifDecoder = gifDecoder;
}
@Override
public boolean handles(@NonNull File source, @NonNull Options options) {
Log.i(TAG, "Checking item for encrypted GIF cache decoder: " + source.toString());
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
return gifDecoder.handles(inputStream, options);
} catch (IOException e) {
Log.w(TAG, e);
return false;
}
}
@Nullable
@Override
public Resource<GifDrawable> decode(@NonNull File source, int width, int height, @NonNull Options options) throws IOException {
Log.i(TAG, "Encrypted GIF cache decoder running...");
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
return gifDecoder.decode(inputStream, width, height, options);
}
}
}

View File

@@ -0,0 +1,45 @@
package org.thoughtcrime.securesms.glide.cache;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.logging.Log;
import com.bumptech.glide.load.EncodeStrategy;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.ResourceEncoder;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.resource.gif.GifDrawable;
import com.bumptech.glide.util.ByteBufferUtil;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
public class EncryptedGifDrawableResourceEncoder extends EncryptedCoder implements ResourceEncoder<GifDrawable> {
private static final String TAG = EncryptedGifDrawableResourceEncoder.class.getSimpleName();
private final byte[] secret;
public EncryptedGifDrawableResourceEncoder(@NonNull byte[] secret) {
this.secret = secret;
}
@Override
public EncodeStrategy getEncodeStrategy(@NonNull Options options) {
return EncodeStrategy.TRANSFORMED;
}
@Override
public boolean encode(@NonNull Resource<GifDrawable> data, @NonNull File file, @NonNull Options options) {
GifDrawable drawable = data.get();
try (OutputStream outputStream = createEncryptedOutputStream(secret, file)) {
ByteBufferUtil.toStream(drawable.getBuffer(), outputStream);
return true;
} catch (IOException e) {
Log.w(TAG, e);
return false;
}
}
}