From cc5cc495517da40df29510f1664843482509edf7 Mon Sep 17 00:00:00 2001 From: simonpoole Date: Wed, 13 Sep 2023 13:19:28 +0200 Subject: [PATCH 1/4] Add options to safe mode --- .../java/de/blau/android/SafeModeTest.java | 95 +++++++ src/main/java/de/blau/android/Logic.java | 2 +- src/main/java/de/blau/android/Splash.java | 234 ++++++++++++------ .../de/blau/android/osm/StorageDelegator.java | 3 +- .../de/blau/android/resources/DataStyle.java | 4 +- src/main/res/layout/safe_mode.xml | 64 +++++ src/main/res/values/strings.xml | 8 + src/main/res/values/styles.xml | 8 +- 8 files changed, 338 insertions(+), 80 deletions(-) create mode 100644 src/androidTest/java/de/blau/android/SafeModeTest.java create mode 100644 src/main/res/layout/safe_mode.xml diff --git a/src/androidTest/java/de/blau/android/SafeModeTest.java b/src/androidTest/java/de/blau/android/SafeModeTest.java new file mode 100644 index 0000000000..e446e56edc --- /dev/null +++ b/src/androidTest/java/de/blau/android/SafeModeTest.java @@ -0,0 +1,95 @@ +package de.blau.android; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import android.app.Instrumentation; +import android.app.Instrumentation.ActivityMonitor; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.rule.ActivityTestRule; +import androidx.test.uiautomator.UiDevice; +import de.blau.android.layer.MapViewLayer; +import de.blau.android.prefs.Preferences; +import de.blau.android.resources.DataStyle; + +/** + * + * This test was originally written with ActivityScenario however that delivers the launch intent -twice- to the + * activity, the 1st time with out the intent extras + * + * @author simon + * + */ +@RunWith(AndroidJUnit4.class) +@LargeTest +public class SafeModeTest { + + Instrumentation instrumentation = null; + Context context = null; + UiDevice device = null; + + @Rule + public ActivityTestRule mActivityRule = new ActivityTestRule<>(Splash.class, false, false); + + /** + * Pre-test setup + */ + @Before + public void setup() { + instrumentation = InstrumentationRegistry.getInstrumentation(); + device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + context = instrumentation.getTargetContext(); + Intent start = Intent.makeMainActivity(new ComponentName(context, Splash.class)); + start.putExtra(Splash.SAFE, true); + Splash splash = mActivityRule.launchActivity(start); + assertNotNull(splash); + } + + /** + * Reset map style and disable all layers + */ + @Test + public void defaultOptions() { + assertTrue(TestUtils.clickText(device, false, context.getString(R.string.Continue), true)); + ActivityMonitor monitor = instrumentation.addMonitor(Main.class.getName(), null, false); + Main main = (Main) monitor.waitForActivityWithTimeout(5000); + TestUtils.grantPermissons(device); + TestUtils.dismissStartUpDialogs(device, context); + + Preferences prefs = App.getLogic().getPrefs(); + assertEquals(prefs.getDataStyle(), DataStyle.getBuiltinStyleName()); + + Map map = App.getLogic().getMap(); + for (MapViewLayer l : map.getLayers()) { + assertFalse(l.isVisible()); + } + } + + /** + * Reset map style and disable all layers + */ + @Test + public void resetState() { + TestUtils.clickResource(device, false, device.getCurrentPackageName() + ":id/safe_state_check", false); + assertTrue(TestUtils.clickText(device, false, context.getString(R.string.Continue), true)); + assertTrue(TestUtils.findText(device, false, context.getString(R.string.safe_delete_state_title), 5000)); + assertTrue(TestUtils.clickText(device, false, context.getString(R.string.safe_delete_state_text), true)); + ActivityMonitor monitor = instrumentation.addMonitor(Main.class.getName(), null, false); + Main main = (Main) monitor.waitForActivityWithTimeout(5000); + TestUtils.grantPermissons(device); + TestUtils.dismissStartUpDialogs(device, context); + assertTrue(App.getDelegator().isEmpty()); + } +} diff --git a/src/main/java/de/blau/android/Logic.java b/src/main/java/de/blau/android/Logic.java index 1f33bbd701..c01efa461d 100644 --- a/src/main/java/de/blau/android/Logic.java +++ b/src/main/java/de/blau/android/Logic.java @@ -3857,7 +3857,7 @@ protected Integer doInBackground(Void v) { setBorders(mainMap); return READ_OK; } - if (getDelegator().readFromFile(activity, StorageDelegator.FILENAME + ".backup")) { + if (getDelegator().readFromFile(activity, StorageDelegator.BACKUP_FILENAME)) { getDelegator().dirty(); // we need to overwrite the saved state asap setBorders(mainMap); return READ_BACKUP; diff --git a/src/main/java/de/blau/android/Splash.java b/src/main/java/de/blau/android/Splash.java index f4cf7cd98f..1dd4e64189 100644 --- a/src/main/java/de/blau/android/Splash.java +++ b/src/main/java/de/blau/android/Splash.java @@ -5,6 +5,7 @@ import android.annotation.SuppressLint; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageInfo; @@ -13,19 +14,30 @@ import android.os.Build; import android.os.Bundle; import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.RelativeLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AlertDialog.Builder; import androidx.appcompat.app.AppCompatActivity; import androidx.core.splashscreen.SplashScreen; import androidx.preference.PreferenceManager; import de.blau.android.contract.Paths; import de.blau.android.dialogs.Progress; +import de.blau.android.layer.LayerConfig; +import de.blau.android.osm.StorageDelegator; +import de.blau.android.prefs.AdvancedPrefDatabase; import de.blau.android.resources.DataStyle; import de.blau.android.resources.KeyDatabaseHelper; import de.blau.android.resources.TileLayerDatabase; import de.blau.android.resources.TileLayerSource; import de.blau.android.util.ExecutorTask; import de.blau.android.util.FileUtil; +import de.blau.android.util.ThemeUtils; /** * Originally based https://www.bignerdranch.com/blog/splash-screens-the-right-way/ @@ -39,8 +51,12 @@ public class Splash extends AppCompatActivity { private static final String DEBUG_TAG = Splash.class.getSimpleName(); - static final String SHORTCUT_EXTRAS_KEY = "shortcut_extras"; - private static final String SAFE = "safe"; + static final String SHORTCUT_EXTRAS_KEY = "shortcut_extras"; + static final String SAFE = "safe"; + + private Bundle shortcutExtras; + private Object startedLock = new Object(); + private boolean started = false; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -56,92 +72,160 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); } + final ExecutorTask startup = new ExecutorTask() { + + boolean newInstall; + boolean newConfig; + boolean migratePublicDirectory; + + @Override + protected Void doInBackground(Void param) { + try (TileLayerDatabase db = new TileLayerDatabase(Splash.this)) { + Log.d(DEBUG_TAG, "checking last tile source update"); + long lastDatabaseUpdate = 0; + try { + lastDatabaseUpdate = Math.max(TileLayerDatabase.getSourceUpdate(db.getReadableDatabase(), TileLayerDatabase.SOURCE_JOSM_IMAGERY), + TileLayerDatabase.getSourceUpdate(db.getReadableDatabase(), TileLayerDatabase.SOURCE_ELI)); + } catch (SQLiteException sex) { + Log.e(DEBUG_TAG, "Exception accessing tile layer database " + sex.getMessage()); + cancel(); + } + Log.d(DEBUG_TAG, "checking last package update"); + long lastUpdateTime = 0L; + try { + String packageName = Splash.this.getPackageName(); + PackageInfo packageInfo = getPackageManager().getPackageInfo(packageName, 0); + lastUpdateTime = packageInfo.lastUpdateTime; + } catch (NameNotFoundException e1) { + // can't really happen + } + newInstall = lastDatabaseUpdate == 0; + newConfig = lastUpdateTime > lastDatabaseUpdate; + if (newInstall || newConfig) { + migratePublicDirectory = !FileUtil.publicDirectoryExists(); + Progress.showDialog(Splash.this, migratePublicDirectory ? Progress.PROGRESS_MIGRATION : Progress.PROGRESS_BUILDING_IMAGERY_DATABASE); + } + if (migratePublicDirectory) { + directoryMigration(Splash.this); + Splash.this.runOnUiThread(() -> { + Progress.dismissDialog(Splash.this, Progress.PROGRESS_MIGRATION); + Progress.showDialog(Splash.this, Progress.PROGRESS_BUILDING_IMAGERY_DATABASE); + }); + } + if (newInstall || newConfig) { + KeyDatabaseHelper.readKeysFromAssets(Splash.this); + } + if (!isCancelled()) { + TileLayerSource.createOrUpdateCustomSource(Splash.this, db.getWritableDatabase(), true); + if (newInstall || newConfig) { + TileLayerSource.createOrUpdateFromAssetsSource(Splash.this, db.getWritableDatabase(), newConfig, true); + } + TileLayerSource.getListsLocked(Splash.this, db.getReadableDatabase(), true); + } + } + if (newInstall || newConfig) { + Progress.dismissDialog(Splash.this, Progress.PROGRESS_BUILDING_IMAGERY_DATABASE); + } + // read Presets here to avoid reading them on UI thread on startup of Main + Progress.showDialog(Splash.this, Progress.PROGRESS_LOADING_PRESET); + App.getCurrentPresets(Splash.this); + // + Intent intent = new Intent(Splash.this, Main.class); + intent.putExtra(SHORTCUT_EXTRAS_KEY, shortcutExtras); + startActivity(intent); + return null; + } + + @Override + protected void onPostExecute(Void result) { + Log.d(DEBUG_TAG, "onPostExecute"); + Progress.dismissDialog(Splash.this, Progress.PROGRESS_LOADING_PRESET); + Splash.this.finish(); + } + }; + @SuppressWarnings("deprecation") @SuppressLint("NewApi") @Override protected void onResume() { super.onResume(); Log.d(DEBUG_TAG, "onResume"); - final Bundle shortcutExtras = getIntent().getExtras(); - if (shortcutExtras != null && shortcutExtras.getBoolean(SAFE)) { - // do anything here to make startup safe - // currently this is only setting the data style to the minimal built in version to avoid issues with Skia - Log.d(DEBUG_TAG, "Starting in safe mode!"); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - prefs.edit().putString(getString(R.string.config_mapProfile_key), DataStyle.getBuiltinStyleName()).commit(); + synchronized (startedLock) { + if (!started) { + started = true; + shortcutExtras = getIntent().getExtras(); + if (shortcutExtras != null && shortcutExtras.getBoolean(SAFE)) { + showSafeModeDialog(startup); + return; + } + startup.execute(); + } } + } - new ExecutorTask() { - - boolean newInstall; - boolean newConfig; - boolean migratePublicDirectory; - - @Override - protected Void doInBackground(Void param) { - try (TileLayerDatabase db = new TileLayerDatabase(Splash.this)) { - Log.d(DEBUG_TAG, "checking last tile source update"); - long lastDatabaseUpdate = 0; - try { - lastDatabaseUpdate = Math.max(TileLayerDatabase.getSourceUpdate(db.getReadableDatabase(), TileLayerDatabase.SOURCE_JOSM_IMAGERY), - TileLayerDatabase.getSourceUpdate(db.getReadableDatabase(), TileLayerDatabase.SOURCE_ELI)); - } catch (SQLiteException sex) { - Log.e(DEBUG_TAG, "Exception accessing tile layer database " + sex.getMessage()); - cancel(); - } - Log.d(DEBUG_TAG, "checking last package update"); - long lastUpdateTime = 0L; - try { - String packageName = Splash.this.getPackageName(); - PackageInfo packageInfo = getPackageManager().getPackageInfo(packageName, 0); - lastUpdateTime = packageInfo.lastUpdateTime; - } catch (NameNotFoundException e1) { - // can't really happen - } - newInstall = lastDatabaseUpdate == 0; - newConfig = lastUpdateTime > lastDatabaseUpdate; - if (newInstall || newConfig) { - migratePublicDirectory = !FileUtil.publicDirectoryExists(); - Progress.showDialog(Splash.this, migratePublicDirectory ? Progress.PROGRESS_MIGRATION : Progress.PROGRESS_BUILDING_IMAGERY_DATABASE); - } - if (migratePublicDirectory) { - directoryMigration(Splash.this); - Splash.this.runOnUiThread(() -> { - Progress.dismissDialog(Splash.this, Progress.PROGRESS_MIGRATION); - Progress.showDialog(Splash.this, Progress.PROGRESS_BUILDING_IMAGERY_DATABASE); - }); - } - if (newInstall || newConfig) { - KeyDatabaseHelper.readKeysFromAssets(Splash.this); - } - if (!isCancelled()) { - TileLayerSource.createOrUpdateCustomSource(Splash.this, db.getWritableDatabase(), true); - if (newInstall || newConfig) { - TileLayerSource.createOrUpdateFromAssetsSource(Splash.this, db.getWritableDatabase(), newConfig, true); + /** + * Show a dialog with options for safe mode + * + * Note: dismissing the dialog before the task is run leads to the background vanishing + * + * @param startupTask the ExecutorTask that is run to actually start the app + */ + private void showSafeModeDialog(@NonNull ExecutorTask startupTask) { + + final LayoutInflater inflater = ThemeUtils.getLayoutInflater(this); + RelativeLayout layout = (RelativeLayout) inflater.inflate(R.layout.safe_mode, null); + + Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.safe_mode_dialog_title); + builder.setView(layout); + + CheckBox style = (CheckBox) layout.findViewById(R.id.safe_style_check); + CheckBox layers = (CheckBox) layout.findViewById(R.id.safe_layer_check); + CheckBox state = (CheckBox) layout.findViewById(R.id.safe_state_check); + + builder.setPositiveButton(R.string.Continue, null); + builder.setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> finish()); + + final AlertDialog dialog = builder.create(); + dialog.setOnShowListener((DialogInterface d) -> { + final Button positive = dialog.getButton(DialogInterface.BUTTON_POSITIVE); + positive.setOnClickListener((View v) -> { + Log.e(DEBUG_TAG, "Starting in safe mode"); + if (style.isChecked()) { + // use minimal data style + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.edit().putString(getString(R.string.config_mapProfile_key), DataStyle.getBuiltinStyleName()).commit(); + } + if (layers.isChecked()) { + // hide all layers + try (AdvancedPrefDatabase db = new AdvancedPrefDatabase(this)) { + final LayerConfig[] layerConfigs = db.getLayers(); + for (LayerConfig config : layerConfigs) { + db.setLayerVisibility(config.getPosition(), false); } - TileLayerSource.getListsLocked(Splash.this, db.getReadableDatabase(), true); } } - if (newInstall || newConfig) { - Progress.dismissDialog(Splash.this, Progress.PROGRESS_BUILDING_IMAGERY_DATABASE); + if (state.isChecked()) { + Builder reallyBuilder = new AlertDialog.Builder(this); + reallyBuilder.setTitle(R.string.safe_delete_state_title); + reallyBuilder.setPositiveButton(R.string.safe_delete_state_text, (DialogInterface dialog2, int which2) -> { + Log.e(DEBUG_TAG, "Removing state files"); + this.deleteFile(StorageDelegator.FILENAME); + this.deleteFile(StorageDelegator.BACKUP_FILENAME); + dialog.dismiss(); + startupTask.execute(); + }); + reallyBuilder.setNegativeButton(R.string.no, null); + reallyBuilder.show(); + return; } - // read Presets here to avoid reading them on UI thread on startup of Main - Progress.showDialog(Splash.this, Progress.PROGRESS_LOADING_PRESET); - App.getCurrentPresets(Splash.this); - // - Intent intent = new Intent(Splash.this, Main.class); - intent.putExtra(SHORTCUT_EXTRAS_KEY, shortcutExtras); - startActivity(intent); - return null; - } + dialog.dismiss(); + startupTask.execute(); + }); + }); + + dialog.show(); - @Override - protected void onPostExecute(Void result) { - Log.d(DEBUG_TAG, "onPostExecute"); - Progress.dismissDialog(Splash.this, Progress.PROGRESS_LOADING_PRESET); - Splash.this.finish(); - } - }.execute(); } /** diff --git a/src/main/java/de/blau/android/osm/StorageDelegator.java b/src/main/java/de/blau/android/osm/StorageDelegator.java index 2f215f95d6..64a260ac6c 100755 --- a/src/main/java/de/blau/android/osm/StorageDelegator.java +++ b/src/main/java/de/blau/android/osm/StorageDelegator.java @@ -83,7 +83,8 @@ public class StorageDelegator implements Serializable, Exportable, DataStorage { */ private transient boolean imageryRecorded = false; - public static final String FILENAME = "lastActivity" + "." + FileExtensions.RES; + public static final String FILENAME = "lastActivity" + "." + FileExtensions.RES; + public static final String BACKUP_FILENAME = FILENAME + ".backup"; private transient SavingHelper savingHelper = new SavingHelper<>(); diff --git a/src/main/java/de/blau/android/resources/DataStyle.java b/src/main/java/de/blau/android/resources/DataStyle.java index 681432cb80..c4c88a72e2 100644 --- a/src/main/java/de/blau/android/resources/DataStyle.java +++ b/src/main/java/de/blau/android/resources/DataStyle.java @@ -67,10 +67,10 @@ import de.blau.android.util.XmlFileFilter; public final class DataStyle extends DefaultHandler { - private static final String I18N_DATASTYLE = "i18n/datastyle_"; - private static final String DEBUG_TAG = "DataStyle"; + private static final String I18N_DATASTYLE = "i18n/datastyle_"; + private static final Version CURRENT_VERSION = new Version("0.3.0"); private static final String FILE_PATH_STYLE_SUFFIX = "-profile.xml"; diff --git a/src/main/res/layout/safe_mode.xml b/src/main/res/layout/safe_mode.xml new file mode 100644 index 0000000000..67f5d25b32 --- /dev/null +++ b/src/main/res/layout/safe_mode.xml @@ -0,0 +1,64 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 01e0f801e3..a0c5e8d8d8 100755 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -332,6 +332,13 @@ Require optional Add re-survey entry Add check entry + + Safe mode options + Set data style to minimal + Disable all layers + Remove saved state + Delete saved data state? + Yes (cannot be undone) Empty relation The %1$s relation has no members.\n\nLeaving it as is will make it non-editable in Vespucci. @@ -1411,6 +1418,7 @@ Default Regular expression State + Continue No entries Geocoder type diff --git a/src/main/res/values/styles.xml b/src/main/res/values/styles.xml index 700c7aefa7..f15dfabc2e 100644 --- a/src/main/res/values/styles.xml +++ b/src/main/res/values/styles.xml @@ -7,28 +7,34 @@ @drawable/vespucci_splash @color/osm_green @style/SplashTheme2 + @color/material_red -