mirror of
https://github.com/keymanapp/keyman.git
synced 2026-09-24 08:37:42 +00:00
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:
commit
fff9ba45ef
11 changed files with 107 additions and 13 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
|
@ -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]));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue