parent
79e925051a
commit
9b8719e2d5
@ -0,0 +1,253 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
|
||||
import android.app.DownloadManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.service.UpdateApkReadyListener;
|
||||
import org.thoughtcrime.securesms.util.FileUtils;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.jobqueue.JobParameters;
|
||||
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class UpdateApkJob extends ContextJob {
|
||||
|
||||
private static final String TAG = UpdateApkJob.class.getSimpleName();
|
||||
|
||||
public UpdateApkJob(Context context) {
|
||||
super(context, JobParameters.newBuilder()
|
||||
.withGroupId(UpdateApkJob.class.getSimpleName())
|
||||
.withRequirement(new NetworkRequirement(context))
|
||||
.withWakeLock(true)
|
||||
.withRetryCount(2)
|
||||
.create());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdded() {}
|
||||
|
||||
@Override
|
||||
public void onRun() throws IOException, PackageManager.NameNotFoundException {
|
||||
if (!BuildConfig.PLAY_STORE_DISABLED) return;
|
||||
|
||||
Log.w(TAG, "Checking for APK update...");
|
||||
|
||||
OkHttpClient client = new OkHttpClient();
|
||||
Request request = new Request.Builder().url(String.format("%s/latest.json", BuildConfig.NOPLAY_UPDATE_URL)).build();
|
||||
|
||||
Response response = client.newCall(request).execute();
|
||||
|
||||
if (!response.isSuccessful()) {
|
||||
throw new IOException("Bad response: " + response.message());
|
||||
}
|
||||
|
||||
UpdateDescriptor updateDescriptor = JsonUtils.fromJson(response.body().string(), UpdateDescriptor.class);
|
||||
byte[] digest = Hex.fromStringCondensed(updateDescriptor.getDigest());
|
||||
|
||||
Log.w(TAG, "Got descriptor: " + updateDescriptor);
|
||||
|
||||
if (updateDescriptor.getVersionCode() > getVersionCode()) {
|
||||
DownloadStatus downloadStatus = getDownloadStatus(updateDescriptor.getUrl(), digest);
|
||||
|
||||
Log.w(TAG, "Download status: " + downloadStatus.getStatus());
|
||||
|
||||
if (downloadStatus.getStatus() == DownloadStatus.Status.COMPLETE) {
|
||||
Log.w(TAG, "Download status complete, notifying...");
|
||||
handleDownloadNotify(downloadStatus.getDownloadId());
|
||||
} else if (downloadStatus.getStatus() == DownloadStatus.Status.MISSING) {
|
||||
Log.w(TAG, "Download status missing, starting download...");
|
||||
handleDownloadStart(updateDescriptor.getUrl(), updateDescriptor.getVersionName(), digest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(Exception e) {
|
||||
return e instanceof IOException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
Log.w(TAG, "Update check failed");
|
||||
}
|
||||
|
||||
private int getVersionCode() throws PackageManager.NameNotFoundException {
|
||||
PackageManager packageManager = context.getPackageManager();
|
||||
PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
|
||||
|
||||
return packageInfo.versionCode;
|
||||
}
|
||||
|
||||
private DownloadStatus getDownloadStatus(String uri, byte[] theirDigest) {
|
||||
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
DownloadManager.Query query = new DownloadManager.Query();
|
||||
|
||||
query.setFilterByStatus(DownloadManager.STATUS_PAUSED | DownloadManager.STATUS_PENDING | DownloadManager.STATUS_RUNNING | DownloadManager.STATUS_SUCCESSFUL);
|
||||
|
||||
long pendingDownloadId = TextSecurePreferences.getUpdateApkDownloadId(context);
|
||||
byte[] pendingDigest = getPendingDigest(context);
|
||||
Cursor cursor = downloadManager.query(query);
|
||||
|
||||
try {
|
||||
DownloadStatus status = new DownloadStatus(DownloadStatus.Status.MISSING, -1);
|
||||
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
int jobStatus = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
|
||||
String jobRemoteUri = cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_URI));
|
||||
long downloadId = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID));
|
||||
byte[] digest = getDigestForDownloadId(downloadId);
|
||||
|
||||
if (jobRemoteUri != null && jobRemoteUri.equals(uri) && downloadId == pendingDownloadId) {
|
||||
|
||||
if (jobStatus == DownloadManager.STATUS_SUCCESSFUL &&
|
||||
digest != null && pendingDigest != null &&
|
||||
MessageDigest.isEqual(pendingDigest, theirDigest) &&
|
||||
MessageDigest.isEqual(digest, theirDigest))
|
||||
{
|
||||
return new DownloadStatus(DownloadStatus.Status.COMPLETE, downloadId);
|
||||
} else if (jobStatus != DownloadManager.STATUS_SUCCESSFUL) {
|
||||
status = new DownloadStatus(DownloadStatus.Status.PENDING, downloadId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDownloadStart(String uri, String versionName, byte[] digest) {
|
||||
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(uri));
|
||||
|
||||
downloadRequest.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
|
||||
downloadRequest.setTitle("Downloading Signal update");
|
||||
downloadRequest.setDescription("Downloading Signal " + versionName);
|
||||
downloadRequest.setVisibleInDownloadsUi(false);
|
||||
downloadRequest.setDestinationInExternalFilesDir(context, null, "signal-update.apk");
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 11) {
|
||||
downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN);
|
||||
}
|
||||
|
||||
long downloadId = downloadManager.enqueue(downloadRequest);
|
||||
TextSecurePreferences.setUpdateApkDownloadId(context, downloadId);
|
||||
TextSecurePreferences.setUpdateApkDigest(context, Hex.toStringCondensed(digest));
|
||||
}
|
||||
|
||||
private void handleDownloadNotify(long downloadId) {
|
||||
Intent intent = new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
|
||||
intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
|
||||
|
||||
new UpdateApkReadyListener().onReceive(context, intent);
|
||||
}
|
||||
|
||||
private @Nullable byte[] getDigestForDownloadId(long downloadId) {
|
||||
try {
|
||||
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
FileInputStream fin = new FileInputStream(downloadManager.openDownloadedFile(downloadId).getFileDescriptor());
|
||||
byte[] digest = FileUtils.getFileDigest(fin);
|
||||
|
||||
fin.close();
|
||||
|
||||
return digest;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable byte[] getPendingDigest(Context context) {
|
||||
try {
|
||||
String encodedDigest = TextSecurePreferences.getUpdateApkDigest(context);
|
||||
|
||||
if (encodedDigest == null) return null;
|
||||
|
||||
return Hex.fromStringCondensed(encodedDigest);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class UpdateDescriptor {
|
||||
@JsonProperty
|
||||
private int versionCode;
|
||||
|
||||
@JsonProperty
|
||||
private String versionName;
|
||||
|
||||
@JsonProperty
|
||||
private String url;
|
||||
|
||||
@JsonProperty
|
||||
private String sha256sum;
|
||||
|
||||
|
||||
public int getVersionCode() {
|
||||
return versionCode;
|
||||
}
|
||||
|
||||
public String getVersionName() {
|
||||
return versionName;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "[" + versionCode + ", " + versionName + ", " + url + "]";
|
||||
}
|
||||
|
||||
public String getDigest() {
|
||||
return sha256sum;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DownloadStatus {
|
||||
enum Status {
|
||||
PENDING,
|
||||
COMPLETE,
|
||||
MISSING
|
||||
}
|
||||
|
||||
private final Status status;
|
||||
private final long downloadId;
|
||||
|
||||
DownloadStatus(Status status, long downloadId) {
|
||||
this.status = status;
|
||||
this.downloadId = downloadId;
|
||||
}
|
||||
|
||||
public Status getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public long getDownloadId() {
|
||||
return downloadId;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
|
||||
import android.app.DownloadManager;
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.FileUtils;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
public class UpdateApkReadyListener extends BroadcastReceiver {
|
||||
|
||||
private static final String TAG = UpdateApkReadyListener.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.w(TAG, "onReceive()");
|
||||
|
||||
if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) {
|
||||
long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -2);
|
||||
|
||||
if (downloadId == TextSecurePreferences.getUpdateApkDownloadId(context)) {
|
||||
Uri uri = getLocalUriForDownloadId(context, downloadId);
|
||||
String encodedDigest = TextSecurePreferences.getUpdateApkDigest(context);
|
||||
|
||||
if (uri == null) {
|
||||
Log.w(TAG, "Downloaded local URI is null?");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMatchingDigest(context, downloadId, encodedDigest)) {
|
||||
displayInstallNotification(context, uri);
|
||||
} else {
|
||||
Log.w(TAG, "Downloaded APK doesn't match digest...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void displayInstallNotification(Context context, Uri uri) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.setDataAndType(uri, "application/vnd.android.package-archive");
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
|
||||
|
||||
Notification notification = new NotificationCompat.Builder(context)
|
||||
.setOngoing(true)
|
||||
.setContentTitle(context.getString(R.string.UpdateApkReadyListener_Signal_update))
|
||||
.setContentText(context.getString(R.string.UpdateApkReadyListener_a_new_version_of_signal_is_available_tap_to_update))
|
||||
.setSmallIcon(R.drawable.icon_notification)
|
||||
.setColor(context.getResources().getColor(R.color.textsecure_primary))
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_REMINDER)
|
||||
.setContentIntent(pendingIntent)
|
||||
.build();
|
||||
|
||||
ServiceUtil.getNotificationManager(context).notify(666, notification);
|
||||
}
|
||||
|
||||
private @Nullable Uri getLocalUriForDownloadId(Context context, long downloadId) {
|
||||
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
DownloadManager.Query query = new DownloadManager.Query();
|
||||
query.setFilterById(downloadId);
|
||||
|
||||
Cursor cursor = downloadManager.query(query);
|
||||
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
String localUri = cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI));
|
||||
|
||||
if (localUri != null) {
|
||||
File localFile = new File(Uri.parse(localUri).getPath());
|
||||
return Uri.fromFile(localFile);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isMatchingDigest(Context context, long downloadId, String theirEncodedDigest) {
|
||||
try {
|
||||
if (theirEncodedDigest == null) return false;
|
||||
|
||||
byte[] theirDigest = Hex.fromStringCondensed(theirEncodedDigest);
|
||||
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
FileInputStream fin = new FileInputStream(downloadManager.openDownloadedFile(downloadId).getFileDescriptor());
|
||||
byte[] ourDigest = FileUtils.getFileDigest(fin);
|
||||
|
||||
fin.close();
|
||||
|
||||
return MessageDigest.isEqual(ourDigest, theirDigest);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.jobs.UpdateApkJob;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class UpdateApkRefreshListener extends PersistentAlarmManagerListener {
|
||||
|
||||
private static final String TAG = UpdateApkRefreshListener.class.getSimpleName();
|
||||
|
||||
private static final long INTERVAL = TimeUnit.HOURS.toMillis(6);
|
||||
|
||||
@Override
|
||||
protected long getNextScheduledExecutionTime(Context context) {
|
||||
return TextSecurePreferences.getUpdateApkRefreshTime(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long onAlarm(Context context, long scheduledTime) {
|
||||
Log.w(TAG, "onAlarm...");
|
||||
|
||||
if (scheduledTime != 0 && BuildConfig.PLAY_STORE_DISABLED) {
|
||||
Log.w(TAG, "Queueing APK update job...");
|
||||
ApplicationContext.getInstance(context)
|
||||
.getJobManager()
|
||||
.add(new UpdateApkJob(context));
|
||||
}
|
||||
|
||||
long newTime = System.currentTimeMillis() + INTERVAL;
|
||||
TextSecurePreferences.setUpdateApkRefreshTime(context, newTime);
|
||||
|
||||
return newTime;
|
||||
}
|
||||
|
||||
public static void schedule(Context context) {
|
||||
new UpdateApkRefreshListener().onReceive(context, new Intent());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION"/>
|
||||
|
||||
<application>
|
||||
<receiver android:name=".service.UpdateApkRefreshListener">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".service.UpdateApkReadyListener">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
</manifest>
|
Loading…
Reference in New Issue