diff --git a/app/build.gradle b/app/build.gradle index f6096214..8b9632e6 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -66,11 +66,13 @@ android { dimension "repo" // versionNameSuffix "-github" // appended in assemble task buildConfigField("String", "dsn", SENTRY_DSN) + buildConfigField("boolean", "isPre", "false") } fdroid { dimension "repo" versionNameSuffix "-fdroid" + buildConfigField("boolean", "isPre", "false") } } @@ -84,6 +86,7 @@ android { def suffix = "${versionName}-${flavor}_${builtType}" // eg. 19.1.0-github_debug or release if (builtType.toString() == 'release' && project.hasProperty("pre")) { + buildConfigField("boolean", "isPre", "true") // append latest commit short hash for pre-release suffix = "${versionName}.${getGitHash()}-${flavor}" // eg. 19.1.0.b123456-github } diff --git a/app/src/fdroid/java/awais/instagrabber/utils/UpdateChecker.java b/app/src/fdroid/java/awais/instagrabber/utils/UpdateChecker.java new file mode 100644 index 00000000..f25b8aa4 --- /dev/null +++ b/app/src/fdroid/java/awais/instagrabber/utils/UpdateChecker.java @@ -0,0 +1,64 @@ +package awais.instagrabber.utils; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import org.json.JSONObject; + +import java.net.HttpURLConnection; +import java.net.URL; + +public class UpdateChecker { + private static final Object LOCK = new Object(); + private static final String TAG = UpdateChecker.class.getSimpleName(); + + private static UpdateChecker instance; + + public static UpdateChecker getInstance() { + if (instance == null) { + synchronized (LOCK) { + if (instance == null) { + instance = new UpdateChecker(); + } + } + } + return instance; + } + + /** + * Needs to be called asynchronously + * + * @return the latest version from f-droid + */ + @Nullable + public String getLatestVersion() { + HttpURLConnection conn = null; + try { + conn = (HttpURLConnection) new URL("https://f-droid.org/api/v1/packages/me.austinhuang.instagrabber").openConnection(); + conn.setUseCaches(false); + conn.setRequestProperty("User-Agent", "https://Barinsta.AustinHuang.me / mailto:Barinsta@AustinHuang.me"); + conn.connect(); + final int responseCode = conn.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_OK) { + final JSONObject data = new JSONObject(NetworkUtils.readFromConnection(conn)); + return "v" + data.getJSONArray("packages").getJSONObject(0).getString("versionName"); + // if (BuildConfig.VERSION_CODE < data.getInt("suggestedVersionCode")) { + // } + } + } catch (final Exception e) { + Log.e(TAG, "", e); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + return null; + } + + public void onDownload(@NonNull final AppCompatActivity context) { + Utils.openURL(context, "https://f-droid.org/packages/me.austinhuang.instagrabber/"); + } +} diff --git a/app/src/github/java/awais/instagrabber/utils/UpdateChecker.java b/app/src/github/java/awais/instagrabber/utils/UpdateChecker.java new file mode 100644 index 00000000..70bc2699 --- /dev/null +++ b/app/src/github/java/awais/instagrabber/utils/UpdateChecker.java @@ -0,0 +1,61 @@ +package awais.instagrabber.utils; + +import android.content.Context; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.net.HttpURLConnection; +import java.net.URL; + +public class UpdateChecker { + private static final Object LOCK = new Object(); + private static final String TAG = UpdateChecker.class.getSimpleName(); + + private static UpdateChecker instance; + + public static UpdateChecker getInstance() { + if (instance == null) { + synchronized (LOCK) { + if (instance == null) { + instance = new UpdateChecker(); + } + } + } + return instance; + } + + /** + * Needs to be called asynchronously + * + * @return the latest version from Github + */ + @Nullable + public String getLatestVersion() { + HttpURLConnection conn = null; + try { + conn = (HttpURLConnection) new URL("https://github.com/austinhuang0131/barinsta/releases/latest").openConnection(); + conn.setInstanceFollowRedirects(false); + conn.setUseCaches(false); + conn.setRequestProperty("User-Agent", "https://Barinsta.AustinHuang.me / mailto:Barinsta@AustinHuang.me"); + conn.connect(); + final int responseCode = conn.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_MOVED_TEMP) { + return "v" + conn.getHeaderField("Location").split("/v")[1]; + // return !version.equals(BuildConfig.VERSION_NAME); + } + } catch (final Exception e) { + Log.e(TAG, "", e); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + return null; + } + + public void onDownload(@NonNull final Context context) { + Utils.openURL(context, "https://github.com/austinhuang0131/instagrabber/releases/latest"); + } +} diff --git a/app/src/main/java/awais/instagrabber/activities/MainActivity.java b/app/src/main/java/awais/instagrabber/activities/MainActivity.java index b03a3696..b45a3200 100644 --- a/app/src/main/java/awais/instagrabber/activities/MainActivity.java +++ b/app/src/main/java/awais/instagrabber/activities/MainActivity.java @@ -59,6 +59,7 @@ import java.util.Deque; import java.util.List; import java.util.stream.Collectors; +import awais.instagrabber.BuildConfig; import awais.instagrabber.R; import awais.instagrabber.adapters.SuggestionsAdapter; import awais.instagrabber.asyncs.PostFetcher; @@ -154,8 +155,10 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage setupBottomNavigationBar(true); } setupSuggestions(); - final boolean checkUpdates = settingsHelper.getBoolean(Constants.CHECK_UPDATES); - if (checkUpdates) FlavorTown.updateCheck(this); + if (!BuildConfig.isPre) { + final boolean checkUpdates = settingsHelper.getBoolean(Constants.CHECK_UPDATES); + if (checkUpdates) FlavorTown.updateCheck(this); + } FlavorTown.changelogCheck(this); new ViewModelProvider(this).get(AppStateViewModel.class); // Just initiate the App state here final Intent intent = getIntent(); diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java index ecfcff77..6e9e13fd 100644 --- a/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java @@ -222,7 +222,10 @@ public class MorePreferencesFragment extends BasePreferencesFragment { BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")", -1, preference -> { - FlavorTown.updateCheck((AppCompatActivity) requireActivity(), true); + if (BuildConfig.isPre) return true; + final AppCompatActivity activity = (AppCompatActivity) getActivity(); + if (activity == null) return true; + FlavorTown.updateCheck(activity, true); return true; })); screen.addPreference(getDivider(context)); diff --git a/app/src/main/java/awais/instagrabber/utils/Constants.java b/app/src/main/java/awais/instagrabber/utils/Constants.java index 969f5d6a..f6da0362 100644 --- a/app/src/main/java/awais/instagrabber/utils/Constants.java +++ b/app/src/main/java/awais/instagrabber/utils/Constants.java @@ -79,7 +79,6 @@ public final class Constants { // public static final String SIGNATURE_KEY = "9193488027538fd3450b83b7d05286d4ca9599a0f7eeed90d8c85925698a05dc"; public static final String BREADCRUMB_KEY = "iN4$aGr0m"; public static final int LOGIN_RESULT_CODE = 5000; - public static final String FDROID_SHA1_FINGERPRINT = "C1661EB8FD09F618307E687786D5E5056F65084D"; public static final String SKIPPED_VERSION = "skipped_version"; public static final String DEFAULT_TAB = "default_tab"; public static final String PREF_DARK_THEME = "dark_theme"; diff --git a/app/src/main/java/awais/instagrabber/utils/FlavorTown.java b/app/src/main/java/awais/instagrabber/utils/FlavorTown.java index ee859700..8f06eb8b 100755 --- a/app/src/main/java/awais/instagrabber/utils/FlavorTown.java +++ b/app/src/main/java/awais/instagrabber/utils/FlavorTown.java @@ -1,104 +1,72 @@ package awais.instagrabber.utils; -import android.annotation.SuppressLint; -import android.content.ActivityNotFoundException; import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.Signature; -import android.content.res.Resources; -import android.net.Uri; -import android.os.AsyncTask; import android.util.Log; import android.widget.Toast; import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; +import java.util.Objects; import java.util.concurrent.ThreadLocalRandom; - -import javax.security.cert.CertificateException; -import javax.security.cert.X509Certificate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import awais.instagrabber.BuildConfig; import awais.instagrabber.R; -import awais.instagrabber.databinding.DialogUpdateBinding; import static awais.instagrabber.utils.Utils.settingsHelper; public final class FlavorTown { private static final String TAG = "FlavorTown"; - private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); - private static AlertDialog dialog; + private static final UpdateChecker UPDATE_CHECKER = UpdateChecker.getInstance(); + private static final Pattern VERSION_NAME_PATTERN = Pattern.compile("v?(\\d+\\.\\d+\\.\\d+)(?:_?)(\\w*)(?:-?)(\\w*)"); + + private static boolean checking = false; public static void updateCheck(@NonNull final AppCompatActivity context) { updateCheck(context, false); } - @SuppressLint("PackageManagerGetSignatures") - public static void updateCheck(@NonNull final AppCompatActivity context, final boolean force) { - boolean isInstalledFromFdroid = false; - final PackageInfo packageInfo; - try { - packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES); - for (Signature signature : packageInfo.signatures) { - final X509Certificate cert = X509Certificate.getInstance(signature.toByteArray()); - final String fingerprint = bytesToHex(MessageDigest.getInstance("SHA-1").digest(cert.getEncoded())); - isInstalledFromFdroid = fingerprint.equals(Constants.FDROID_SHA1_FINGERPRINT); - // Log.d(TAG, "fingerprint:" + fingerprint); - } - } catch (PackageManager.NameNotFoundException | NoSuchAlgorithmException | CertificateException e) { - Log.e(TAG, "Error", e); - } - if (isInstalledFromFdroid) return; - final DialogUpdateBinding binding = DialogUpdateBinding.inflate(context.getLayoutInflater(), null, false); - binding.skipUpdate.setOnCheckedChangeListener((buttonView, isChecked) -> { - if (dialog == null) return; - dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(!isChecked); - }); - Resources res = context.getResources(); - new UpdateChecker(version -> { - if (force && version.equals(BuildConfig.VERSION_NAME)) { - Toast.makeText(context, "You're already on the latest version", Toast.LENGTH_SHORT).show(); + public static void updateCheck(@NonNull final AppCompatActivity context, + final boolean force) { + if (checking) return; + checking = true; + AppExecutors.getInstance().networkIO().execute(() -> { + final String onlineVersionName = UPDATE_CHECKER.getLatestVersion(); + if (onlineVersionName == null) return; + final String onlineVersion = getVersion(onlineVersionName); + final String localVersion = getVersion(BuildConfig.VERSION_NAME); + if (Objects.equals(onlineVersion, localVersion)) { + if (force) { + AppExecutors.getInstance().mainThread().execute(() -> { + final Context applicationContext = context.getApplicationContext(); + // Check if app was closed or crashed before reaching here + if (applicationContext == null) return; + // Show toast if version number preference was tapped + Toast.makeText(applicationContext, R.string.on_latest_version, Toast.LENGTH_SHORT).show(); + }); + } return; } - final String skippedVersion = settingsHelper.getString(Constants.SKIPPED_VERSION); - final boolean shouldShowDialog = force || (!version.equals(BuildConfig.VERSION_NAME) && !BuildConfig.DEBUG && !skippedVersion - .equals(version)); + final boolean shouldShowDialog = UpdateCheckCommon.shouldShowUpdateDialog(force, onlineVersionName); if (!shouldShowDialog) return; - dialog = new AlertDialog.Builder(context) - .setTitle(res.getString(R.string.update_available, version)) - .setView(binding.getRoot()) - .setNeutralButton(R.string.cancel, (dialog, which) -> { - if (binding.skipUpdate.isChecked()) { - settingsHelper.putString(Constants.SKIPPED_VERSION, version); - } - dialog.dismiss(); - }) - .setPositiveButton(R.string.action_github, (dialog1, which) -> { - try { - context.startActivity(new Intent(Intent.ACTION_VIEW).setData( - Uri.parse("https://github.com/austinhuang0131/instagrabber/releases/latest"))); - } catch (final ActivityNotFoundException e) { - // do nothing - } - }) - // if we don't show dialog for fdroid users, is the below required? - .setNegativeButton(R.string.action_fdroid, (dialog, which) -> { - try { - context.startActivity(new Intent(Intent.ACTION_VIEW).setData( - Uri.parse("https://f-droid.org/packages/me.austinhuang.instagrabber/"))); - } catch (final ActivityNotFoundException e) { - // do nothing - } - }) - .show(); - }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + UpdateCheckCommon.showUpdateDialog(context, onlineVersionName, (dialog, which) -> { + UPDATE_CHECKER.onDownload(context); + dialog.dismiss(); + }); + }); + } + + private static String getVersion(@NonNull final String versionName) { + final Matcher matcher = VERSION_NAME_PATTERN.matcher(versionName); + if (!matcher.matches()) return versionName; + try { + return matcher.group(1); + } catch (Exception e) { + Log.e(TAG, "getVersion: ", e); + } + return versionName; } public static void changelogCheck(@NonNull final Context context) { @@ -121,14 +89,4 @@ public final class FlavorTown { settingsHelper.putInteger(Constants.PREV_INSTALL_VERSION, BuildConfig.VERSION_CODE); } } - - public static String bytesToHex(byte[] bytes) { - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = HEX_ARRAY[v >>> 4]; - hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; - } - return new String(hexChars); - } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/UpdateCheckCommon.java b/app/src/main/java/awais/instagrabber/utils/UpdateCheckCommon.java new file mode 100644 index 00000000..285fbb13 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/utils/UpdateCheckCommon.java @@ -0,0 +1,38 @@ +package awais.instagrabber.utils; + +import android.content.Context; +import android.content.DialogInterface; + +import androidx.annotation.NonNull; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import awais.instagrabber.BuildConfig; +import awais.instagrabber.R; + +import static awais.instagrabber.utils.Utils.settingsHelper; + +public final class UpdateCheckCommon { + + public static boolean shouldShowUpdateDialog(final boolean force, + @NonNull final String version) { + final String skippedVersion = settingsHelper.getString(Constants.SKIPPED_VERSION); + return force || (!BuildConfig.DEBUG && !skippedVersion.equals(version)); + } + + public static void showUpdateDialog(@NonNull final Context context, + @NonNull final String version, + @NonNull final DialogInterface.OnClickListener onDownloadClickListener) { + AppExecutors.getInstance().mainThread().execute(() -> { + new MaterialAlertDialogBuilder(context) + .setTitle(context.getString(R.string.update_available, version)) + .setNeutralButton(R.string.skip_update, (dialog, which) -> { + settingsHelper.putString(Constants.SKIPPED_VERSION, version); + dialog.dismiss(); + }) + .setPositiveButton(R.string.action_download, onDownloadClickListener) + .setNegativeButton(R.string.cancel, null) + .show(); + }); + } +} diff --git a/app/src/main/java/awais/instagrabber/utils/UpdateChecker.java b/app/src/main/java/awais/instagrabber/utils/UpdateChecker.java deleted file mode 100755 index 3ea67ba1..00000000 --- a/app/src/main/java/awais/instagrabber/utils/UpdateChecker.java +++ /dev/null @@ -1,58 +0,0 @@ -package awais.instagrabber.utils; - -import android.os.AsyncTask; -import android.util.Log; - -import androidx.annotation.NonNull; - -import org.json.JSONObject; - -import java.net.HttpURLConnection; -import java.net.URL; - -import awais.instagrabber.BuildConfig; -import awais.instagrabber.interfaces.FetchListener; - -public final class UpdateChecker extends AsyncTask { - private final FetchListener fetchListener; - private String version; - - public UpdateChecker(final FetchListener fetchListener) { - this.fetchListener = fetchListener; - } - - @NonNull - @Override - protected Boolean doInBackground(final Void... voids) { - try { - version = ""; - - HttpURLConnection conn = - (HttpURLConnection) new URL("https://f-droid.org/api/v1/packages/me.austinhuang.instagrabber").openConnection(); - conn.setUseCaches(false); - conn.setRequestProperty("User-Agent", "https://Barinsta.AustinHuang.me / mailto:Barinsta@AustinHuang.me"); - conn.connect(); - - final int responseCode = conn.getResponseCode(); - if (responseCode == HttpURLConnection.HTTP_OK) { - final JSONObject data = new JSONObject(NetworkUtils.readFromConnection(conn)); - if (BuildConfig.VERSION_CODE < data.getInt("suggestedVersionCode")) { - version = data.getJSONArray("packages").getJSONObject(0).getString("versionName"); - return true; - } - } - - conn.disconnect(); - } catch (final Exception e) { - if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); - } - - return false; - } - - @Override - protected void onPostExecute(final Boolean result) { - if (result != null && result && fetchListener != null) - fetchListener.onResult("v"+version); - } -} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ee55f274..ab8c25f3 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -475,6 +475,8 @@ Delete unsuccessful Barinsta Crash Report Select an email app to send crash logs + Skip this update + You\'re already on the latest version Screen order Other tabs The tab order will be reflected on next launch