feat: implemented version number control and update dialog for Github flavor

This commit is contained in:
CappielloAntonio 2024-05-31 22:41:01 +02:00
parent c243fa9edc
commit 0a26f0a7b8
19 changed files with 477 additions and 4 deletions

View file

@ -6,6 +6,7 @@ import android.content.SharedPreferences;
import androidx.preference.PreferenceManager;
import com.cappielloantonio.tempo.github.Github;
import com.cappielloantonio.tempo.helper.ThemeHelper;
import com.cappielloantonio.tempo.subsonic.Subsonic;
import com.cappielloantonio.tempo.subsonic.SubsonicPreferences;
@ -15,6 +16,7 @@ public class App extends Application {
private static App instance;
private static Context context;
private static Subsonic subsonic;
private static Github github;
private static SharedPreferences preferences;
@Override
@ -53,6 +55,13 @@ public class App extends Application {
return subsonic;
}
public static Github getGithubClientInstance() {
if (github == null) {
github = new Github();
}
return github;
}
public SharedPreferences getPreferences() {
if (preferences == null) {
preferences = PreferenceManager.getDefaultSharedPreferences(context);

View file

@ -0,0 +1,29 @@
package com.cappielloantonio.tempo.github;
import com.cappielloantonio.tempo.github.api.release.ReleaseClient;
public class Github {
private static final String OWNER = "CappielloAntonio";
private static final String REPO = "Tempo";
private ReleaseClient releaseClient;
public ReleaseClient getReleaseClient() {
if (releaseClient == null) {
releaseClient = new ReleaseClient(this);
}
return releaseClient;
}
public String getUrl() {
return "https://api.github.com/";
}
public static String getOwner() {
return OWNER;
}
public static String getRepo() {
return REPO;
}
}

View file

@ -0,0 +1,30 @@
package com.cappielloantonio.tempo.github
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class GithubRetrofitClient(github: Github) {
var retrofit: Retrofit
init {
retrofit = Retrofit.Builder()
.baseUrl(github.url)
.addConverterFactory(GsonConverterFactory.create())
.client(getOkHttpClient())
.build()
}
private fun getOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(getHttpLoggingInterceptor())
.build()
}
private fun getHttpLoggingInterceptor(): HttpLoggingInterceptor {
val loggingInterceptor = HttpLoggingInterceptor()
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
return loggingInterceptor
}
}

View file

@ -0,0 +1,24 @@
package com.cappielloantonio.tempo.github.api.release;
import android.util.Log;
import com.cappielloantonio.tempo.github.Github;
import com.cappielloantonio.tempo.github.GithubRetrofitClient;
import com.cappielloantonio.tempo.github.models.LatestRelease;
import retrofit2.Call;
public class ReleaseClient {
private static final String TAG = "ReleaseClient";
private final ReleaseService releaseService;
public ReleaseClient(Github github) {
this.releaseService = new GithubRetrofitClient(github).getRetrofit().create(ReleaseService.class);
}
public Call<LatestRelease> getLatestRelease() {
Log.d(TAG, "getLatestRelease()");
return releaseService.getLatestRelease(Github.getOwner(), Github.getRepo());
}
}

View file

@ -0,0 +1,12 @@
package com.cappielloantonio.tempo.github.api.release;
import com.cappielloantonio.tempo.github.models.LatestRelease;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;
public interface ReleaseService {
@GET("repos/{owner}/{repo}/releases/latest")
Call<LatestRelease> getLatestRelease(@Path("owner") String owner, @Path("repo") String repo);
}

View file

@ -0,0 +1,32 @@
package com.cappielloantonio.tempo.github.models
import com.google.gson.annotations.SerializedName
data class Assets(
@SerializedName("url")
var url: String? = null,
@SerializedName("id")
var id: Int? = null,
@SerializedName("node_id")
var nodeId: String? = null,
@SerializedName("name")
var name: String? = null,
@SerializedName("label")
var label: String? = null,
@SerializedName("uploader")
var uploader: Uploader? = Uploader(),
@SerializedName("content_type")
var contentType: String? = null,
@SerializedName("state")
var state: String? = null,
@SerializedName("size")
var size: Int? = null,
@SerializedName("download_count")
var downloadCount: Int? = null,
@SerializedName("created_at")
var createdAt: String? = null,
@SerializedName("updated_at")
var updatedAt: String? = null,
@SerializedName("browser_download_url")
var browserDownloadUrl: String? = null
)

View file

@ -0,0 +1,42 @@
package com.cappielloantonio.tempo.github.models
import com.google.gson.annotations.SerializedName
data class Author(
@SerializedName("login")
var login: String? = null,
@SerializedName("id")
var id: Int? = null,
@SerializedName("node_id")
var nodeId: String? = null,
@SerializedName("avatar_url")
var avatarUrl: String? = null,
@SerializedName("gravatar_id")
var gravatarId: String? = null,
@SerializedName("url")
var url: String? = null,
@SerializedName("html_url")
var htmlUrl: String? = null,
@SerializedName("followers_url")
var followersUrl: String? = null,
@SerializedName("following_url")
var followingUrl: String? = null,
@SerializedName("gists_url")
var gistsUrl: String? = null,
@SerializedName("starred_url")
var starredUrl: String? = null,
@SerializedName("subscriptions_url")
var subscriptionsUrl: String? = null,
@SerializedName("organizations_url")
var organizationsUrl: String? = null,
@SerializedName("repos_url")
var reposUrl: String? = null,
@SerializedName("events_url")
var eventsUrl: String? = null,
@SerializedName("received_events_url")
var receivedEventsUrl: String? = null,
@SerializedName("type")
var type: String? = null,
@SerializedName("site_admin")
var siteAdmin: Boolean? = null
)

View file

@ -0,0 +1,44 @@
package com.cappielloantonio.tempo.github.models
import com.google.gson.annotations.SerializedName
data class LatestRelease(
@SerializedName("url")
var url: String? = null,
@SerializedName("assets_url")
var assetsUrl: String? = null,
@SerializedName("upload_url")
var uploadUrl: String? = null,
@SerializedName("html_url")
var htmlUrl: String? = null,
@SerializedName("id")
var id: Int? = null,
@SerializedName("author")
var author: Author? = Author(),
@SerializedName("node_id")
var nodeId: String? = null,
@SerializedName("tag_name")
var tagName: String? = null,
@SerializedName("target_commitish")
var targetCommitish: String? = null,
@SerializedName("name")
var name: String? = null,
@SerializedName("draft")
var draft: Boolean? = null,
@SerializedName("prerelease")
var prerelease: Boolean? = null,
@SerializedName("created_at")
var createdAt: String? = null,
@SerializedName("published_at")
var publishedAt: String? = null,
@SerializedName("assets")
var assets: ArrayList<Assets> = arrayListOf(),
@SerializedName("tarball_url")
var tarballUrl: String? = null,
@SerializedName("zipball_url")
var zipballUrl: String? = null,
@SerializedName("body")
var body: String? = null,
@SerializedName("reactions")
var reactions: Reactions? = Reactions()
)

View file

@ -0,0 +1,26 @@
package com.cappielloantonio.tempo.github.models
import com.google.gson.annotations.SerializedName
data class Reactions(
@SerializedName("url")
var url: String? = null,
@SerializedName("total_count")
var totalCount: Int? = null,
@SerializedName("+1")
var like: Int? = null,
@SerializedName("-1")
var dislike: Int? = null,
@SerializedName("laugh")
var laugh: Int? = null,
@SerializedName("hooray")
var hooray: Int? = null,
@SerializedName("confused")
var confused: Int? = null,
@SerializedName("heart")
var heart: Int? = null,
@SerializedName("rocket")
var rocket: Int? = null,
@SerializedName("eyes")
var eyes: Int? = null
)

View file

@ -0,0 +1,42 @@
package com.cappielloantonio.tempo.github.models
import com.google.gson.annotations.SerializedName
data class Uploader(
@SerializedName("login")
var login: String? = null,
@SerializedName("id")
var id: Int? = null,
@SerializedName("node_id")
var nodeId: String? = null,
@SerializedName("avatar_url")
var avatarUrl: String? = null,
@SerializedName("gravatar_id")
var gravatarId: String? = null,
@SerializedName("url")
var url: String? = null,
@SerializedName("html_url")
var htmlUrl: String? = null,
@SerializedName("followers_url")
var followersUrl: String? = null,
@SerializedName("following_url")
var followingUrl: String? = null,
@SerializedName("gists_url")
var gistsUrl: String? = null,
@SerializedName("starred_url")
var starredUrl: String? = null,
@SerializedName("subscriptions_url")
var subscriptionsUrl: String? = null,
@SerializedName("organizations_url")
var organizationsUrl: String? = null,
@SerializedName("repos_url")
var reposUrl: String? = null,
@SerializedName("events_url")
var eventsUrl: String? = null,
@SerializedName("received_events_url")
var receivedEventsUrl: String? = null,
@SerializedName("type")
var type: String? = null,
@SerializedName("site_admin")
var siteAdmin: Boolean? = null
)

View file

@ -0,0 +1,31 @@
package com.cappielloantonio.tempo.github.utils;
import com.cappielloantonio.tempo.BuildConfig;
import com.cappielloantonio.tempo.github.models.LatestRelease;
public class UpdateUtil {
public static boolean showUpdateDialog(LatestRelease release) {
if (release.getTagName() == null) return false;
try {
String[] local = BuildConfig.VERSION_NAME.split("\\.");
String[] remote = release.getTagName().split("\\.");
for (int i = 0; i < local.length; i++) {
int localPart = Integer.parseInt(local[i]);
int remotePart = Integer.parseInt(remote[i]);
if (localPart > remotePart) {
return false;
} else if (localPart < remotePart) {
return true;
}
}
} catch (Exception exception) {
return false;
}
return false;
}
}

View file

@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.github.models.LatestRelease;
import com.cappielloantonio.tempo.interfaces.SystemCallback;
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import com.cappielloantonio.tempo.subsonic.models.OpenSubsonicExtension;
@ -92,4 +93,27 @@ public class SystemRepository {
return extensionsResult;
}
public MutableLiveData<LatestRelease> checkTempoUpdate() {
MutableLiveData<LatestRelease> latestRelease = new MutableLiveData<>();
App.getGithubClientInstance()
.getReleaseClient()
.getLatestRelease()
.enqueue(new Callback<LatestRelease>() {
@Override
public void onResponse(@NonNull Call<LatestRelease> call, @NonNull Response<LatestRelease> response) {
if (response.isSuccessful() && response.body() != null) {
latestRelease.postValue(response.body());
}
}
@Override
public void onFailure(@NonNull Call<LatestRelease> call, @NonNull Throwable t) {
latestRelease.postValue(null);
}
});
return latestRelease;
}
}

View file

@ -18,12 +18,15 @@ import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;
import androidx.navigation.ui.NavigationUI;
import com.cappielloantonio.tempo.BuildConfig;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.broadcast.receiver.ConnectivityStatusBroadcastReceiver;
import com.cappielloantonio.tempo.databinding.ActivityMainBinding;
import com.cappielloantonio.tempo.github.utils.UpdateUtil;
import com.cappielloantonio.tempo.service.MediaManager;
import com.cappielloantonio.tempo.ui.activity.base.BaseActivity;
import com.cappielloantonio.tempo.ui.dialog.ConnectionAlertDialog;
import com.cappielloantonio.tempo.ui.dialog.GithubTempoUpdateDialog;
import com.cappielloantonio.tempo.ui.dialog.ServerUnreachableDialog;
import com.cappielloantonio.tempo.ui.fragment.PlayerBottomSheetFragment;
import com.cappielloantonio.tempo.util.Constants;
@ -71,6 +74,7 @@ public class MainActivity extends BaseActivity {
init();
checkConnectionType();
getOpenSubsonicExtensions();
checkTempoUpdate();
}
@Override
@ -358,6 +362,17 @@ public class MainActivity extends BaseActivity {
}
}
private void checkTempoUpdate() {
if (BuildConfig.FLAVOR.equals("tempo") && Preferences.showTempoUpdateDialog()) {
mainViewModel.checkTempoUpdate().observe(this, latestRelease -> {
if (latestRelease != null && UpdateUtil.showUpdateDialog(latestRelease)) {
GithubTempoUpdateDialog dialog = new GithubTempoUpdateDialog(latestRelease);
dialog.show(getSupportFragmentManager(), null);
}
});
}
}
private void checkConnectionType() {
if (Preferences.isWifiOnly()) {
ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);

View file

@ -0,0 +1,73 @@
package com.cappielloantonio.tempo.ui.dialog;
import android.app.Dialog;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.DialogGithubTempoUpdateBinding;
import com.cappielloantonio.tempo.github.models.LatestRelease;
import com.cappielloantonio.tempo.util.Preferences;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.Objects;
public class GithubTempoUpdateDialog extends DialogFragment {
private final LatestRelease latestRelease;
public GithubTempoUpdateDialog(LatestRelease latestRelease) {
this.latestRelease = latestRelease;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
DialogGithubTempoUpdateBinding bind = DialogGithubTempoUpdateBinding.inflate(getLayoutInflater());
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity())
.setView(bind.getRoot())
.setTitle(R.string.github_update_dialog_title)
.setPositiveButton(R.string.github_update_dialog_positive_button, (dialog, id) -> { })
.setNegativeButton(R.string.github_update_dialog_negative_button, (dialog, id) -> { })
.setNeutralButton(R.string.github_update_dialog_neutral_button, (dialog, id) -> { });
return builder.create();
}
@Override
public void onStart() {
super.onStart();
setButtonAction();
}
private void setButtonAction() {
AlertDialog alertDialog = (AlertDialog) Objects.requireNonNull(getDialog());
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
openLink(latestRelease.getHtmlUrl());
Objects.requireNonNull(getDialog()).dismiss();
});
alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener(v -> {
Preferences.setTempoUpdateReminder();
Objects.requireNonNull(getDialog()).dismiss();
});
alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> {
openLink(getString(R.string.support_url));
Objects.requireNonNull(getDialog()).dismiss();
});
}
private void openLink(String link) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(link));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
}

View file

@ -56,7 +56,7 @@ public class ServerUnreachableDialog extends DialogFragment {
});
alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
Preferences.setServerUnreachableDatetime(System.currentTimeMillis());
Preferences.setServerUnreachableDatetime();
alertDialog.dismiss();
});
}

View file

@ -59,6 +59,7 @@ object Preferences {
private const val AUDIO_QUALITY_PER_ITEM = "audio_quality_per_item"
private const val HOME_SECTOR_LIST = "home_sector_list"
private const val RATING_PER_ITEM = "rating_per_item"
private const val NEXT_UPDATE_CHECK = "next_update_check"
@JvmStatic
@ -248,12 +249,12 @@ object Preferences {
fun showServerUnreachableDialog(): Boolean {
return App.getInstance().preferences.getLong(
SERVER_UNREACHABLE, 0
) + 360000 < System.currentTimeMillis()
) + 86400000 < System.currentTimeMillis()
}
@JvmStatic
fun setServerUnreachableDatetime(datetime: Long) {
App.getInstance().preferences.edit().putLong(SERVER_UNREACHABLE, datetime).apply()
fun setServerUnreachableDatetime() {
App.getInstance().preferences.edit().putLong(SERVER_UNREACHABLE, System.currentTimeMillis()).apply()
}
@JvmStatic
@ -422,4 +423,16 @@ object Preferences {
fun showItemRating(): Boolean {
return App.getInstance().preferences.getBoolean(RATING_PER_ITEM, false)
}
@JvmStatic
fun showTempoUpdateDialog(): Boolean {
return App.getInstance().preferences.getLong(
NEXT_UPDATE_CHECK, 0
) + 86400000 < System.currentTimeMillis()
}
@JvmStatic
fun setTempoUpdateReminder() {
App.getInstance().preferences.edit().putLong(NEXT_UPDATE_CHECK, System.currentTimeMillis()).apply()
}
}

View file

@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import com.cappielloantonio.tempo.github.models.LatestRelease;
import com.cappielloantonio.tempo.repository.QueueRepository;
import com.cappielloantonio.tempo.repository.SystemRepository;
import com.cappielloantonio.tempo.subsonic.models.OpenSubsonicExtension;
@ -36,4 +37,8 @@ public class MainViewModel extends AndroidViewModel {
public LiveData<List<OpenSubsonicExtension>> getOpenSubsonicExtensions() {
return systemRepository.getOpenSubsonicExtensions();
}
public LiveData<LatestRelease> checkTempoUpdate() {
return systemRepository.checkTempoUpdate();
}
}

View file

@ -0,0 +1,14 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="4dp"
android:text="@string/github_update_dialog_summary" />
</LinearLayout>

View file

@ -89,6 +89,13 @@
<string name="filter_title_expanded">Filter Genres</string>
<string name="genre_catalogue_title">Genre Catalogue</string>
<string name="genre_catalogue_title_expanded">Browse Genres</string>
<string name="github_update_dialog_negative_button">Remind me later</string>
<string name="github_update_dialog_neutral_button">Support me</string>
<string name="github_update_dialog_positive_button">Download now</string>
<string name="github_update_dialog_summary">There is a new version of the app available on Github.</string>
<string name="github_update_dialog_title">Update available</string>
<string name="home_rearrangement_dialog_negative_button">Cancel</string>
<string name="home_rearrangement_dialog_neutral_button">Reset</string>
<string name="home_rearrangement_dialog_positive_button">Save</string>
@ -366,6 +373,7 @@
<string name="streaming_cache_storage_dialog_title">Select storage option</string>
<string name="streaming_cache_storage_external_dialog_positive_button">External</string>
<string name="streaming_cache_storage_internal_dialog_negative_button">Internal</string>
<string name="support_url">https://buymeacoffee.com/a.cappiello</string>
<string name="track_info_album">Album</string>
<string name="track_info_artist">Artist</string>
<string name="track_info_bitrate">Bitrate</string>