/*
* This file is part of Popcorn Time.
*
* Popcorn Time is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Popcorn Time is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Popcorn Time. If not, see <http://www.gnu.org/licenses/>.
*/
package pct.droid.base.updater;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import com.google.gson.Gson;
import com.squareup.okhttp.Callback;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.util.Locale;
import java.util.Map;
import java.util.Observable;
import java.util.zip.CRC32;
import java.util.zip.Checksum;
import okio.BufferedSink;
import okio.Okio;
import pct.droid.base.BuildConfig;
import pct.droid.base.Constants;
import pct.droid.base.PopcornApplication;
import pct.droid.base.content.preferences.Prefs;
import pct.droid.base.utils.NetworkUtils;
import pct.droid.base.utils.PrefUtils;
import pct.droid.base.utils.VersionUtils;
public class PopcornUpdater extends Observable {
private static PopcornUpdater sThis;
public static int NOTIFICATION_ID = 0xDEADBEEF;
public final String STATUS_CHECKING = "checking_updates";
public final String STATUS_NO_UPDATE = "no_updates";
public final String STATUS_GOT_UPDATE = "got_update";
public final String STATUS_HAVE_UPDATE = "have_update";
private final long MINUTES = 60 * 1000;
private final long HOURS = 60 * MINUTES;
private final long DAYS = 24 * HOURS;
private final long WAKEUP_INTERVAL = 15 * MINUTES;
private long UPDATE_INTERVAL = 3 * HOURS;
public static final String ANDROID_PACKAGE = "application/vnd.android.package-archive";
private final String DATA_URLS[] = {"http://popcorntimece.tk/android.json","http://popcorntime.ml/android.json"};
private Integer mCurrentUrl = 0;
public static final String LAST_UPDATE_CHECK = "update_check";
private static final String LAST_UPDATE_KEY = "last_update";
public static final String UPDATE_FILE = "update_file";
private static final String SHA1_TIME = "sha1_update_time";
private static final String SHA1_KEY = "sha1_update";
private final OkHttpClient mHttpClient = PopcornApplication.getHttpClient();
private final Gson mGson = new Gson();
private final Handler mUpdateHandler = new Handler();
private Context mContext = null;
private long lastUpdate = 0;
private String mPackageName;
private Integer mVersionCode;
private String mVariantStr;
private String mChannelStr;
private String mAbi;
private Listener mListener;
private PopcornUpdater(Context context) {
if (Constants.DEBUG_ENABLED) {
UPDATE_INTERVAL = 3 * HOURS;
} else {
UPDATE_INTERVAL = 2 * DAYS;
}
mContext = context;
mPackageName = context.getPackageName();
try {
PackageInfo pInfo = context.getPackageManager().getPackageInfo(mPackageName, 0);
mVersionCode = pInfo.versionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
lastUpdate = PrefUtils.get(mContext, LAST_UPDATE_KEY, 0l);
NOTIFICATION_ID += crc32(mPackageName);
ApplicationInfo appinfo = context.getApplicationInfo();
if (new File(appinfo.sourceDir).lastModified() > PrefUtils.get(mContext, SHA1_TIME, 0l)) {
PrefUtils.save(mContext, SHA1_KEY, SHA1(appinfo.sourceDir));
PrefUtils.save(mContext, SHA1_TIME, System.currentTimeMillis());
String updateFile = PrefUtils.get(mContext, UPDATE_FILE, "");
if (updateFile.length() > 0) {
if (new File(updateFile).delete()) {
PrefUtils.remove(mContext, UPDATE_FILE);
}
}
}
context.registerReceiver(mConnectivityReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
}
public static PopcornUpdater getInstance(Context context) {
if (sThis == null) {
sThis = new PopcornUpdater(context);
}
sThis.mContext = context;
return sThis;
}
public static PopcornUpdater getInstance(Context context, Listener listener) {
PopcornUpdater instance = getInstance(context);
instance.setListener(listener);
return instance;
}
public void setListener(Listener listener) {
mListener = listener;
}
private Runnable periodicUpdate = new Runnable() {
@Override
public void run() {
checkUpdates(false);
mUpdateHandler.removeCallbacks(periodicUpdate);
mUpdateHandler.postDelayed(this, WAKEUP_INTERVAL);
}
};
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void checkUpdates(boolean forced) {
long now = System.currentTimeMillis();
if ((!PrefUtils.get(mContext, Prefs.AUTOMATIC_UPDATES, true) || (PrefUtils.get(mContext, Prefs.WIFI_ONLY, true) && !NetworkUtils.isWifiConnected(mContext))) && !forced) {
return;
}
PrefUtils.save(mContext, LAST_UPDATE_CHECK, now);
if (forced || (lastUpdate + UPDATE_INTERVAL) < now) {
lastUpdate = System.currentTimeMillis();
PrefUtils.save(mContext, LAST_UPDATE_KEY, lastUpdate);
if (!forced && BuildConfig.GIT_BRANCH.contains("local")) return;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
mAbi = Build.CPU_ABI.toLowerCase(Locale.US);
} else {
mAbi = Build.SUPPORTED_ABIS[0].toLowerCase(Locale.US);
}
if (mPackageName.contains("tv")) {
mVariantStr = "tv";
} else {
mVariantStr = "mobile";
}
if (BuildConfig.RELEASE_TYPE.toLowerCase(Locale.US).contains("release")) {
mChannelStr = "release";
} else {
mChannelStr = BuildConfig.GIT_BRANCH;
}
Request request = new Request.Builder()
.url(DATA_URLS[mCurrentUrl] + "/" + mVariantStr)
.build();
mHttpClient.newCall(request).enqueue(mCallback);
} else if(PrefUtils.contains(mContext, UPDATE_FILE)) {
String fileName = PrefUtils.get(mContext, UPDATE_FILE, "");
if (fileName.length() > 0) {
if (!new File(fileName).exists()) {
PrefUtils.remove(mContext, UPDATE_FILE);
} else {
if(mListener != null)
mListener.updateAvailable(fileName);
}
}
}
}
Callback mCallback = new Callback() {
@Override
public void onFailure(Request request, IOException e) {
if(mCurrentUrl < DATA_URLS.length - 1) {
mCurrentUrl++;
Request newRequest = new Request.Builder()
.url(DATA_URLS[mCurrentUrl] + "/" + mVariantStr)
.build();
mHttpClient.newCall(newRequest).enqueue(mCallback);
} else {
setChanged();
notifyObservers(STATUS_NO_UPDATE);
}
}
@Override
public void onResponse(Response response) {
try {
if (response.isSuccessful()) {
UpdaterData data = mGson.fromJson(response.body().string(), UpdaterData.class);
Map<String, Map<String, UpdaterData.Arch>> variant;
if (mVariantStr.equals("tv")) {
variant = data.tv;
} else {
variant = data.mobile;
}
UpdaterData.Arch channel = null;
if (variant.containsKey(mChannelStr) && variant.get(mChannelStr).containsKey(mAbi)) {
channel = variant.get(mChannelStr).get(mAbi);
}
ApplicationInfo appinfo = mContext.getApplicationInfo();
if ((channel == null || channel.checksum.equals(SHA1(appinfo.sourceDir)) || channel.versionCode <= mVersionCode) && VersionUtils.isUsingCorrectBuild()) {
setChanged();
notifyObservers(STATUS_NO_UPDATE);
} else {
downloadFile(channel.updateUrl);
setChanged();
notifyObservers(STATUS_GOT_UPDATE);
}
} else {
setChanged();
notifyObservers(STATUS_NO_UPDATE);
}
} catch (Exception e) {
e.printStackTrace();
}
}
};
private void downloadFile(final String location) {
Request request = new Request.Builder()
.url(location)
.build();
mHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Request request, IOException e) {
// uhoh
}
@Override
public void onResponse(Response response) throws IOException {
if (response.isSuccessful()) {
String fileName = location.substring(location.lastIndexOf('/') + 1);
File downloadedFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName);
BufferedSink sink = Okio.buffer(Okio.sink(downloadedFile));
sink.writeAll(response.body().source());
sink.close();
String updateFilePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath() + "/" + fileName;
PrefUtils.getPrefs(mContext).edit()
.putString(SHA1_KEY, SHA1(updateFilePath))
.putString(UPDATE_FILE, updateFilePath)
.putLong(SHA1_TIME, System.currentTimeMillis())
.apply();
if(mListener != null) {
mListener.updateAvailable(updateFilePath);
}
}
}
});
}
public void checkUpdatesManually() {
checkUpdates(true);
}
private BroadcastReceiver mConnectivityReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// do application-specific task(s) based on the current network state, such
// as enabling queuing of HTTP requests when currentNetworkInfo is connected etc.
if (NetworkUtils.isWifiConnected(context)) {
checkUpdates(false);
mUpdateHandler.postDelayed(periodicUpdate, UPDATE_INTERVAL);
} else {
mUpdateHandler.removeCallbacks(periodicUpdate); // no network anyway
}
}
};
private String SHA1(String filename) {
final int BUFFER_SIZE = 8192;
byte[] buf = new byte[BUFFER_SIZE];
int length;
try {
FileInputStream fis = new FileInputStream(filename);
BufferedInputStream bis = new BufferedInputStream(fis);
MessageDigest md = MessageDigest.getInstance("SHA1");
while ((length = bis.read(buf)) != -1) {
md.update(buf, 0, length);
}
byte[] array = md.digest();
StringBuilder sb = new StringBuilder();
for (byte anArray : array) {
sb.append(Integer.toHexString((anArray & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString();
} catch (Exception e) {
e.printStackTrace();
}
return "sha1bad";
}
private static int crc32(String str) {
byte bytes[] = str.getBytes();
Checksum checksum = new CRC32();
checksum.update(bytes, 0, bytes.length);
return (int) checksum.getValue();
}
public interface Listener {
void updateAvailable(String fileName);
}
}