Migliorata gestione aggiornamento obbligatorio

This commit is contained in:
Giuseppe Scorrano 2025-12-05 13:05:05 +01:00
parent faa45cd096
commit 809d4ef5af
6 changed files with 215 additions and 146 deletions

View File

@ -1,6 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="app" type="AndroidRunConfigurationType" factoryName="Android App">
<module name="WMS_Native.app" />
<module name="WMS.app" />
<option name="ANDROID_RUN_CONFIGURATION_SCHEMA_VERSION" value="1" />
<option name="DEPLOY" value="true" />
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />

View File

@ -7,18 +7,22 @@ import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.text.Html;
import android.util.Log;
import androidx.fragment.app.FragmentActivity;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentManager;
import androidx.preference.PreferenceManager;
import com.google.firebase.crashlytics.FirebaseCrashlytics;
import java.io.File;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import javax.inject.Singleton;
import it.integry.integrywmsnative.BuildConfig;
import it.integry.integrywmsnative.core.helper.ContextHelper;
import it.integry.integrywmsnative.MainApplication;
import it.integry.integrywmsnative.core.rest.consumers.SystemRESTConsumer;
import it.integry.integrywmsnative.core.rest.model.system.LatestAppVersionInfoDTO;
import it.integry.integrywmsnative.core.settings.SettingsManager;
@ -26,11 +30,14 @@ import it.integry.integrywmsnative.core.utility.FileDownloader;
import it.integry.integrywmsnative.core.utility.UtilityExceptions;
import it.integry.integrywmsnative.gest.settings.MainSettingsFragment;
import it.integry.integrywmsnative.view.dialogs.DialogProgressView;
import it.integry.integrywmsnative.view.dialogs.base.DialogSimpleMessageView;
import it.integry.integrywmsnative.view.dialogs.update_available.DialogUpdateAvailableView;
@Singleton
public class UpdatesManager {
private static final String TAG = "UpdatesManager";
private final ExecutorService executorService;
private final Handler handler;
private final SystemRESTConsumer systemRESTConsumer;
@ -41,109 +48,149 @@ public class UpdatesManager {
this.systemRESTConsumer = systemRESTConsumer;
}
public void executeCheck(Context context, boolean forceReinstall) {
public void executeCheck(@NonNull Context context, @NonNull FragmentManager fragmentManager, boolean forceReinstall) {
executorService.execute(() -> {
try {
final String baseEndpoint = SettingsManager.i().getServer().getProtocol() + "://" + SettingsManager.i().getServer().getHost() +
(SettingsManager.i().getServer().getPort() > 0 ? ":" + SettingsManager.i().getServer().getPort() : "") + "/ems-api/";
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
var betaEnabled = sharedPreferences.getBoolean(MainSettingsFragment.KEY_BUTTON_ENABLE_BETA, false);
boolean betaEnabled = sharedPreferences.getBoolean(MainSettingsFragment.KEY_BUTTON_ENABLE_BETA, false);
LatestAppVersionInfoDTO latestData = systemRESTConsumer.retrieveUpdatesInfoSynchronized(betaEnabled);
if (latestData == null)
if (latestData == null) {
Log.d(TAG, "No update information received from server.");
return;
}
if (latestData.getLatestVersionCode() < BuildConfig.VERSION_CODE)
boolean isUpdateRequired = latestData.getLatestVersionCode() > BuildConfig.VERSION_CODE;
boolean currentVersionIsBeta = BuildConfig.VERSION_NAME.toLowerCase().contains("beta");
boolean isChannelSwitch = currentVersionIsBeta != betaEnabled;
if (!isUpdateRequired && !forceReinstall && !isChannelSwitch) {
Log.d(TAG, "App is up to date.");
return;
}
boolean currentVersionIsBeta = BuildConfig.VERSION_NAME.contains("beta");
if(!forceReinstall && currentVersionIsBeta == betaEnabled && latestData.getLatestVersionCode() == BuildConfig.VERSION_CODE)
return;
//Se sto passando da una beta a una stable e viceversa non obbligo l'utente a fare l'aggiornamento
if(currentVersionIsBeta != betaEnabled || forceReinstall) {
// Se si passa da beta a stabile (o viceversa) o se è una reinstallazione forzata,
// l'aggiornamento non deve essere obbligatorio per non bloccare l'utente.
if (isChannelSwitch || forceReinstall) {
latestData.setForced(false);
}
String currentDownloadUrl = baseEndpoint + latestData.getUrl();
final String baseEndpoint = SettingsManager.i().getServer().getProtocol() + "://" + SettingsManager.i().getServer().getHost() +
(SettingsManager.i().getServer().getPort() > 0 ? ":" + SettingsManager.i().getServer().getPort() : "") + "/ems-api/";
String downloadUrl = baseEndpoint + latestData.getUrl();
showDialog(context, latestData, () -> {
installAPK(context, currentDownloadUrl);
});
handler.post(() -> showUpdateAvailableDialog(context, fragmentManager, latestData, downloadUrl));
} catch (Exception e) {
handler.post(() -> {
UtilityExceptions.defaultException(context, e);
});
Log.e(TAG, "Error during update check", e);
// Se il check fallisce, non è un errore bloccante. L'utente può continuare a usare l'app.
}
});
}
private void showDialog(Context context, LatestAppVersionInfoDTO updatesData, Runnable onUpdateStart) {
DialogUpdateAvailableView.newInstance(updatesData, onUpdateStart)
.show(((FragmentActivity)context).getSupportFragmentManager(), "dialog-updates");
private void showUpdateAvailableDialog(@NonNull Context context, @NonNull FragmentManager fragmentManager, @NonNull LatestAppVersionInfoDTO updatesData, @NonNull String downloadUrl) {
DialogUpdateAvailableView dialog = DialogUpdateAvailableView.newInstance(updatesData, () -> {
// L'utente ha accettato di aggiornare
downloadAndInstall(context, fragmentManager, downloadUrl, updatesData.isForced());
});
if (updatesData.isForced()) {
dialog.setCancelable(false);
}
dialog.show(fragmentManager, "dialog-updates");
}
private void installAPK(Context context, String downloadURL) {
File destination = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
var progressDialogBuilder = new DialogProgressView("Download", null, false);
progressDialogBuilder.show(Objects.requireNonNull(ContextHelper.getFragmentManagerFromContext(context)), "tag");
var fileDownloader = new FileDownloader()
.setDestFolder(destination)
.setUrlString(downloadURL)
.setOnProgressUpdate(progress -> {
handler.post(() -> {
progressDialogBuilder.setProgress(progress);
});
})
.setOnDownloadCompleted(destPath -> {
handler.post(() -> {
progressDialogBuilder.dismiss();
if (!destination.exists()) {
UtilityExceptions.defaultException(context, new Exception("Errore durante il download dell'aggiornamento"));
return;
}
Uri fileLoc;
Intent intent;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
fileLoc = GenericFileProvider.getUriForFile(context,
context.getApplicationContext().getPackageName() + ".core.update.GenericFileProvider",
destPath);
} else {
intent = new Intent(Intent.ACTION_VIEW);
fileLoc = Uri.fromFile(destPath);
}
intent.setDataAndType(fileLoc, "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
context.startActivity(intent);
});
});
private void downloadAndInstall(@NonNull Context context, @NonNull FragmentManager fragmentManager, @NonNull String downloadUrl, boolean isForced) {
DialogProgressView progressDialog = new DialogProgressView("Download", null, false);
if (isForced) {
progressDialog.setCancelable(false);
}
progressDialog.show(fragmentManager, "progress-dialog");
FileDownloader fileDownloader = new FileDownloader()
.setExecutorService(executorService)
.setDestFolder(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS))
.setUrlString(downloadUrl)
.setOnProgressUpdate(progress -> handler.post(() -> progressDialog.setProgress(progress)));
executorService.execute(() -> {
try {
fileDownloader.download();
File apkFile = fileDownloader.download();
handler.post(() -> {
progressDialog.dismiss();
installApk(context, fragmentManager, apkFile, isForced);
});
} catch (Exception e) {
progressDialogBuilder.dismissAllowingStateLoss();
UtilityExceptions.defaultException(context, e);
Log.e(TAG, "Download failed", e);
handler.post(() -> {
progressDialog.dismissAllowingStateLoss();
handleUpdateError(fragmentManager, e, isForced);
});
}
});
}
private void installApk(@NonNull Context context, @NonNull FragmentManager fragmentManager, @NonNull File apkFile, boolean isForced) {
if (!apkFile.exists()) {
handleUpdateError(fragmentManager, new Exception("File APK non trovato dopo il download."), isForced);
return;
}
Uri fileUri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
fileUri = GenericFileProvider.getUriForFile(context,
context.getApplicationContext().getPackageName() + ".core.update.GenericFileProvider",
apkFile);
} else {
fileUri = Uri.fromFile(apkFile);
}
Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
intent.setDataAndType(fileUri, "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_NEW_TASK);
try {
context.startActivity(intent);
// Se l'aggiornamento è forzato, l'app si chiuderà per attendere l'installazione manuale.
if (isForced) {
// Diamo tempo all'utente di vedere l'installer di sistema prima di chiudere l'app.
handler.postDelayed(MainApplication::exit, 1000);
}
} catch (Exception e) {
Log.e(TAG, "Failed to start APK installation", e);
handleUpdateError(fragmentManager, e, isForced);
}
}
private void handleUpdateError(@NonNull FragmentManager fragmentManager, @NonNull Exception ex, boolean isForced) {
FirebaseCrashlytics.getInstance().recordException(ex);
String errorMessage = UtilityExceptions.recognizeExceptionMessage(ex);
handler.post(() -> {
String title = isForced ? "Aggiornamento Obbligatorio Fallito" : "Errore Aggiornamento";
Runnable onPositiveClick = null;
if (isForced) {
onPositiveClick = MainApplication::exit;
}
DialogSimpleMessageView dialog = DialogSimpleMessageView.newInstance(
isForced ? DialogSimpleMessageView.TYPE.ERROR : DialogSimpleMessageView.TYPE.WARNING,
title,
Html.fromHtml(errorMessage),
null,
onPositiveClick,
null,
null,
null);
if (isForced) {
dialog.setCancelable(false);
}
dialog.show(fragmentManager, "error-dialog");
});
}
}

View File

@ -9,6 +9,9 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;
import it.integry.integrywmsnative.core.expansion.RunnableArgs;
@ -18,68 +21,87 @@ public class FileDownloader {
private File destFolder;
private RunnableArgs<Integer> onProgressUpdate;
private RunnableArgs<File> onDownloadCompleted;
private ExecutorService executorService;
public void download() throws Exception {
InputStream input = null;
OutputStream output = null;
HttpURLConnection connection = null;
File downloadFile = null;
public File download() throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(1);
try {
if (!destFolder.exists()) destFolder.mkdirs();
AtomicReference<File> result = new AtomicReference<>();
AtomicReference<Exception> exceptionAtomicReference = new AtomicReference<>();
URL url = new URL(urlString);
connection = (HttpURLConnection) url.openConnection();
connection.connect();
connection.setConnectTimeout(120 * 1000);
executorService.execute(() -> {
InputStream input = null;
OutputStream output = null;
HttpURLConnection connection = null;
File downloadFile = null;
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK)
throw new Exception("Server returned HTTP " + connection.getResponseCode() + " " + connection.getResponseMessage());
input = connection.getInputStream();
int totalBytesToDownload = connection.getContentLength();
int downloadedBytes = 0;
String title = URLUtil.guessFileName(String.valueOf(url), null, null);
downloadFile = new File(destFolder, title);
if (downloadFile.exists())
downloadFile.delete();
output = new FileOutputStream(downloadFile);
byte[] buf = new byte[1024];
int len;
while ((len = input.read(buf)) > 0) {
output.write(buf, 0, len);
downloadedBytes += len;
if (onProgressUpdate != null)
onProgressUpdate.run((downloadedBytes * 100) / totalBytesToDownload);
}
} catch (Exception e) {
if(downloadFile != null && downloadFile.exists())
downloadFile.delete();
throw e;
} finally {
try {
if (output != null)
output.close();
if (input != null)
input.close();
} catch (IOException ignored) {
}
if (connection != null)
connection.disconnect();
}
if (!destFolder.exists()) destFolder.mkdirs();
if (onDownloadCompleted != null) onDownloadCompleted.run(downloadFile);
URL url = new URL(urlString);
connection = (HttpURLConnection) url.openConnection();
connection.connect();
connection.setConnectTimeout(120 * 1000);
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK)
throw new Exception("Server returned HTTP " + connection.getResponseCode() + " " + connection.getResponseMessage());
input = connection.getInputStream();
int totalBytesToDownload = connection.getContentLength();
int downloadedBytes = 0;
String title = URLUtil.guessFileName(String.valueOf(url), null, null);
downloadFile = new File(destFolder, title);
if (downloadFile.exists())
downloadFile.delete();
output = new FileOutputStream(downloadFile);
byte[] buf = new byte[1024];
int len;
while ((len = input.read(buf)) > 0) {
output.write(buf, 0, len);
downloadedBytes += len;
if (onProgressUpdate != null)
onProgressUpdate.run((downloadedBytes * 100) / totalBytesToDownload);
}
result.set(downloadFile);
} catch (Exception e) {
if(downloadFile != null && downloadFile.exists())
downloadFile.delete();
exceptionAtomicReference.set(e);
} finally {
try {
if (output != null)
output.close();
if (input != null)
input.close();
} catch (IOException ignored) {
}
if (connection != null)
connection.disconnect();
countDownLatch.countDown();
}
});
countDownLatch.await();
if(exceptionAtomicReference.get() != null)
throw exceptionAtomicReference.get();
return result.get();
}
public FileDownloader setExecutorService(ExecutorService executorService) {
this.executorService = executorService;
return this;
}
public String getUrlString() {
return urlString;
@ -103,9 +125,4 @@ public class FileDownloader {
this.onProgressUpdate = onProgressUpdate;
return this;
}
public FileDownloader setOnDownloadCompleted(RunnableArgs<File> onDownloadCompleted) {
this.onDownloadCompleted = onDownloadCompleted;
return this;
}
}

View File

@ -38,13 +38,7 @@ public class UtilityExceptions {
Logger.e(ex, "Errore", (Object) ex.getStackTrace());
}
String errorMessage = CommonRESTException.tryRecognizeThenGetMessage(ex);
if (errorMessage == null) {
errorMessage = ex.getMessage();
if (ex.getCause() != null) errorMessage += "<br />" + ex.getCause().getMessage();
}
String errorMessage = recognizeExceptionMessage(ex);
FragmentManager fm = ContextHelper.getFragmentManagerFromContext(context);
@ -68,4 +62,15 @@ public class UtilityExceptions {
}
}
public static String recognizeExceptionMessage(Exception ex) {
String errorMessage = CommonRESTException.tryRecognizeThenGetMessage(ex);
if (errorMessage == null) {
errorMessage = ex.getMessage();
if (ex.getCause() != null) errorMessage += "<br />" + ex.getCause().getMessage();
}
return errorMessage;
}
}

View File

@ -93,7 +93,7 @@ public class MainActivity extends BaseActivity
mBinding = DataBindingUtil.inflate(LayoutInflater.from(this), R.layout.activity_main, null, false);
setContentView(mBinding.getRoot());
updatesManager.executeCheck(this, false);
updatesManager.executeCheck(this, getSupportFragmentManager(), false);
UtilityContext.initMainActivity(this);
setSupportActionBar(mBinding.appBarMain.toolbar);

View File

@ -253,7 +253,7 @@ public class MainSettingsFragment extends PreferenceFragmentCompat implements IT
private void checkUpdates() {
Snackbar.make(getView(), R.string.checking_updates, Snackbar.LENGTH_SHORT)
.show();
updatesManager.executeCheck(getContext(), true);
updatesManager.executeCheck(getContext(), getParentFragmentManager(), true);
}