diff --git a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java index 4f5bd29dd3..a59f81dbc9 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java +++ b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java @@ -4,6 +4,7 @@ package com.keyman.android; +import com.keyman.engine.util.DownloadFileUtils; import com.tavultesoft.kmapro.AdjustLongpressDelayActivity; import com.tavultesoft.kmapro.BuildConfig; import com.tavultesoft.kmapro.DefaultLanguageResource; @@ -84,7 +85,12 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven boolean mayHaveHapticFeedback = prefs.getBoolean(KeymanSettingsActivity.hapticFeedbackKey, false); KMManager.setHapticFeedback(mayHaveHapticFeedback); - KMManager.executeResourceUpdate(this); + // Checking for updates should never be allowed to crash the keyboard. + // Just silently fail if this occurs. + if(DownloadFileUtils.getDownloadManager(this) != null) { + // Will try to emit a toast if it fails - i.e., is not silent. + KMManager.executeResourceUpdate(this); + } } @Override diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java index 602112edf6..989c7716a6 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java @@ -157,6 +157,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene // Verify WebView installed and enabled before attempting to initialize KMManager KMManager.initialize(getApplicationContext(), KeyboardType.KEYBOARD_TYPE_INAPP); + KMManager.executeResourceUpdate(this); DefaultLanguageResource.install(context); diff --git a/android/KMAPro/kMAPro/src/main/res/values/strings.xml b/android/KMAPro/kMAPro/src/main/res/values/strings.xml index 01546f91ba..a0447d0f89 100644 --- a/android/KMAPro/kMAPro/src/main/res/values/strings.xml +++ b/android/KMAPro/kMAPro/src/main/res/values/strings.xml @@ -27,7 +27,6 @@ Install Updates - Version: %1$s diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardDownloaderActivity.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardDownloaderActivity.java index e4f3055763..3f96c18289 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardDownloaderActivity.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardDownloaderActivity.java @@ -72,7 +72,7 @@ public class KMKeyboardDownloaderActivity extends BaseActivity { //TODO: move to keyboard manager class private static ArrayList kbDownloadEventListeners = null; - + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/cloud/CloudDownloadMgr.java b/android/KMEA/app/src/main/java/com/keyman/engine/cloud/CloudDownloadMgr.java index 8a25ab09ba..bb8ee190b6 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/cloud/CloudDownloadMgr.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/cloud/CloudDownloadMgr.java @@ -5,11 +5,15 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.util.Log; +import android.widget.Toast; +import com.keyman.engine.R; import com.keyman.engine.util.KMLog; +import com.keyman.engine.util.DownloadFileUtils; import java.io.File; import java.util.HashMap; @@ -206,12 +210,34 @@ public class CloudDownloadMgr{ * @param params the cloud api params for download * @param the target models type * @param the cloud requests result type + * @return `false` if the download request cannot be executed; `true` otherwise. */ - public void executeAsDownload(Context aContext, String aDownloadIdentifier, + public boolean executeAsDownload(Context aContext, String aDownloadIdentifier, ModelType aTargetModel, ICloudDownloadCallback aCallback, - CloudApiTypes.CloudApiParam... params) - { + CloudApiTypes.CloudApiParam... params) { + try { + executeAsDownloadInternal(aContext, aDownloadIdentifier, aTargetModel, aCallback, params); + } catch (DownloadManagerDisabledException e) { + Toast.makeText(aContext, + aContext.getString(R.string.update_check_download_manager_disabled), + Toast.LENGTH_SHORT).show(); + return false; + } catch (Exception e) { + Toast.makeText(aContext, + aContext.getString(R.string.update_check_unavailable), + Toast.LENGTH_SHORT).show(); + KMLog.LogException(TAG, "Unexpected exception occurred during download/query attempt", e); + return false; + } + + return true; + } + + private void executeAsDownloadInternal(Context aContext, String aDownloadIdentifier, + ModelType aTargetModel, + ICloudDownloadCallback aCallback, + CloudApiTypes.CloudApiParam... params) throws DownloadManagerDisabledException { if(!isInitialized) { Log.w(TAG, "DownloadManager not initialized. Initializing CloudDownloadMgr."); initialize(aContext); @@ -219,16 +245,22 @@ public class CloudDownloadMgr{ KMLog.LogBreadcrumb("CloudDownloadMgr", "CloudDownloadMgr.executeAsDownload() called; already initialized", true); } + DownloadManager downloadManager = DownloadFileUtils.getDownloadManager(aContext); + if(downloadManager == null) { + // The callback object provided to us provides no way to directly signal a + // failure. That said, we can also immediately detect that we WILL fail and + // corresponding error _now_, rather than later. + // + // Unique custom error so it's easy to explicitly filter. + throw new DownloadManagerDisabledException(); + } + synchronized (downloadSetByDownloadIdentifier) { if (alreadyDownloadingData(aDownloadIdentifier) || params == null) { return; } - DownloadManager downloadManager = (DownloadManager) aContext.getSystemService(Context.DOWNLOAD_SERVICE); - if(downloadManager==null) - throw new IllegalStateException("DownloadManager is not available"); - aCallback.initializeContext(aContext); CloudApiTypes.CloudDownloadSet _downloadSet = diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/cloud/DownloadManagerDisabledException.java b/android/KMEA/app/src/main/java/com/keyman/engine/cloud/DownloadManagerDisabledException.java new file mode 100644 index 0000000000..2011ad0eb0 --- /dev/null +++ b/android/KMEA/app/src/main/java/com/keyman/engine/cloud/DownloadManagerDisabledException.java @@ -0,0 +1,7 @@ +package com.keyman.engine.cloud; + +public class DownloadManagerDisabledException extends RuntimeException { + DownloadManagerDisabledException() { + super("System service DownloadManager is not available and cannot facilitate downloads or queries."); + } +} \ No newline at end of file diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/cloud/impl/CloudLexicalModelMetaDataDownloadCallback.java b/android/KMEA/app/src/main/java/com/keyman/engine/cloud/impl/CloudLexicalModelMetaDataDownloadCallback.java index 2063d2d10f..62f65a96ed 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/cloud/impl/CloudLexicalModelMetaDataDownloadCallback.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/cloud/impl/CloudLexicalModelMetaDataDownloadCallback.java @@ -111,8 +111,8 @@ public class CloudLexicalModelMetaDataDownloadCallback implements ICloudDownload BaseActivity.makeToast(aContext, R.string.dictionary_download_start_in_background, Toast.LENGTH_SHORT); - CloudDownloadMgr.getInstance().executeAsDownload(aContext, - _r.additionalDownloadid, null, _callback, + CloudDownloadMgr.getInstance().executeAsDownload( + aContext, _r.additionalDownloadid, null, _callback, _r.additionalDownloads.toArray(new CloudApiTypes.CloudApiParam[0])); } } diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/data/CloudRepository.java b/android/KMEA/app/src/main/java/com/keyman/engine/data/CloudRepository.java index 4aad0a4e9f..4676d59c77 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/data/CloudRepository.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/data/CloudRepository.java @@ -13,12 +13,14 @@ import com.keyman.engine.KMManager; import com.keyman.engine.KeyboardPickerActivity; import com.keyman.engine.R; import com.keyman.engine.cloud.CloudApiTypes; +import com.keyman.engine.cloud.DownloadManagerDisabledException; import com.keyman.engine.cloud.impl.CloudCatalogDownloadCallback; import com.keyman.engine.cloud.impl.CloudCatalogDownloadReturns; import com.keyman.engine.cloud.CloudDataJsonUtil; import com.keyman.engine.cloud.CloudDownloadMgr; import com.keyman.engine.packages.JSONUtils; import com.keyman.engine.util.BCP47; +import com.keyman.engine.util.DownloadFileUtils; import com.keyman.engine.util.KMLog; import com.keyman.engine.util.VersionUtils; @@ -366,6 +368,10 @@ public class CloudRepository { * @param onFailure A callback to be triggered upon failure of a query. */ private void downloadMetaDataFromServer(@NonNull Context context, UpdateHandler updateHandler, Runnable onSuccess, Runnable onFailure) { + if(DownloadFileUtils.getDownloadManager(context) == null) { + onFailure.run(); + return; + } boolean cacheValid = getCacheValidity(context); // For local and PR test builds, force download of metadata @@ -405,8 +411,13 @@ public class CloudRepository { BaseActivity.makeToast(context, R.string.catalog_download_is_running_in_background, Toast.LENGTH_SHORT); } else { updateIsRunning = true; - CloudDownloadMgr.getInstance().executeAsDownload( + boolean executionStarted = CloudDownloadMgr.getInstance().executeAsDownload( context, DOWNLOAD_IDENTIFIER_CATALOGUE, memCachedDataset, _download_callback, params); + if(!executionStarted) { + // Since we couldn't initiate the execution's async components, + // we need to immediately clear the update-running flag. + updateIsRunning = false; + } } } } diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/logic/ResourcesUpdateTool.java b/android/KMEA/app/src/main/java/com/keyman/engine/logic/ResourcesUpdateTool.java index 9f02480bf2..8ff46bb0b7 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/logic/ResourcesUpdateTool.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/logic/ResourcesUpdateTool.java @@ -122,6 +122,8 @@ public class ResourcesUpdateTool implements KeyboardEventHandler.OnKeyboardDownl return; } + // Warning: can be run by our system keyboard, which will attempt to + // display the toast if it or the app is visible! BaseActivity.makeToast(currentContext, R.string.update_check_unavailable, Toast.LENGTH_SHORT); lastUpdateCheck = Calendar.getInstance(); updateCheckFailed = true; diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/util/DownloadFileUtils.java b/android/KMEA/app/src/main/java/com/keyman/engine/util/DownloadFileUtils.java index c16d3d381d..d9315f3f4d 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/util/DownloadFileUtils.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/util/DownloadFileUtils.java @@ -3,7 +3,9 @@ */ package com.keyman.engine.util; +import android.app.DownloadManager; import android.content.Context; +import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.provider.OpenableColumns; @@ -21,6 +23,37 @@ import java.io.InputStream; public final class DownloadFileUtils { private static final String TAG = "DownloadFileUtils"; + private static final String DOWNLOAD_MANAGER_PACKAGE_NAME = "com.android.providers.downloads"; + + /** + * Determines whether or not Android's `DownloadManager` service is active, only + * returning an instance of DownloadManager when it is currently accessible and enabled. + * Downloads and cloud queries are impossible when it's disabled. + * + * This solution is based heavily on + * https://gist.github.com/Folyd/b9412bb6e2b06eb511f7. + * @return A valid and enabled reference to the system's DownloadManager service. May be null + * if it is not accessible or is disabled. + */ + public static DownloadManager getDownloadManager(Context context) { + DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + if(downloadManager==null) { + return null; + } + + int state = context.getPackageManager().getApplicationEnabledSetting(DOWNLOAD_MANAGER_PACKAGE_NAME); + + if( + state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED || + state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER || + state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED + ) { + return null; + }; + + return downloadManager; + } + /** * Small class for returning information about a file downloaded via DownloadManager. */ diff --git a/android/KMEA/app/src/main/res/values/strings.xml b/android/KMEA/app/src/main/res/values/strings.xml index 737d728154..992b419952 100644 --- a/android/KMEA/app/src/main/res/values/strings.xml +++ b/android/KMEA/app/src/main/res/values/strings.xml @@ -208,6 +208,9 @@ Failed to access server! + + DownloadManager disabled - cannot check for updates + "All resources are up to date!"