Merge pull request #13218 from keymanapp/fix/android/prevent-update-check-crashes

fix(android): prevent update-checks from crashing system keyboard and app if `DownloadManager` is disabled
This commit is contained in:
Joshua Horton 2025-02-17 14:36:54 +07:00 committed by GitHub
commit fff9ba45ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 107 additions and 13 deletions

View file

@ -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

View file

@ -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);

View file

@ -27,7 +27,6 @@
<!-- Context: Menu Action -->
<string name="action_install_updates" comment="Menu notification that keyboard or dictionary updates available">Install Updates</string>
<!-- Context: Title -->
<string name="title_version" comment="Title of Keyman for Android version">Version: %1$s</string>

View file

@ -72,7 +72,7 @@ public class KMKeyboardDownloaderActivity extends BaseActivity {
//TODO: move to keyboard manager class
private static ArrayList<KeyboardEventHandler.OnKeyboardDownloadEventListener> kbDownloadEventListeners = null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

View file

@ -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 <ModelType> the target models type
* @param <ResultType> the cloud requests result type
* @return `false` if the download request cannot be executed; `true` otherwise.
*/
public <ModelType,ResultType> void executeAsDownload(Context aContext, String aDownloadIdentifier,
public <ModelType,ResultType> boolean executeAsDownload(Context aContext, String aDownloadIdentifier,
ModelType aTargetModel,
ICloudDownloadCallback<ModelType,ResultType> 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 <ModelType,ResultType> void executeAsDownloadInternal(Context aContext, String aDownloadIdentifier,
ModelType aTargetModel,
ICloudDownloadCallback<ModelType,ResultType> 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<ModelType,ResultType> _downloadSet =

View file

@ -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.");
}
}

View file

@ -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]));
}
}

View file

@ -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;
}
}
}
}

View file

@ -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;

View file

@ -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.
*/

View file

@ -208,6 +208,9 @@
<!-- Context: General Updates -->
<string name="update_check_unavailable" comment="Error message when a Keyman server can't be reached">Failed to access server!</string>
<!-- Context: Update-check failure notification -->
<string name="update_check_download_manager_disabled" comment="Notification that a system service needed for updates is disabled">DownloadManager disabled - cannot check for updates</string>
<!-- Context: General Updates -->
<string name="update_check_current" comment="Notification that all resources are up to date">"All resources are up to date!"</string>