diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2ac76d35..fbedfe47 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,17 +18,12 @@ coreApp="true" package="org.dslul.openboard.inputmethod.latin"> - - - - - - - - - - - @@ -162,22 +145,6 @@ - - - - - - - - - - - - - - - - dictFiles = getDictionaryWordListsForLocale(clientId, locale); // TODO: pass clientId to the following function - DictionaryService.updateNowIfNotUpdatedInAVeryLongTime(getContext()); if (null != dictFiles && dictFiles.size() > 0) { PrivateLog.log("Returned " + dictFiles.size() + " files"); return new ResourcePathCursor(dictFiles); @@ -364,18 +363,8 @@ public final class DictionaryProvider extends ContentProvider { final String[] wordListIdArray = TextUtils.split(wordListId, ID_CATEGORY_SEPARATOR); final String wordListCategory; - if (2 == wordListIdArray.length) { - // This is at the category:manual_id format. - wordListCategory = wordListIdArray[0]; - // We don't need to read wordListIdArray[1] here, because it's irrelevant to - // word list selection - it's just a name we use to identify which data file - // is a newer version of which word list. We do however return the full id - // string for each selected word list, so in this sense we are 'using' it. - } else { - // This does not contain a colon, like the old format does. Old-format IDs - // always point to main dictionaries, so we force the main category upon it. - wordListCategory = UpdateHandler.MAIN_DICTIONARY_CATEGORY; - } + // This is at the category:manual_id format. + wordListCategory = wordListIdArray[0]; final String wordListLocale = results.getString(localeIndex); final String wordListLocalFilename = results.getString(localFileNameIndex); final String wordListRawChecksum = results.getString(rawChecksumIndex); @@ -403,10 +392,6 @@ public final class DictionaryProvider extends ContentProvider { if (!f.isFile()) { continue; } - } else if (MetadataDbHelper.STATUS_AVAILABLE == wordListStatus) { - // The locale is the id for the main dictionary. - UpdateHandler.installIfNeverRequested(context, clientId, wordListId); - continue; } final WordListInfo currentBestMatch = dicts.get(wordListCategory); if (null == currentBestMatch @@ -457,26 +442,6 @@ public final class DictionaryProvider extends ContentProvider { } final int status = wordList.getAsInteger(MetadataDbHelper.STATUS_COLUMN); final int version = wordList.getAsInteger(MetadataDbHelper.VERSION_COLUMN); - if (MetadataDbHelper.STATUS_DELETING == status) { - UpdateHandler.markAsDeleted(getContext(), clientId, wordlistId, version, status); - return 1; - } - if (MetadataDbHelper.STATUS_INSTALLED == status) { - final String result = uri.getQueryParameter(QUERY_PARAMETER_DELETE_RESULT); - if (QUERY_PARAMETER_FAILURE.equals(result)) { - if (DEBUG) { - Log.d(TAG, - "Dictionary is broken, attempting to retry download & installation."); - } - UpdateHandler.markAsBrokenOrRetrying(getContext(), clientId, wordlistId, version); - } - final String localFilename = - wordList.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN); - final File f = getContext().getFileStreamPath(localFilename); - // f.delete() returns true if the file was successfully deleted, false otherwise - return f.delete() ? 1 : 0; - } - Log.e(TAG, "Attempt to delete a file whose status is " + status); return 0; } @@ -512,11 +477,6 @@ public final class DictionaryProvider extends ContentProvider { } catch (final BadFormatException e) { Log.w(TAG, "Not enough information to insert this dictionary " + values, e); } - // We just received new information about the list of dictionary for this client. - // For all intents and purposes, this is new metadata, so we should publish it - // so that any listeners (like the Settings interface for example) can update - // themselves. - UpdateHandler.publishUpdateMetadataCompleted(getContext(), true); break; case DICTIONARY_V1_WHOLE_LIST: case DICTIONARY_V1_DICT_INFO: diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryService.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryService.java deleted file mode 100644 index 55e4484c..00000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryService.java +++ /dev/null @@ -1,274 +0,0 @@ -/** - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package org.dslul.openboard.inputmethod.dictionarypack; - -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.os.IBinder; -import android.util.Log; -import android.widget.Toast; - -import org.dslul.openboard.inputmethod.latin.BinaryDictionaryFileDumper; -import org.dslul.openboard.inputmethod.latin.R; -import org.dslul.openboard.inputmethod.latin.common.LocaleUtils; - -import java.util.Locale; -import java.util.Random; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -import javax.annotation.Nonnull; - -/** - * Service that handles background tasks for the dictionary provider. - * - * This service provides the context for the long-running operations done by the - * dictionary provider. Those include: - * - Checking for the last update date and scheduling the next update. This runs every - * day around midnight, upon reception of the DATE_CHANGED_INTENT_ACTION broadcast. - * Every four days, it schedules an update of the metadata with the alarm manager. - * - Issuing the order to update the metadata. This runs every four days, between 0 and - * 6, upon reception of the UPDATE_NOW_INTENT_ACTION broadcast sent by the alarm manager - * as a result of the above action. - * - Handling a download that just ended. These come in two flavors: - * - Metadata is finished downloading. We should check whether there are new dictionaries - * available, and download those that we need that have new versions. - * - A dictionary file finished downloading. We should put the file ready for a client IME - * to access, and mark the current state as such. - */ -public final class DictionaryService extends Service { - private static final String TAG = DictionaryService.class.getSimpleName(); - - /** - * The package name, to use in the intent actions. - */ - private static final String PACKAGE_NAME = "org.dslul.openboard.inputmethod.latin"; - - /** - * The action of the date changing, used to schedule a periodic freshness check - */ - private static final String DATE_CHANGED_INTENT_ACTION = - Intent.ACTION_DATE_CHANGED; - - /** - * The action of displaying a toast to warn the user an automatic download is starting. - */ - /* package */ static final String SHOW_DOWNLOAD_TOAST_INTENT_ACTION = - PACKAGE_NAME + ".SHOW_DOWNLOAD_TOAST_INTENT_ACTION"; - - /** - * A locale argument, as a String. - */ - /* package */ static final String LOCALE_INTENT_ARGUMENT = "locale"; - - /** - * How often, in milliseconds, we want to update the metadata. This is a - * floor value; actually, it may happen several hours later, or even more. - */ - private static final long UPDATE_FREQUENCY_MILLIS = TimeUnit.DAYS.toMillis(4); - - /** - * We are waked around midnight, local time. We want to wake between midnight and 6 am, - * roughly. So use a random time between 0 and this delay. - */ - private static final int MAX_ALARM_DELAY_MILLIS = (int)TimeUnit.HOURS.toMillis(6); - - /** - * How long we consider a "very long time". If no update took place in this time, - * the content provider will trigger an update in the background. - */ - private static final long VERY_LONG_TIME_MILLIS = TimeUnit.DAYS.toMillis(14); - - /** - * After starting a download, how long we wait before considering it may be stuck. After this - * period is elapsed, if the keyboard tries to download again, then we cancel and re-register - * the request; if it's within this time, we just leave it be. - * It's important to note that we do not re-submit the request merely because the time is up. - * This is only to decide whether to cancel the old one and re-requesting when the keyboard - * fires a new request for the same data. - */ - public static final long NO_CANCEL_DOWNLOAD_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(30); - - /** - * An executor that serializes tasks given to it. - */ - private ThreadPoolExecutor mExecutor; - private static final int WORKER_THREAD_TIMEOUT_SECONDS = 15; - - @Override - public void onCreate() { - // By default, a thread pool executor does not timeout its core threads, so it will - // never kill them when there isn't any work to do any more. That would mean the service - // can never die! By creating it this way and calling allowCoreThreadTimeOut, we allow - // the single thread to time out after WORKER_THREAD_TIMEOUT_SECONDS = 15 seconds, allowing - // the process to be reclaimed by the system any time after that if it's not doing - // anything else. - // Executors#newSingleThreadExecutor creates a ThreadPoolExecutor but it returns the - // superclass ExecutorService which does not have the #allowCoreThreadTimeOut method, - // so we can't use that. - mExecutor = new ThreadPoolExecutor(1 /* corePoolSize */, 1 /* maximumPoolSize */, - WORKER_THREAD_TIMEOUT_SECONDS /* keepAliveTime */, - TimeUnit.SECONDS /* unit for keepAliveTime */, - new LinkedBlockingQueue() /* workQueue */); - mExecutor.allowCoreThreadTimeOut(true); - } - - @Override - public void onDestroy() { - } - - @Override - public IBinder onBind(Intent intent) { - // This service cannot be bound - return null; - } - - /** - * Executes an explicit command. - * - * This is the entry point for arbitrary commands that are executed upon reception of certain - * events that should be executed on the context of this service. The supported commands are: - * - Check last update time and possibly schedule an update of the data for later. - * This is triggered every day, upon reception of the DATE_CHANGED_INTENT_ACTION broadcast. - * - Update data NOW. - * This is normally received upon trigger of the scheduled update. - * - Handle a finished download. - * This executes the actions that must be taken after a file (metadata or dictionary data - * has been downloaded (or failed to download). - * The commands that can be spun an another thread will be executed serially, in order, on - * a worker thread that is created on demand and terminates after a short while if there isn't - * any work left to do. - */ - @Override - public synchronized int onStartCommand(final Intent intent, final int flags, - final int startId) { - final DictionaryService self = this; - if (SHOW_DOWNLOAD_TOAST_INTENT_ACTION.equals(intent.getAction())) { - final String localeString = intent.getStringExtra(LOCALE_INTENT_ARGUMENT); - if (localeString == null) { - Log.e(TAG, "Received " + intent.getAction() + " without locale; skipped"); - } else { - // This is a UI action, it can't be run in another thread - showStartDownloadingToast( - this, LocaleUtils.constructLocaleFromString(localeString)); - } - } else { - // If it's a command that does not require UI, arrange for the work to be done on a - // separate thread, so that we can return right away. The executor will spawn a thread - // if necessary, or reuse a thread that has become idle as appropriate. - // DATE_CHANGED or UPDATE_NOW are examples of commands that can be done on another - // thread. - mExecutor.submit(new Runnable() { - @Override - public void run() { - dispatchBroadcast(self, intent); - // Since calls to onStartCommand are serialized, the submissions to the executor - // are serialized. That means we are guaranteed to call the stopSelfResult() - // in the same order that we got them, so we don't need to take care of the - // order. - stopSelfResult(startId); - } - }); - } - return Service.START_REDELIVER_INTENT; - } - - static void dispatchBroadcast(final Context context, final Intent intent) { - final String action = intent.getAction(); - if (DATE_CHANGED_INTENT_ACTION.equals(action)) { - // This happens when the date of the device changes. This normally happens - // at midnight local time, but it may happen if the user changes the date - // by hand or something similar happens. - checkTimeAndMaybeSetupUpdateAlarm(context); - } else if (DictionaryPackConstants.UPDATE_NOW_INTENT_ACTION.equals(action)) { - // Intent to trigger an update now. - UpdateHandler.tryUpdate(context); - } else if (DictionaryPackConstants.INIT_AND_UPDATE_NOW_INTENT_ACTION.equals(action)) { - // Initialize the client Db. - final String mClientId = context.getString(R.string.dictionary_pack_client_id); - BinaryDictionaryFileDumper.initializeClientRecordHelper(context, mClientId); - - // Updates the metadata and the download the dictionaries. - UpdateHandler.tryUpdate(context); - } else { - UpdateHandler.downloadFinished(context, intent); - } - } - - /** - * Setups an alarm to check for updates if an update is due. - */ - private static void checkTimeAndMaybeSetupUpdateAlarm(final Context context) { - // Of all clients, if the one that hasn't been updated for the longest - // is still more recent than UPDATE_FREQUENCY_MILLIS, do nothing. - if (!isLastUpdateAtLeastThisOld(context, UPDATE_FREQUENCY_MILLIS)) return; - - PrivateLog.log("Date changed - registering alarm"); - AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); - - // Best effort to wake between midnight and MAX_ALARM_DELAY_MILLIS in the morning. - // It doesn't matter too much if this is very inexact. - final long now = System.currentTimeMillis(); - final long alarmTime = now + new Random().nextInt(MAX_ALARM_DELAY_MILLIS); - final Intent updateIntent = new Intent(DictionaryPackConstants.UPDATE_NOW_INTENT_ACTION); - final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, - updateIntent, PendingIntent.FLAG_CANCEL_CURRENT); - - // We set the alarm in the type that doesn't forcefully wake the device - // from sleep, but fires the next time the device actually wakes for any - // other reason. - if (null != alarmManager) alarmManager.set(AlarmManager.RTC, alarmTime, pendingIntent); - } - - /** - * Utility method to decide whether the last update is older than a certain time. - * - * @return true if at least `time' milliseconds have elapsed since last update, false otherwise. - */ - private static boolean isLastUpdateAtLeastThisOld(final Context context, final long time) { - final long now = System.currentTimeMillis(); - final long lastUpdate = MetadataDbHelper.getOldestUpdateTime(context); - PrivateLog.log("Last update was " + lastUpdate); - return lastUpdate + time < now; - } - - /** - * Refreshes data if it hasn't been refreshed in a very long time. - * - * This will check the last update time, and if it's been more than VERY_LONG_TIME_MILLIS, - * update metadata now - and possibly take subsequent update actions. - */ - public static void updateNowIfNotUpdatedInAVeryLongTime(final Context context) { - if (!isLastUpdateAtLeastThisOld(context, VERY_LONG_TIME_MILLIS)) return; - UpdateHandler.tryUpdate(context); - } - - /** - * Shows a toast informing the user that an automatic dictionary download is starting. - */ - private static void showStartDownloadingToast(final Context context, - @Nonnull final Locale locale) { - final String toastText = String.format( - context.getString(R.string.toast_downloading_suggestions), - locale.getDisplayName()); - Toast.makeText(context, toastText, Toast.LENGTH_LONG).show(); - } -} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionarySettingsFragment.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionarySettingsFragment.java index 18d4f2ee..7ed924fb 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionarySettingsFragment.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionarySettingsFragment.java @@ -53,8 +53,7 @@ import java.util.TreeMap; /** * Preference screen. */ -public final class DictionarySettingsFragment extends PreferenceFragment - implements UpdateHandler.UpdateEventListener { +public final class DictionarySettingsFragment extends PreferenceFragment { private static final String TAG = DictionarySettingsFragment.class.getSimpleName(); static final private String DICT_LIST_ID = "list"; @@ -72,13 +71,6 @@ public final class DictionarySettingsFragment extends PreferenceFragment // never null private TreeMap mCurrentPreferenceMap = new TreeMap<>(); - private final BroadcastReceiver mConnectivityChangedReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - refreshNetworkState(); - } - }; - /** * Empty constructor for fragment generation. */ @@ -105,41 +97,12 @@ public final class DictionarySettingsFragment extends PreferenceFragment setHasOptionsMenu(true); } - @Override - public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { - new AsyncTask() { - @Override - protected String doInBackground(Void... params) { - return MetadataDbHelper.getMetadataUriAsString(getActivity(), mClientId); - } - - @Override - protected void onPostExecute(String metadataUri) { - // We only add the "Refresh" button if we have a non-empty URL to refresh from. If - // the URL is empty, of course we can't refresh so it makes no sense to display - // this. - if (!TextUtils.isEmpty(metadataUri)) { - if (mUpdateNowMenu == null) { - mUpdateNowMenu = menu.add(Menu.NONE, MENU_UPDATE_NOW, 0, - R.string.check_for_updates_now); - mUpdateNowMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); - } - refreshNetworkState(); - } - } - }.execute(); - } - @Override public void onResume() { super.onResume(); mChangedSettings = false; - UpdateHandler.registerUpdateEventListener(this); final Activity activity = getActivity(); final IntentFilter filter = new IntentFilter(); - filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); - getActivity().registerReceiver(mConnectivityChangedReceiver, filter); - refreshNetworkState(); new Thread("onResume") { @Override @@ -161,8 +124,6 @@ public final class DictionarySettingsFragment extends PreferenceFragment public void onPause() { super.onPause(); final Activity activity = getActivity(); - UpdateHandler.unregisterUpdateEventListener(this); - activity.unregisterReceiver(mConnectivityChangedReceiver); if (mChangedSettings) { final Intent newDictBroadcast = new Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION); @@ -171,36 +132,6 @@ public final class DictionarySettingsFragment extends PreferenceFragment } } - @Override - public void downloadedMetadata(final boolean succeeded) { - stopLoadingAnimation(); - if (!succeeded) return; // If the download failed nothing changed, so no need to refresh - new Thread("refreshInterface") { - @Override - public void run() { - refreshInterface(); - } - }.start(); - } - - @Override - public void wordListDownloadFinished(final String wordListId, final boolean succeeded) { - final WordListPreference pref = findWordListPreference(wordListId); - if (null == pref) return; - // TODO: Report to the user if !succeeded - final Activity activity = getActivity(); - if (null == activity) return; - activity.runOnUiThread(new Runnable() { - @Override - public void run() { - // We have to re-read the db in case the description has changed, and to - // find out what state it ended up if the download wasn't successful - // TODO: don't redo everything, only re-read and set this word list status - refreshInterface(); - } - }); - } - private WordListPreference findWordListPreference(final String id) { final PreferenceGroup prefScreen = getPreferenceScreen(); if (null == prefScreen) { @@ -220,17 +151,6 @@ public final class DictionarySettingsFragment extends PreferenceFragment return null; } - @Override - public void updateCycleCompleted() {} - - void refreshNetworkState() { - /* - NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); - boolean isConnected = null == info ? false : info.isConnected(); - */ - if (null != mUpdateNowMenu) mUpdateNowMenu.setEnabled(false); - } - void refreshInterface() { final Activity activity = getActivity(); if (null == activity) return; @@ -243,8 +163,6 @@ public final class DictionarySettingsFragment extends PreferenceFragment public void run() { // TODO: display this somewhere // if (0 != lastUpdate) mUpdateNowPreference.setSummary(updateNowSummary); - refreshNetworkState(); - removeAnyDictSettings(prefScreen); int i = 0; for (Preference preference : prefList) { @@ -361,80 +279,4 @@ public final class DictionarySettingsFragment extends PreferenceFragment } } - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case MENU_UPDATE_NOW: - if (View.GONE == mLoadingView.getVisibility()) { - startRefresh(); - } else { - cancelRefresh(); - } - return true; - } - return false; - } - - private void startRefresh() { - startLoadingAnimation(); - mChangedSettings = true; - UpdateHandler.registerUpdateEventListener(this); - final Activity activity = getActivity(); - new Thread("updateByHand") { - @Override - public void run() { - // We call tryUpdate(), which returns whether we could successfully start an update. - // If we couldn't, we'll never receive the end callback, so we stop the loading - // animation and return to the previous screen. - if (!UpdateHandler.tryUpdate(activity)) { - stopLoadingAnimation(); - } - } - }.start(); - } - - private void cancelRefresh() { - UpdateHandler.unregisterUpdateEventListener(this); - final Context context = getActivity(); - new Thread("cancelByHand") { - @Override - public void run() { - UpdateHandler.cancelUpdate(context, mClientId); - stopLoadingAnimation(); - } - }.start(); - } - - private void startLoadingAnimation() { - mLoadingView.setVisibility(View.VISIBLE); - getView().setVisibility(View.GONE); - // We come here when the menu element is pressed so presumably it can't be null. But - // better safe than sorry. - if (null != mUpdateNowMenu) mUpdateNowMenu.setTitle(R.string.cancel); - } - - void stopLoadingAnimation() { - final View preferenceView = getView(); - final Activity activity = getActivity(); - if (null == activity) return; - final View loadingView = mLoadingView; - final MenuItem updateNowMenu = mUpdateNowMenu; - activity.runOnUiThread(new Runnable() { - @Override - public void run() { - loadingView.setVisibility(View.GONE); - preferenceView.setVisibility(View.VISIBLE); - loadingView.startAnimation(AnimationUtils.loadAnimation( - activity, android.R.anim.fade_out)); - preferenceView.startAnimation(AnimationUtils.loadAnimation( - activity, android.R.anim.fade_in)); - // The menu is created by the framework asynchronously after the activity, - // which means it's possible to have the activity running but the menu not - // created yet - hence the necessity for a null check here. - if (null != updateNowMenu) { - updateNowMenu.setTitle(R.string.check_for_updates_now); - } - } - }); - } } diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DownloadIdAndStartDate.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DownloadIdAndStartDate.java deleted file mode 100644 index bdc92148..00000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DownloadIdAndStartDate.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.dslul.openboard.inputmethod.dictionarypack; - -/** - * A simple container of download ID and download start date. - */ -public class DownloadIdAndStartDate { - public final long mId; - public final long mStartDate; - public DownloadIdAndStartDate(final long id, final long startDate) { - mId = id; - mStartDate = startDate; - } -} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DownloadManagerWrapper.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DownloadManagerWrapper.java deleted file mode 100644 index 75caff66..00000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DownloadManagerWrapper.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package org.dslul.openboard.inputmethod.dictionarypack; - -import android.app.DownloadManager; -import android.app.DownloadManager.Query; -import android.app.DownloadManager.Request; -import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteException; -import android.os.ParcelFileDescriptor; -import android.util.Log; - -import java.io.FileNotFoundException; - -import javax.annotation.Nullable; - -/** - * A class to help with calling DownloadManager methods. - * - * Mostly, the problem here is that most methods from DownloadManager may throw SQL exceptions if - * they can't open the database on disk. We want to avoid crashing in these cases but can't do - * much more, so this class insulates the callers from these. SQLiteException also inherit from - * RuntimeException so they are unchecked :( - * While we're at it, we also insulate callers from the cases where DownloadManager is disabled, - * and getSystemService returns null. - */ -public class DownloadManagerWrapper { - private final static String TAG = DownloadManagerWrapper.class.getSimpleName(); - private final DownloadManager mDownloadManager; - - public DownloadManagerWrapper(final Context context) { - this((DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE)); - } - - private DownloadManagerWrapper(final DownloadManager downloadManager) { - mDownloadManager = downloadManager; - } - - public void remove(final long... ids) { - try { - if (null != mDownloadManager) { - mDownloadManager.remove(ids); - } - } catch (IllegalArgumentException e) { - // This is expected to happen on boot when the device is encrypted. - } catch (SQLiteException e) { - // We couldn't remove the file from DownloadManager. Apparently, the database can't - // be opened. It may be a problem with file system corruption. In any case, there is - // not much we can do apart from avoiding crashing. - Log.e(TAG, "Can't remove files with ID " + ids + " from download manager", e); - } - } - - public ParcelFileDescriptor openDownloadedFile(final long fileId) throws FileNotFoundException { - try { - if (null != mDownloadManager) { - return mDownloadManager.openDownloadedFile(fileId); - } - } catch (IllegalArgumentException e) { - // This is expected to happen on boot when the device is encrypted. - } catch (SQLiteException e) { - Log.e(TAG, "Can't open downloaded file with ID " + fileId, e); - } - // We come here if mDownloadManager is null or if an exception was thrown. - throw new FileNotFoundException(); - } - - @Nullable - public Cursor query(final Query query) { - try { - if (null != mDownloadManager) { - return mDownloadManager.query(query); - } - } catch (IllegalArgumentException e) { - // This is expected to happen on boot when the device is encrypted. - } catch (SQLiteException e) { - Log.e(TAG, "Can't query the download manager", e); - } - // We come here if mDownloadManager is null or if an exception was thrown. - return null; - } - - public long enqueue(final Request request) { - try { - if (null != mDownloadManager) { - return mDownloadManager.enqueue(request); - } - } catch (IllegalArgumentException e) { - // This is expected to happen on boot when the device is encrypted. - } catch (SQLiteException e) { - Log.e(TAG, "Can't enqueue a request with the download manager", e); - } - return 0; - } -} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DownloadOverMeteredDialog.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DownloadOverMeteredDialog.java deleted file mode 100644 index a3e518df..00000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DownloadOverMeteredDialog.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package org.dslul.openboard.inputmethod.dictionarypack; - -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.text.Html; -import android.view.View; -import android.widget.Button; -import android.widget.TextView; - -import org.dslul.openboard.inputmethod.annotations.ExternallyReferenced; -import org.dslul.openboard.inputmethod.latin.R; -import org.dslul.openboard.inputmethod.latin.common.LocaleUtils; - -import javax.annotation.Nullable; - -/** - * This implements the dialog for asking the user whether it's okay to download dictionaries over - * a metered connection or not (e.g. their mobile data plan). - */ -public final class DownloadOverMeteredDialog extends Activity { - final public static String CLIENT_ID_KEY = "client_id"; - final public static String WORDLIST_TO_DOWNLOAD_KEY = "wordlist_to_download"; - final public static String SIZE_KEY = "size"; - final public static String LOCALE_KEY = "locale"; - private String mClientId; - private String mWordListToDownload; - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - final Intent intent = getIntent(); - mClientId = intent.getStringExtra(CLIENT_ID_KEY); - mWordListToDownload = intent.getStringExtra(WORDLIST_TO_DOWNLOAD_KEY); - final String localeString = intent.getStringExtra(LOCALE_KEY); - final long size = intent.getIntExtra(SIZE_KEY, 0); - setContentView(R.layout.download_over_metered); - setTexts(localeString, size); - } - - private void setTexts(@Nullable final String localeString, final long size) { - final String promptFormat = getString(R.string.should_download_over_metered_prompt); - final String allowButtonFormat = getString(R.string.download_over_metered); - final String language = (null == localeString) ? "" - : LocaleUtils.constructLocaleFromString(localeString).getDisplayLanguage(); - final TextView prompt = (TextView)findViewById(R.id.download_over_metered_prompt); - prompt.setText(Html.fromHtml(String.format(promptFormat, language))); - final Button allowButton = (Button)findViewById(R.id.allow_button); - allowButton.setText(String.format(allowButtonFormat, ((float)size)/(1024*1024))); - } - - // This method is externally referenced from layout/download_over_metered.xml using onClick - // attribute of Button. - @ExternallyReferenced - @SuppressWarnings("unused") - public void onClickDeny(final View v) { - UpdateHandler.setDownloadOverMeteredSetting(this, false); - finish(); - } - - // This method is externally referenced from layout/download_over_metered.xml using onClick - // attribute of Button. - @ExternallyReferenced - @SuppressWarnings("unused") - public void onClickAllow(final View v) { - UpdateHandler.setDownloadOverMeteredSetting(this, true); - UpdateHandler.installIfNeverRequested(this, mClientId, mWordListToDownload); - finish(); - } -} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DownloadRecord.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DownloadRecord.java deleted file mode 100644 index 2c2a4a65..00000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DownloadRecord.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.dslul.openboard.inputmethod.dictionarypack; - -import android.content.ContentValues; - -/** - * Struct class to encapsulate a client ID with content values about a download. - */ -public class DownloadRecord { - public final String mClientId; - // Only word lists have attributes, and the ContentValues should contain the same - // keys as they do for all MetadataDbHelper functions. Since only word lists have - // attributes, a null pointer here means this record represents metadata. - public final ContentValues mAttributes; - public DownloadRecord(final String clientId, final ContentValues attributes) { - mClientId = clientId; - mAttributes = attributes; - } - public boolean isMetadata() { - return null == mAttributes; - } -} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/EventHandler.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/EventHandler.java deleted file mode 100644 index b329344a..00000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/EventHandler.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package org.dslul.openboard.inputmethod.dictionarypack; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -public final class EventHandler extends BroadcastReceiver { - /** - * Receives a intent broadcast. - * - * We receive every day a broadcast indicating that date changed. - * Then we wait a random amount of time before actually registering - * the download, to avoid concentrating too many accesses around - * midnight in more populated timezones. - * We receive all broadcasts here, so this can be either the DATE_CHANGED broadcast, the - * UPDATE_NOW private broadcast that we receive when the time-randomizing alarm triggers - * for regular update or from applications that want to test the dictionary pack, or a - * broadcast from DownloadManager telling that a download has finished. - * See inside of AndroidManifest.xml to see which events are caught. - * Also @see {@link BroadcastReceiver#onReceive(Context, Intent)} - * - * @param context the context of the application. - * @param intent the intent that was broadcast. - */ - @Override - public void onReceive(final Context context, final Intent intent) { - intent.setClass(context, DictionaryService.class); - context.startService(intent); - } -} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataDbHelper.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataDbHelper.java index e4f6e8fd..4b5df472 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataDbHelper.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataDbHelper.java @@ -211,7 +211,6 @@ public class MetadataDbHelper extends SQLiteOpenHelper { final ContentValues defaultMetadataValues = new ContentValues(); defaultMetadataValues.put(CLIENT_CLIENT_ID_COLUMN, ""); defaultMetadataValues.put(CLIENT_METADATA_URI_COLUMN, defaultMetadataUri); - defaultMetadataValues.put(CLIENT_PENDINGID_COLUMN, UpdateHandler.NOT_AN_ID); db.insert(CLIENT_TABLE_NAME, null, defaultMetadataValues); } } @@ -429,31 +428,6 @@ public class MetadataDbHelper extends SQLiteOpenHelper { } } - /** - * Get the metadata download ID for a metadata URI. - * - * This will retrieve the download ID for the metadata file that has the passed URI. - * If this URI is not being downloaded right now, it will return NOT_AN_ID. - * - * @param context a context instance to open the database on - * @param uri the URI to retrieve the metadata download ID of - * @return the download id and start date, or null if the URL is not known - */ - public static DownloadIdAndStartDate getMetadataDownloadIdAndStartDateForURI( - final Context context, final String uri) { - SQLiteDatabase defaultDb = getDb(context, null); - final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME, - new String[] { CLIENT_PENDINGID_COLUMN, CLIENT_LAST_UPDATE_DATE_COLUMN }, - CLIENT_METADATA_URI_COLUMN + " = ?", new String[] { uri }, - null, null, null, null); - try { - if (!cursor.moveToFirst()) return null; - return new DownloadIdAndStartDate(cursor.getInt(0), cursor.getLong(1)); - } finally { - cursor.close(); - } - } - public static long getOldestUpdateTime(final Context context) { SQLiteDatabase defaultDb = getDb(context, null); final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME, @@ -541,9 +515,6 @@ public class MetadataDbHelper extends SQLiteOpenHelper { if (null == result.get(FILESIZE_COLUMN)) result.put(FILESIZE_COLUMN, 0); // Smallest possible version unless specified if (null == result.get(VERSION_COLUMN)) result.put(VERSION_COLUMN, 1); - // Assume current format unless specified - if (null == result.get(FORMATVERSION_COLUMN)) - result.put(FORMATVERSION_COLUMN, UpdateHandler.MAXIMUM_SUPPORTED_FORMAT_VERSION); // No flags unless specified if (null == result.get(FLAGS_COLUMN)) result.put(FLAGS_COLUMN, 0); return result; @@ -656,48 +627,6 @@ public class MetadataDbHelper extends SQLiteOpenHelper { } } - /** - * Given a specific download ID, return records for all pending downloads across all clients. - * - * If several clients use the same metadata URL, we know to only download it once, and - * dispatch the update process across all relevant clients when the download ends. This means - * several clients may share a single download ID if they share a metadata URI. - * The dispatching is done in - * {@link UpdateHandler#downloadFinished(Context, android.content.Intent)}, which - * finds out about the list of relevant clients by calling this method. - * - * @param context a context instance to open the databases - * @param downloadId the download ID to query about - * @return the list of records. Never null, but may be empty. - */ - public static ArrayList getDownloadRecordsForDownloadId(final Context context, - final long downloadId) { - final SQLiteDatabase defaultDb = getDb(context, ""); - final ArrayList results = new ArrayList<>(); - final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME, CLIENT_TABLE_COLUMNS, - null, null, null, null, null); - try { - if (!cursor.moveToFirst()) return results; - final int clientIdIndex = cursor.getColumnIndex(CLIENT_CLIENT_ID_COLUMN); - final int pendingIdColumn = cursor.getColumnIndex(CLIENT_PENDINGID_COLUMN); - do { - final long pendingId = cursor.getInt(pendingIdColumn); - final String clientId = cursor.getString(clientIdIndex); - if (pendingId == downloadId) { - results.add(new DownloadRecord(clientId, null)); - } - final ContentValues valuesForThisClient = - getContentValuesByPendingId(getDb(context, clientId), downloadId); - if (null != valuesForThisClient) { - results.add(new DownloadRecord(clientId, valuesForThisClient)); - } - } while (cursor.moveToNext()); - } finally { - cursor.close(); - } - return results; - } - /** * Gets the info about a specific word list. * @@ -716,7 +645,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { new String[] { id, Integer.toString(version), - Integer.toString(UpdateHandler.MAXIMUM_SUPPORTED_FORMAT_VERSION) + Integer.toString(version) }, null /* groupBy */, null /* having */, @@ -893,7 +822,6 @@ public class MetadataDbHelper extends SQLiteOpenHelper { return; } // Default value for a pending ID is NOT_AN_ID - values.put(CLIENT_PENDINGID_COLUMN, UpdateHandler.NOT_AN_ID); final SQLiteDatabase defaultDb = getDb(context, ""); if (-1 == defaultDb.insert(CLIENT_TABLE_NAME, null, values)) { defaultDb.update(CLIENT_TABLE_NAME, values, @@ -911,42 +839,6 @@ public class MetadataDbHelper extends SQLiteOpenHelper { new String[] { CLIENT_CLIENT_ID_COLUMN }, null, null, null, null, null); } - /** - * Register a download ID for a specific metadata URI. - * - * This method should be called when a download for a metadata URI is starting. It will - * search for all clients using this metadata URI and will register for each of them - * the download ID into the database for later retrieval by - * {@link #getDownloadRecordsForDownloadId(Context, long)}. - * - * @param context a context for opening databases - * @param uri the metadata URI - * @param downloadId the download ID - */ - public static void registerMetadataDownloadId(final Context context, final String uri, - final long downloadId) { - final ContentValues values = new ContentValues(); - values.put(CLIENT_PENDINGID_COLUMN, downloadId); - values.put(CLIENT_LAST_UPDATE_DATE_COLUMN, System.currentTimeMillis()); - final SQLiteDatabase defaultDb = getDb(context, ""); - final Cursor cursor = MetadataDbHelper.queryClientIds(context); - if (null == cursor) return; - try { - if (!cursor.moveToFirst()) return; - do { - final String clientId = cursor.getString(0); - final String metadataUri = - MetadataDbHelper.getMetadataUriAsString(context, clientId); - if (metadataUri.equals(uri)) { - defaultDb.update(CLIENT_TABLE_NAME, values, - CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId }); - } - } while (cursor.moveToNext()); - } finally { - cursor.close(); - } - } - /** * Marks a downloading entry as having successfully downloaded and being installed. * diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataHandler.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataHandler.java index 1c3f9b30..2b153fdb 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataHandler.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataHandler.java @@ -161,7 +161,6 @@ public class MetadataHandler { int bestFormatVersion = Integer.MIN_VALUE; // To be sure we can't be inadvertently smaller for (WordListMetadata wordList : metadata) { if (id.equals(wordList.mId) - && wordList.mFormatVersion <= UpdateHandler.MAXIMUM_SUPPORTED_FORMAT_VERSION && wordList.mFormatVersion > bestFormatVersion) { bestWordList = wordList; bestFormatVersion = wordList.mFormatVersion; diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/PrivateLog.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/PrivateLog.java index 3d9be650..80d31feb 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/PrivateLog.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/PrivateLog.java @@ -30,7 +30,7 @@ import java.util.Locale; */ public class PrivateLog { - public static final boolean DEBUG = DictionaryProvider.DEBUG; + public static final boolean DEBUG = false; private static final String LOG_DATABASE_NAME = "log"; private static final String LOG_TABLE_NAME = "log"; diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/UpdateHandler.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/UpdateHandler.java deleted file mode 100644 index dd22784f..00000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/UpdateHandler.java +++ /dev/null @@ -1,1137 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package org.dslul.openboard.inputmethod.dictionarypack; - -import android.app.DownloadManager; -import android.app.DownloadManager.Query; -import android.app.DownloadManager.Request; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.net.ConnectivityManager; -import android.net.Uri; -import android.os.ParcelFileDescriptor; -import android.provider.Settings; -import android.text.TextUtils; -import android.util.Log; - -import org.dslul.openboard.inputmethod.compat.ConnectivityManagerCompatUtils; -import org.dslul.openboard.inputmethod.compat.NotificationCompatUtils; -import org.dslul.openboard.inputmethod.latin.R; -import org.dslul.openboard.inputmethod.latin.common.LocaleUtils; -import org.dslul.openboard.inputmethod.latin.makedict.FormatSpec; -import org.dslul.openboard.inputmethod.latin.utils.ApplicationUtils; -import org.dslul.openboard.inputmethod.latin.utils.DebugLogUtils; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.nio.channels.FileChannel; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; -import java.util.TreeSet; - -import javax.annotation.Nullable; - -/** - * Handler for the update process. - * - * This class is in charge of coordinating the update process for the various dictionaries - * stored in the dictionary pack. - */ -public final class UpdateHandler { - static final String TAG = "DictionaryProvider:" + UpdateHandler.class.getSimpleName(); - private static final boolean DEBUG = DictionaryProvider.DEBUG; - - // Used to prevent trying to read the id of the downloaded file before it is written - static final Object sSharedIdProtector = new Object(); - - // Value used to mean this is not a real DownloadManager downloaded file id - // DownloadManager uses as an ID numbers returned out of an AUTOINCREMENT column - // in SQLite, so it should never return anything < 0. - public static final int NOT_AN_ID = -1; - public static final int MAXIMUM_SUPPORTED_FORMAT_VERSION = - FormatSpec.MAXIMUM_SUPPORTED_STATIC_VERSION; - - // Arbitrary. Probably good if it's a power of 2, and a couple thousand bytes long. - private static final int FILE_COPY_BUFFER_SIZE = 8192; - - // Table fixed values for metadata / downloads - final static String METADATA_NAME = "metadata"; - final static int METADATA_TYPE = 0; - final static int WORDLIST_TYPE = 1; - - // Suffix for generated dictionary files - private static final String DICT_FILE_SUFFIX = ".dict"; - // Name of the category for the main dictionary - public static final String MAIN_DICTIONARY_CATEGORY = "main"; - - public static final String TEMP_DICT_FILE_SUB = "___"; - - // The id for the "dictionary available" notification. - static final int DICT_AVAILABLE_NOTIFICATION_ID = 1; - - /** - * An interface for UIs or services that want to know when something happened. - * - * This is chiefly used by the dictionary manager UI. - */ - public interface UpdateEventListener { - void downloadedMetadata(boolean succeeded); - void wordListDownloadFinished(String wordListId, boolean succeeded); - void updateCycleCompleted(); - } - - /** - * The list of currently registered listeners. - */ - private static List sUpdateEventListeners - = Collections.synchronizedList(new LinkedList()); - - /** - * Register a new listener to be notified of updates. - * - * Don't forget to call unregisterUpdateEventListener when done with it, or - * it will leak the register. - */ - public static void registerUpdateEventListener(final UpdateEventListener listener) { - sUpdateEventListeners.add(listener); - } - - /** - * Unregister a previously registered listener. - */ - public static void unregisterUpdateEventListener(final UpdateEventListener listener) { - sUpdateEventListeners.remove(listener); - } - - private static final String DOWNLOAD_OVER_METERED_SETTING_PREFS_KEY = "downloadOverMetered"; - - /** - * Write the DownloadManager ID of the currently downloading metadata to permanent storage. - * - * @param context to open shared prefs - * @param uri the uri of the metadata - * @param downloadId the id returned by DownloadManager - */ - private static void writeMetadataDownloadId(final Context context, final String uri, - final long downloadId) { - MetadataDbHelper.registerMetadataDownloadId(context, uri, downloadId); - } - - public static final int DOWNLOAD_OVER_METERED_SETTING_UNKNOWN = 0; - public static final int DOWNLOAD_OVER_METERED_ALLOWED = 1; - public static final int DOWNLOAD_OVER_METERED_DISALLOWED = 2; - - /** - * Sets the setting that tells us whether we may download over a metered connection. - */ - public static void setDownloadOverMeteredSetting(final Context context, - final boolean shouldDownloadOverMetered) { - final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context); - final SharedPreferences.Editor editor = prefs.edit(); - editor.putInt(DOWNLOAD_OVER_METERED_SETTING_PREFS_KEY, shouldDownloadOverMetered - ? DOWNLOAD_OVER_METERED_ALLOWED : DOWNLOAD_OVER_METERED_DISALLOWED); - editor.apply(); - } - - /** - * Gets the setting that tells us whether we may download over a metered connection. - * - * This returns one of the constants above. - */ - public static int getDownloadOverMeteredSetting(final Context context) { - final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context); - final int setting = prefs.getInt(DOWNLOAD_OVER_METERED_SETTING_PREFS_KEY, - DOWNLOAD_OVER_METERED_SETTING_UNKNOWN); - return setting; - } - - /** - * Download latest metadata from the server through DownloadManager for all known clients - * @param context The context for retrieving resources - * @return true if an update successfully started, false otherwise. - */ - public static boolean tryUpdate(final Context context) { - // TODO: loop through all clients instead of only doing the default one. - final TreeSet uris = new TreeSet<>(); - final Cursor cursor = MetadataDbHelper.queryClientIds(context); - if (null == cursor) return false; - try { - if (!cursor.moveToFirst()) return false; - do { - final String clientId = cursor.getString(0); - final String metadataUri = - MetadataDbHelper.getMetadataUriAsString(context, clientId); - PrivateLog.log("Update for clientId " + DebugLogUtils.s(clientId)); - DebugLogUtils.l("Update for clientId", clientId, " which uses URI ", metadataUri); - uris.add(metadataUri); - } while (cursor.moveToNext()); - } finally { - cursor.close(); - } - boolean started = false; - for (final String metadataUri : uris) { - if (!TextUtils.isEmpty(metadataUri)) { - // If the metadata URI is empty, that means we should never update it at all. - // It should not be possible to come here with a null metadata URI, because - // it should have been rejected at the time of client registration; if there - // is a bug and it happens anyway, doing nothing is the right thing to do. - // For more information, {@see DictionaryProvider#insert(Uri, ContentValues)}. - updateClientsWithMetadataUri(context, metadataUri); - started = true; - } - } - return started; - } - - /** - * Download latest metadata from the server through DownloadManager for all relevant clients - * - * @param context The context for retrieving resources - * @param metadataUri The client to update - */ - private static void updateClientsWithMetadataUri( - final Context context, final String metadataUri) { - Log.i(TAG, "updateClientsWithMetadataUri() : MetadataUri = " + metadataUri); - // Adding a disambiguator to circumvent a bug in older versions of DownloadManager. - // DownloadManager also stupidly cuts the extension to replace with its own that it - // gets from the content-type. We need to circumvent this. - final String disambiguator = "#" + System.currentTimeMillis() - + ApplicationUtils.getVersionName(context) + ".json"; - final Request metadataRequest = new Request(Uri.parse(metadataUri + disambiguator)); - DebugLogUtils.l("Request =", metadataRequest); - - final Resources res = context.getResources(); - metadataRequest.setAllowedNetworkTypes(Request.NETWORK_WIFI | Request.NETWORK_MOBILE); - metadataRequest.setTitle(res.getString(R.string.download_description)); - // Do not show the notification when downloading the metadata. - metadataRequest.setNotificationVisibility(Request.VISIBILITY_HIDDEN); - metadataRequest.setVisibleInDownloadsUi( - res.getBoolean(R.bool.metadata_downloads_visible_in_download_UI)); - - final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); - if (maybeCancelUpdateAndReturnIfStillRunning(context, metadataUri, manager, - DictionaryService.NO_CANCEL_DOWNLOAD_PERIOD_MILLIS)) { - // We already have a recent download in progress. Don't register a new download. - return; - } - final long downloadId; - synchronized (sSharedIdProtector) { - downloadId = manager.enqueue(metadataRequest); - DebugLogUtils.l("Metadata download requested with id", downloadId); - // If there is still a download in progress, it's been there for a while and - // there is probably something wrong with download manager. It's best to just - // overwrite the id and request it again. If the old one happens to finish - // anyway, we don't know about its ID any more, so the downloadFinished - // method will ignore it. - writeMetadataDownloadId(context, metadataUri, downloadId); - } - Log.i(TAG, "updateClientsWithMetadataUri() : DownloadId = " + downloadId); - } - - /** - * Cancels downloading a file if there is one for this URI and it's too long. - * - * If we are not currently downloading the file at this URI, this is a no-op. - * - * @param context the context to open the database on - * @param metadataUri the URI to cancel - * @param manager an wrapped instance of DownloadManager - * @param graceTime if there was a download started less than this many milliseconds, don't - * cancel and return true - * @return whether the download is still active - */ - private static boolean maybeCancelUpdateAndReturnIfStillRunning(final Context context, - final String metadataUri, final DownloadManagerWrapper manager, final long graceTime) { - synchronized (sSharedIdProtector) { - final DownloadIdAndStartDate metadataDownloadIdAndStartDate = - MetadataDbHelper.getMetadataDownloadIdAndStartDateForURI(context, metadataUri); - if (null == metadataDownloadIdAndStartDate) return false; - if (NOT_AN_ID == metadataDownloadIdAndStartDate.mId) return false; - if (metadataDownloadIdAndStartDate.mStartDate + graceTime - > System.currentTimeMillis()) { - return true; - } - manager.remove(metadataDownloadIdAndStartDate.mId); - writeMetadataDownloadId(context, metadataUri, NOT_AN_ID); - } - // Consider a cancellation as a failure. As such, inform listeners that the download - // has failed. - for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) { - listener.downloadedMetadata(false); - } - return false; - } - - /** - * Cancels a pending update for this client, if there is one. - * - * If we are not currently updating metadata for this client, this is a no-op. This is a helper - * method that gets the download manager service and the metadata URI for this client. - * - * @param context the context, to get an instance of DownloadManager - * @param clientId the ID of the client we want to cancel the update of - */ - public static void cancelUpdate(final Context context, final String clientId) { - final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); - final String metadataUri = MetadataDbHelper.getMetadataUriAsString(context, clientId); - maybeCancelUpdateAndReturnIfStillRunning(context, metadataUri, manager, 0 /* graceTime */); - } - - /** - * Registers a download request and flags it as downloading in the metadata table. - * - * This is a helper method that exists to avoid race conditions where DownloadManager might - * finish downloading the file before the data is committed to the database. - * It registers the request with the DownloadManager service and also updates the metadata - * database directly within a synchronized section. - * This method has no intelligence about the data it commits to the database aside from the - * download request id, which is not known before submitting the request to the download - * manager. Hence, it only updates the relevant line. - * - * @param manager a wrapped download manager service to register the request with. - * @param request the request to register. - * @param db the metadata database. - * @param id the id of the word list. - * @param version the version of the word list. - * @return the download id returned by the download manager. - */ - public static long registerDownloadRequest(final DownloadManagerWrapper manager, - final Request request, final SQLiteDatabase db, final String id, final int version) { - Log.i(TAG, "registerDownloadRequest() : Id = " + id + " : Version = " + version); - final long downloadId; - synchronized (sSharedIdProtector) { - downloadId = manager.enqueue(request); - Log.i(TAG, "registerDownloadRequest() : DownloadId = " + downloadId); - MetadataDbHelper.markEntryAsDownloading(db, id, version, downloadId); - } - return downloadId; - } - - /** - * Retrieve information about a specific download from DownloadManager. - */ - private static CompletedDownloadInfo getCompletedDownloadInfo( - final DownloadManagerWrapper manager, final long downloadId) { - final Query query = new Query().setFilterById(downloadId); - final Cursor cursor = manager.query(query); - - if (null == cursor) { - return new CompletedDownloadInfo(null, downloadId, DownloadManager.STATUS_FAILED); - } - try { - final String uri; - final int status; - if (cursor.moveToNext()) { - final int columnStatus = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS); - final int columnError = cursor.getColumnIndex(DownloadManager.COLUMN_REASON); - final int columnUri = cursor.getColumnIndex(DownloadManager.COLUMN_URI); - final int error = cursor.getInt(columnError); - status = cursor.getInt(columnStatus); - final String uriWithAnchor = cursor.getString(columnUri); - int anchorIndex = uriWithAnchor.indexOf('#'); - if (anchorIndex != -1) { - uri = uriWithAnchor.substring(0, anchorIndex); - } else { - uri = uriWithAnchor; - } - if (DownloadManager.STATUS_SUCCESSFUL != status) { - Log.e(TAG, "Permanent failure of download " + downloadId - + " with error code: " + error); - } - } else { - uri = null; - status = DownloadManager.STATUS_FAILED; - } - return new CompletedDownloadInfo(uri, downloadId, status); - } finally { - cursor.close(); - } - } - - private static ArrayList getDownloadRecordsForCompletedDownloadInfo( - final Context context, final CompletedDownloadInfo downloadInfo) { - // Get and check the ID of the file we are waiting for, compare them to downloaded ones - synchronized(sSharedIdProtector) { - final ArrayList downloadRecords = - MetadataDbHelper.getDownloadRecordsForDownloadId(context, - downloadInfo.mDownloadId); - // If any of these is metadata, we should update the DB - boolean hasMetadata = false; - for (DownloadRecord record : downloadRecords) { - if (record.isMetadata()) { - hasMetadata = true; - break; - } - } - if (hasMetadata) { - writeMetadataDownloadId(context, downloadInfo.mUri, NOT_AN_ID); - MetadataDbHelper.saveLastUpdateTimeOfUri(context, downloadInfo.mUri); - } - return downloadRecords; - } - } - - /** - * Take appropriate action after a download finished, in success or in error. - * - * This is called by the system upon broadcast from the DownloadManager that a file - * has been downloaded successfully. - * After a simple check that this is actually the file we are waiting for, this - * method basically coordinates the parsing and comparison of metadata, and fires - * the computation of the list of actions that should be taken then executes them. - * - * @param context The context for this action. - * @param intent The intent from the DownloadManager containing details about the download. - */ - /* package */ static void downloadFinished(final Context context, final Intent intent) { - // Get and check the ID of the file that was downloaded - final long fileId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, NOT_AN_ID); - Log.i(TAG, "downloadFinished() : DownloadId = " + fileId); - if (NOT_AN_ID == fileId) return; // Spurious wake-up: ignore - - final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); - final CompletedDownloadInfo downloadInfo = getCompletedDownloadInfo(manager, fileId); - - final ArrayList recordList = - getDownloadRecordsForCompletedDownloadInfo(context, downloadInfo); - if (null == recordList) return; // It was someone else's download. - DebugLogUtils.l("Received result for download ", fileId); - - // TODO: handle gracefully a null pointer here. This is practically impossible because - // we come here only when DownloadManager explicitly called us when it ended a - // download, so we are pretty sure it's alive. It's theoretically possible that it's - // disabled right inbetween the firing of the intent and the control reaching here. - - for (final DownloadRecord record : recordList) { - // downloadSuccessful is not final because we may still have exceptions from now on - boolean downloadSuccessful = false; - try { - if (downloadInfo.wasSuccessful()) { - downloadSuccessful = handleDownloadedFile(context, record, manager, fileId); - Log.i(TAG, "downloadFinished() : Success = " + downloadSuccessful); - } - } finally { - final String resultMessage = downloadSuccessful ? "Success" : "Failure"; - if (record.isMetadata()) { - Log.i(TAG, "downloadFinished() : Metadata " + resultMessage); - publishUpdateMetadataCompleted(context, downloadSuccessful); - } else { - Log.i(TAG, "downloadFinished() : WordList " + resultMessage); - final SQLiteDatabase db = MetadataDbHelper.getDb(context, record.mClientId); - publishUpdateWordListCompleted(context, downloadSuccessful, fileId, - db, record.mAttributes, record.mClientId); - } - } - } - // Now that we're done using it, we can remove this download from DLManager - manager.remove(fileId); - } - - /** - * Sends a broadcast informing listeners that the dictionaries were updated. - * - * This will call all local listeners through the UpdateEventListener#downloadedMetadata - * callback (for example, the dictionary provider interface uses this to stop the Loading - * animation) and send a broadcast about the metadata having been updated. For a client of - * the dictionary pack like Latin IME, this means it should re-query the dictionary pack - * for any relevant new data. - * - * @param context the context, to send the broadcast. - * @param downloadSuccessful whether the download of the metadata was successful or not. - */ - public static void publishUpdateMetadataCompleted(final Context context, - final boolean downloadSuccessful) { - // We need to warn all listeners of what happened. But some listeners may want to - // remove themselves or re-register something in response. Hence we should take a - // snapshot of the listener list and warn them all. This also prevents any - // concurrent modification problem of the static list. - for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) { - listener.downloadedMetadata(downloadSuccessful); - } - publishUpdateCycleCompletedEvent(context); - } - - private static void publishUpdateWordListCompleted(final Context context, - final boolean downloadSuccessful, final long fileId, - final SQLiteDatabase db, final ContentValues downloadedFileRecord, - final String clientId) { - synchronized(sSharedIdProtector) { - if (downloadSuccessful) { - final ActionBatch actions = new ActionBatch(); - actions.add(new ActionBatch.InstallAfterDownloadAction(clientId, - downloadedFileRecord)); - actions.execute(context, new LogProblemReporter(TAG)); - } else { - MetadataDbHelper.deleteDownloadingEntry(db, fileId); - } - } - // See comment above about #linkedCopyOfLists - for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) { - listener.wordListDownloadFinished(downloadedFileRecord.getAsString( - MetadataDbHelper.WORDLISTID_COLUMN), downloadSuccessful); - } - publishUpdateCycleCompletedEvent(context); - } - - private static void publishUpdateCycleCompletedEvent(final Context context) { - // Even if this is not successful, we have to publish the new state. - PrivateLog.log("Publishing update cycle completed event"); - DebugLogUtils.l("Publishing update cycle completed event"); - for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) { - listener.updateCycleCompleted(); - } - signalNewDictionaryState(context); - } - - private static boolean handleDownloadedFile(final Context context, - final DownloadRecord downloadRecord, final DownloadManagerWrapper manager, - final long fileId) { - try { - // {@link handleWordList(Context,InputStream,ContentValues)}. - // Handle the downloaded file according to its type - if (downloadRecord.isMetadata()) { - DebugLogUtils.l("Data D/L'd is metadata for", downloadRecord.mClientId); - // #handleMetadata() closes its InputStream argument - handleMetadata(context, new ParcelFileDescriptor.AutoCloseInputStream( - manager.openDownloadedFile(fileId)), downloadRecord.mClientId); - } else { - DebugLogUtils.l("Data D/L'd is a word list"); - final int wordListStatus = downloadRecord.mAttributes.getAsInteger( - MetadataDbHelper.STATUS_COLUMN); - if (MetadataDbHelper.STATUS_DOWNLOADING == wordListStatus) { - // #handleWordList() closes its InputStream argument - handleWordList(context, new ParcelFileDescriptor.AutoCloseInputStream( - manager.openDownloadedFile(fileId)), downloadRecord); - } else { - Log.e(TAG, "Spurious download ended. Maybe a cancelled download?"); - } - } - return true; - } catch (FileNotFoundException e) { - Log.e(TAG, "A file was downloaded but it can't be opened", e); - } catch (IOException e) { - // Can't read the file... disk damage? - Log.e(TAG, "Can't read a file", e); - // TODO: Check with UX how we should warn the user. - } catch (IllegalStateException e) { - // The format of the downloaded file is incorrect. We should maybe report upstream? - Log.e(TAG, "Incorrect data received", e); - } catch (BadFormatException e) { - // The format of the downloaded file is incorrect. We should maybe report upstream? - Log.e(TAG, "Incorrect data received", e); - } - return false; - } - - /** - * Returns a copy of the specified list, with all elements copied. - * - * This returns a linked list. - */ - private static List linkedCopyOfList(final List src) { - // Instantiation of a parameterized type is not possible in Java, so it's not possible to - // return the same type of list that was passed - probably the same reason why Collections - // does not do it. So we need to decide statically which concrete type to return. - return new LinkedList<>(src); - } - - /** - * Warn Android Keyboard that the state of dictionaries changed and it should refresh its data. - */ - private static void signalNewDictionaryState(final Context context) { - // TODO: Also provide the locale of the updated dictionary so that the LatinIme - // does not have to reset if it is a different locale. - final Intent newDictBroadcast = - new Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION); - context.sendBroadcast(newDictBroadcast); - } - - /** - * Parse metadata and take appropriate action (that is, upgrade dictionaries). - * @param context the context to read settings. - * @param stream an input stream pointing to the downloaded data. May not be null. - * Will be closed upon finishing. - * @param clientId the ID of the client to update - * @throws BadFormatException if the metadata is not in a known format. - * @throws IOException if the downloaded file can't be read from the disk - */ - public static void handleMetadata(final Context context, final InputStream stream, - final String clientId) throws IOException, BadFormatException { - DebugLogUtils.l("Entering handleMetadata"); - final List newMetadata; - final InputStreamReader reader = new InputStreamReader(stream); - try { - // According to the doc InputStreamReader buffers, so no need to add a buffering layer - newMetadata = MetadataHandler.readMetadata(reader); - } finally { - reader.close(); - } - - DebugLogUtils.l("Downloaded metadata :", newMetadata); - PrivateLog.log("Downloaded metadata\n" + newMetadata); - - final ActionBatch actions = computeUpgradeTo(context, clientId, newMetadata); - // TODO: Check with UX how we should report to the user - // TODO: add an action to close the database - actions.execute(context, new LogProblemReporter(TAG)); - } - - /** - * Handle a word list: put it in its right place, and update the passed content values. - * @param context the context for opening files. - * @param inputStream an input stream pointing to the downloaded data. May not be null. - * Will be closed upon finishing. - * @param downloadRecord the content values to fill the file name in. - * @throws IOException if files can't be read or written. - * @throws BadFormatException if the md5 checksum doesn't match the metadata. - */ - private static void handleWordList(final Context context, - final InputStream inputStream, final DownloadRecord downloadRecord) - throws IOException, BadFormatException { - - // DownloadManager does not have the ability to put the file directly where we want - // it, so we had it download to a temporary place. Now we move it. It will be deleted - // automatically by DownloadManager. - DebugLogUtils.l("Downloaded a new word list :", downloadRecord.mAttributes.getAsString( - MetadataDbHelper.DESCRIPTION_COLUMN), "for", downloadRecord.mClientId); - PrivateLog.log("Downloaded a new word list with description : " - + downloadRecord.mAttributes.getAsString(MetadataDbHelper.DESCRIPTION_COLUMN) - + " for " + downloadRecord.mClientId); - - final String locale = - downloadRecord.mAttributes.getAsString(MetadataDbHelper.LOCALE_COLUMN); - final String destinationFile = getTempFileName(context, locale); - downloadRecord.mAttributes.put(MetadataDbHelper.LOCAL_FILENAME_COLUMN, destinationFile); - - FileOutputStream outputStream = null; - try { - outputStream = context.openFileOutput(destinationFile, Context.MODE_PRIVATE); - copyFile(inputStream, outputStream); - } finally { - inputStream.close(); - if (outputStream != null) { - outputStream.close(); - } - } - - // TODO: Consolidate this MD5 calculation with file copying above. - // We need to reopen the file because the inputstream bytes have been consumed, and there - // is nothing in InputStream to reopen or rewind the stream - FileInputStream copiedFile = null; - final String md5sum; - try { - copiedFile = context.openFileInput(destinationFile); - md5sum = MD5Calculator.checksum(copiedFile); - } finally { - if (copiedFile != null) { - copiedFile.close(); - } - } - if (TextUtils.isEmpty(md5sum)) { - return; // We can't compute the checksum anyway, so return and hope for the best - } - if (!md5sum.equals(downloadRecord.mAttributes.getAsString( - MetadataDbHelper.CHECKSUM_COLUMN))) { - context.deleteFile(destinationFile); - throw new BadFormatException("MD5 checksum check failed : \"" + md5sum + "\" <> \"" - + downloadRecord.mAttributes.getAsString(MetadataDbHelper.CHECKSUM_COLUMN) - + "\""); - } - } - - /** - * Copies in to out using FileChannels. - * - * This tries to use channels for fast copying. If it doesn't work, fall back to - * copyFileFallBack below. - * - * @param in the stream to copy from. - * @param out the stream to copy to. - * @throws IOException if both the normal and fallback methods raise exceptions. - */ - private static void copyFile(final InputStream in, final OutputStream out) - throws IOException { - DebugLogUtils.l("Copying files"); - if (!(in instanceof FileInputStream) || !(out instanceof FileOutputStream)) { - DebugLogUtils.l("Not the right types"); - copyFileFallback(in, out); - } else { - try { - final FileChannel sourceChannel = ((FileInputStream) in).getChannel(); - final FileChannel destinationChannel = ((FileOutputStream) out).getChannel(); - sourceChannel.transferTo(0, Integer.MAX_VALUE, destinationChannel); - } catch (IOException e) { - // Can't work with channels, or something went wrong. Copy by hand. - DebugLogUtils.l("Won't work"); - copyFileFallback(in, out); - } - } - } - - /** - * Copies in to out with read/write methods, not FileChannels. - * - * @param in the stream to copy from. - * @param out the stream to copy to. - * @throws IOException if a read or a write fails. - */ - private static void copyFileFallback(final InputStream in, final OutputStream out) - throws IOException { - DebugLogUtils.l("Falling back to slow copy"); - final byte[] buffer = new byte[FILE_COPY_BUFFER_SIZE]; - for (int readBytes = in.read(buffer); readBytes >= 0; readBytes = in.read(buffer)) - out.write(buffer, 0, readBytes); - } - - /** - * Creates and returns a new file to store a dictionary - * @param context the context to use to open the file. - * @param locale the locale for this dictionary, to make the file name more readable. - * @return the file name, or throw an exception. - * @throws IOException if the file cannot be created. - */ - private static String getTempFileName(final Context context, final String locale) - throws IOException { - DebugLogUtils.l("Entering openTempFileOutput"); - final File dir = context.getFilesDir(); - final File f = File.createTempFile(locale + TEMP_DICT_FILE_SUB, DICT_FILE_SUFFIX, dir); - DebugLogUtils.l("File name is", f.getName()); - return f.getName(); - } - - /** - * Compare metadata (collections of word lists). - * - * This method takes whole metadata sets directly and compares them, matching the wordlists in - * each of them on the id. It creates an ActionBatch object that can be .execute()'d to perform - * the actual upgrade from `from' to `to'. - * - * @param context the context to open databases on. - * @param clientId the id of the client. - * @param from the dictionary descriptor (as a list of wordlists) to upgrade from. - * @param to the dictionary descriptor (as a list of wordlists) to upgrade to. - * @return an ordered list of runnables to be called to upgrade. - */ - private static ActionBatch compareMetadataForUpgrade(final Context context, - final String clientId, @Nullable final List from, - @Nullable final List to) { - final ActionBatch actions = new ActionBatch(); - // Upgrade existing word lists - DebugLogUtils.l("Comparing dictionaries"); - final Set wordListIds = new TreeSet<>(); - // TODO: Can these be null? - final List fromList = (from == null) ? new ArrayList() - : from; - final List toList = (to == null) ? new ArrayList() - : to; - for (WordListMetadata wlData : fromList) wordListIds.add(wlData.mId); - for (WordListMetadata wlData : toList) wordListIds.add(wlData.mId); - for (String id : wordListIds) { - final WordListMetadata currentInfo = MetadataHandler.findWordListById(fromList, id); - final WordListMetadata metadataInfo = MetadataHandler.findWordListById(toList, id); - // TODO: Remove the following unnecessary check, since we are now doing the filtering - // inside findWordListById. - final WordListMetadata newInfo = null == metadataInfo - || metadataInfo.mFormatVersion > MAXIMUM_SUPPORTED_FORMAT_VERSION - ? null : metadataInfo; - DebugLogUtils.l("Considering updating ", id, "currentInfo =", currentInfo); - - if (null == currentInfo && null == newInfo) { - // This may happen if a new word list appeared that we can't handle. - if (null == metadataInfo) { - // What happened? Bug in Set<>? - Log.e(TAG, "Got an id for a wordlist that is neither in from nor in to"); - } else { - // We may come here if there is a new word list that we can't handle. - Log.i(TAG, "Can't handle word list with id '" + id + "' because it has format" - + " version " + metadataInfo.mFormatVersion + " and the maximum version" - + " we can handle is " + MAXIMUM_SUPPORTED_FORMAT_VERSION); - } - continue; - } else if (null == currentInfo) { - // This is the case where a new list that we did not know of popped on the server. - // Make it available. - actions.add(new ActionBatch.MakeAvailableAction(clientId, newInfo)); - } else if (null == newInfo) { - // This is the case where an old list we had is not in the server data any more. - // Pass false to ForgetAction: this may be installed and we still want to apply - // a forget-like action (remove the URL) if it is, so we want to turn off the - // status == AVAILABLE check. If it's DELETING, this is the right thing to do, - // as we want to leave the record as long as Android Keyboard has not deleted it ; - // the record will be removed when the file is actually deleted. - actions.add(new ActionBatch.ForgetAction(clientId, currentInfo, false)); - } else { - final SQLiteDatabase db = MetadataDbHelper.getDb(context, clientId); - if (newInfo.mVersion == currentInfo.mVersion) { - if (TextUtils.equals(newInfo.mRemoteFilename, currentInfo.mRemoteFilename)) { - // If the dictionary url hasn't changed, we should preserve the retryCount. - newInfo.mRetryCount = currentInfo.mRetryCount; - } - // If it's the same id/version, we update the DB with the new values. - // It doesn't matter too much if they didn't change. - actions.add(new ActionBatch.UpdateDataAction(clientId, newInfo)); - } else if (newInfo.mVersion > currentInfo.mVersion) { - // If it's a new version, it's a different entry in the database. Make it - // available, and if it's installed, also start the download. - final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db, - currentInfo.mId, currentInfo.mVersion); - final int status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN); - actions.add(new ActionBatch.MakeAvailableAction(clientId, newInfo)); - if (status == MetadataDbHelper.STATUS_INSTALLED - || status == MetadataDbHelper.STATUS_DISABLED) { - actions.add(new ActionBatch.StartDownloadAction(clientId, newInfo)); - } else { - // Pass true to ForgetAction: this is indeed an update to a non-installed - // word list, so activate status == AVAILABLE check - // In case the status is DELETING, this is the right thing to do. It will - // leave the entry as DELETING and remove its URL so that Android Keyboard - // can delete it the next time it starts up. - actions.add(new ActionBatch.ForgetAction(clientId, currentInfo, true)); - } - } else if (DEBUG) { - Log.i(TAG, "Not updating word list " + id - + " : current list timestamp is " + currentInfo.mLastUpdate - + " ; new list timestamp is " + newInfo.mLastUpdate); - } - } - } - return actions; - } - - /** - * Computes an upgrade from the current state of the dictionaries to some desired state. - * @param context the context for reading settings and files. - * @param clientId the id of the client. - * @param newMetadata the state we want to upgrade to. - * @return the upgrade from the current state to the desired state, ready to be executed. - */ - public static ActionBatch computeUpgradeTo(final Context context, final String clientId, - final List newMetadata) { - final List currentMetadata = - MetadataHandler.getCurrentMetadata(context, clientId); - return compareMetadataForUpgrade(context, clientId, currentMetadata, newMetadata); - } - - /** - * Shows the notification that informs the user a dictionary is available. - * - * When this notification is clicked, the dialog for downloading the dictionary - * over a metered connection is shown. - */ - private static void showDictionaryAvailableNotification(final Context context, - final String clientId, final ContentValues installCandidate) { - final String localeString = installCandidate.getAsString(MetadataDbHelper.LOCALE_COLUMN); - final Intent intent = new Intent(); - intent.setClass(context, DownloadOverMeteredDialog.class); - intent.putExtra(DownloadOverMeteredDialog.CLIENT_ID_KEY, clientId); - intent.putExtra(DownloadOverMeteredDialog.WORDLIST_TO_DOWNLOAD_KEY, - installCandidate.getAsString(MetadataDbHelper.WORDLISTID_COLUMN)); - intent.putExtra(DownloadOverMeteredDialog.SIZE_KEY, - installCandidate.getAsInteger(MetadataDbHelper.FILESIZE_COLUMN)); - intent.putExtra(DownloadOverMeteredDialog.LOCALE_KEY, localeString); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - final PendingIntent notificationIntent = PendingIntent.getActivity(context, - 0 /* requestCode */, intent, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT); - final NotificationManager notificationManager = - (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - // None of those are expected to happen, but just in case... - if (null == notificationIntent || null == notificationManager) return; - - final String language = (null == localeString) ? "" - : LocaleUtils.constructLocaleFromString(localeString).getDisplayLanguage(); - final String titleFormat = context.getString(R.string.dict_available_notification_title); - final String notificationTitle = String.format(titleFormat, language); - final Notification.Builder builder = new Notification.Builder(context) - .setAutoCancel(true) - .setContentIntent(notificationIntent) - .setContentTitle(notificationTitle) - .setContentText(context.getString(R.string.dict_available_notification_description)) - .setTicker(notificationTitle) - .setOngoing(false) - .setOnlyAlertOnce(true) - .setSmallIcon(R.drawable.ic_notify_dictionary); - NotificationCompatUtils.setColor(builder, - context.getResources().getColor(R.color.notification_accent_color)); - NotificationCompatUtils.setPriorityToLow(builder); - NotificationCompatUtils.setVisibilityToSecret(builder); - NotificationCompatUtils.setCategoryToRecommendation(builder); - final Notification notification = NotificationCompatUtils.build(builder); - notificationManager.notify(DICT_AVAILABLE_NOTIFICATION_ID, notification); - } - - /** - * Installs a word list if it has never been requested. - * - * This is called when a word list is requested, and is available but not installed. It checks - * the conditions for auto-installation: if the dictionary is a main dictionary for this - * language, and it has never been opted out through the dictionary interface, then we start - * installing it. For the user who enables a language and uses it for the first time, the - * dictionary should magically start being used a short time after they start typing. - * The mayPrompt argument indicates whether we should prompt the user for a decision to - * download or not, in case we decide we are in the case where we should download - this - * roughly happens when the current connectivity is 3G. See - * DictionaryProvider#getDictionaryWordListsForContentUri for details. - */ - // As opposed to many other methods, this method does not need the version of the word - // list because it may only install the latest version we know about for this specific - // word list ID / client ID combination. - public static void installIfNeverRequested(final Context context, final String clientId, - final String wordlistId) { - Log.i(TAG, "installIfNeverRequested() : ClientId = " + clientId - + " : WordListId = " + wordlistId); - final String[] idArray = wordlistId.split(DictionaryProvider.ID_CATEGORY_SEPARATOR); - // If we have a new-format dictionary id (category:manual_id), then use the - // specified category. Otherwise, it is a main dictionary, so force the - // MAIN category upon it. - final String category = 2 == idArray.length ? idArray[0] : MAIN_DICTIONARY_CATEGORY; - if (!MAIN_DICTIONARY_CATEGORY.equals(category)) { - // Not a main dictionary. We only auto-install main dictionaries, so we can return now. - return; - } - if (CommonPreferences.getCommonPreferences(context).contains(wordlistId)) { - // If some kind of settings has been done in the past for this specific id, then - // this is not a candidate for auto-install. Because it already is either true, - // in which case it may be installed or downloading or whatever, and we don't - // need to care about it because it's already handled or being handled, or it's false - // in which case it means the user explicitely turned it off and don't want to have - // it installed. So we quit right away. - return; - } - - final SQLiteDatabase db = MetadataDbHelper.getDb(context, clientId); - final ContentValues installCandidate = - MetadataDbHelper.getContentValuesOfLatestAvailableWordlistById(db, wordlistId); - if (MetadataDbHelper.STATUS_AVAILABLE - != installCandidate.getAsInteger(MetadataDbHelper.STATUS_COLUMN)) { - // If it's not "AVAILABLE", we want to stop now. Because candidates for auto-install - // are lists that we know are available, but we also know have never been installed. - // It does obviously not concern already installed lists, or downloading lists, - // or those that have been disabled, flagged as deleting... So anything else than - // AVAILABLE means we don't auto-install. - return; - } - - // We decided against prompting the user for a decision. This may be because we were - // explicitly asked not to, or because we are currently on wi-fi anyway, or because we - // already know the answer to the question. We'll enqueue a request ; StartDownloadAction - // knows to use the correct type of network according to the current settings. - - // Also note that once it's auto-installed, a word list will be marked as INSTALLED. It will - // thus receive automatic updates if there are any, which is what we want. If the user does - // not want this word list, they will have to go to the settings and change them, which will - // change the shared preferences. So there is no way for a word list that has been - // auto-installed once to get auto-installed again, and that's what we want. - final ActionBatch actions = new ActionBatch(); - WordListMetadata metadata = WordListMetadata.createFromContentValues(installCandidate); - actions.add(new ActionBatch.StartDownloadAction(clientId, metadata)); - final String localeString = installCandidate.getAsString(MetadataDbHelper.LOCALE_COLUMN); - - // We are in a content provider: we can't do any UI at all. We have to defer the displaying - // itself to the service. Also, we only display this when the user does not have a - // dictionary for this language already. During setup wizard, however, this UI is - // suppressed. - final boolean deviceProvisioned = Settings.Global.getInt(context.getContentResolver(), - Settings.Global.DEVICE_PROVISIONED, 0) != 0; - if (deviceProvisioned) { - final Intent intent = new Intent(); - intent.setClass(context, DictionaryService.class); - intent.setAction(DictionaryService.SHOW_DOWNLOAD_TOAST_INTENT_ACTION); - intent.putExtra(DictionaryService.LOCALE_INTENT_ARGUMENT, localeString); - context.startService(intent); - } else { - Log.i(TAG, "installIfNeverRequested() : Don't show download toast"); - } - - Log.i(TAG, "installIfNeverRequested() : StartDownloadAction for " + metadata); - actions.execute(context, new LogProblemReporter(TAG)); - } - - /** - * Marks the word list with the passed id as used. - * - * This will download/install the list as required. The action will see that the destination - * word list is a valid list, and take appropriate action - in this case, mark it as used. - * @see ActionBatch.Action#execute - * - * @param context the context for using action batches. - * @param clientId the id of the client. - * @param wordlistId the id of the word list to mark as installed. - * @param version the version of the word list to mark as installed. - * @param status the current status of the word list. - * @param allowDownloadOnMeteredData whether to download even on metered data connection - */ - // The version argument is not used yet, because we don't need it to retrieve the information - // we need. However, the pair (id, version) being the primary key to a word list in the database - // it feels better for consistency to pass it, and some methods retrieving information about a - // word list need it so we may need it in the future. - public static void markAsUsed(final Context context, final String clientId, - final String wordlistId, final int version, - final int status, final boolean allowDownloadOnMeteredData) { - final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( - context, clientId, wordlistId, version); - - if (null == wordListMetaData) return; - - final ActionBatch actions = new ActionBatch(); - if (MetadataDbHelper.STATUS_DISABLED == status - || MetadataDbHelper.STATUS_DELETING == status) { - actions.add(new ActionBatch.EnableAction(clientId, wordListMetaData)); - } else if (MetadataDbHelper.STATUS_AVAILABLE == status) { - actions.add(new ActionBatch.StartDownloadAction(clientId, wordListMetaData)); - } else { - Log.e(TAG, "Unexpected state of the word list for markAsUsed : " + status); - } - actions.execute(context, new LogProblemReporter(TAG)); - signalNewDictionaryState(context); - } - - /** - * Marks the word list with the passed id as unused. - * - * This leaves the file on the disk for ulterior use. The action will see that the destination - * word list is null, and take appropriate action - in this case, mark it as unused. - * @see ActionBatch.Action#execute - * - * @param context the context for using action batches. - * @param clientId the id of the client. - * @param wordlistId the id of the word list to mark as installed. - * @param version the version of the word list to mark as installed. - * @param status the current status of the word list. - */ - // The version and status arguments are not used yet, but this method matches its interface to - // markAsUsed for consistency. - public static void markAsUnused(final Context context, final String clientId, - final String wordlistId, final int version, final int status) { - - final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( - context, clientId, wordlistId, version); - - if (null == wordListMetaData) return; - final ActionBatch actions = new ActionBatch(); - actions.add(new ActionBatch.DisableAction(clientId, wordListMetaData)); - actions.execute(context, new LogProblemReporter(TAG)); - signalNewDictionaryState(context); - } - - /** - * Marks the word list with the passed id as deleting. - * - * This basically means that on the next chance there is (right away if Android Keyboard - * happens to be up, or the next time it gets up otherwise) the dictionary pack will - * supply an empty dictionary to it that will replace whatever dictionary is installed. - * This allows to release the space taken by a dictionary (except for the few bytes the - * empty dictionary takes up), and override a built-in default dictionary so that we - * can fake delete a built-in dictionary. - * - * @param context the context to open the database on. - * @param clientId the id of the client. - * @param wordlistId the id of the word list to mark as deleted. - * @param version the version of the word list to mark as deleted. - * @param status the current status of the word list. - */ - public static void markAsDeleting(final Context context, final String clientId, - final String wordlistId, final int version, final int status) { - - final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( - context, clientId, wordlistId, version); - - if (null == wordListMetaData) return; - final ActionBatch actions = new ActionBatch(); - actions.add(new ActionBatch.DisableAction(clientId, wordListMetaData)); - actions.add(new ActionBatch.StartDeleteAction(clientId, wordListMetaData)); - actions.execute(context, new LogProblemReporter(TAG)); - signalNewDictionaryState(context); - } - - /** - * Marks the word list with the passed id as actually deleted. - * - * This reverts to available status or deletes the row as appropriate. - * - * @param context the context to open the database on. - * @param clientId the id of the client. - * @param wordlistId the id of the word list to mark as deleted. - * @param version the version of the word list to mark as deleted. - * @param status the current status of the word list. - */ - public static void markAsDeleted(final Context context, final String clientId, - final String wordlistId, final int version, final int status) { - final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( - context, clientId, wordlistId, version); - - if (null == wordListMetaData) return; - - final ActionBatch actions = new ActionBatch(); - actions.add(new ActionBatch.FinishDeleteAction(clientId, wordListMetaData)); - actions.execute(context, new LogProblemReporter(TAG)); - signalNewDictionaryState(context); - } - - /** - * Checks whether the word list should be downloaded again; in which case an download & - * installation attempt is made. Otherwise the word list is marked broken. - * - * @param context the context to open the database on. - * @param clientId the id of the client. - * @param wordlistId the id of the word list which is broken. - * @param version the version of the broken word list. - */ - public static void markAsBrokenOrRetrying(final Context context, final String clientId, - final String wordlistId, final int version) { - boolean isRetryPossible = MetadataDbHelper.maybeMarkEntryAsRetrying( - MetadataDbHelper.getDb(context, clientId), wordlistId, version); - - if (isRetryPossible) { - if (DEBUG) { - Log.d(TAG, "Attempting to download & install the wordlist again."); - } - final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( - context, clientId, wordlistId, version); - if (wordListMetaData == null) { - return; - } - - final ActionBatch actions = new ActionBatch(); - actions.add(new ActionBatch.StartDownloadAction(clientId, wordListMetaData)); - actions.execute(context, new LogProblemReporter(TAG)); - } else { - if (DEBUG) { - Log.d(TAG, "Retries for wordlist exhausted, deleting the wordlist from table."); - } - MetadataDbHelper.deleteEntry(MetadataDbHelper.getDb(context, clientId), - wordlistId, version); - } - } -} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/WordListPreference.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/WordListPreference.java index 1b93d7b5..ae3572bc 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/WordListPreference.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/WordListPreference.java @@ -169,7 +169,6 @@ public final class WordListPreference extends Preference { final Context context = getContext(); final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context); CommonPreferences.disable(prefs, mWordlistId); - UpdateHandler.markAsUnused(context, mClientId, mWordlistId, mVersion, mStatus); if (MetadataDbHelper.STATUS_DOWNLOADING == mStatus) { setStatus(MetadataDbHelper.STATUS_AVAILABLE); } else if (MetadataDbHelper.STATUS_INSTALLED == mStatus) { @@ -185,8 +184,6 @@ public final class WordListPreference extends Preference { final Context context = getContext(); final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context); CommonPreferences.enable(prefs, mWordlistId); - // Explicit enabling by the user : allow downloading on metered data connection. - UpdateHandler.markAsUsed(context, mClientId, mWordlistId, mVersion, mStatus, true); if (MetadataDbHelper.STATUS_AVAILABLE == mStatus) { setStatus(MetadataDbHelper.STATUS_DOWNLOADING); } else if (MetadataDbHelper.STATUS_DISABLED == mStatus @@ -207,7 +204,6 @@ public final class WordListPreference extends Preference { final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context); CommonPreferences.disable(prefs, mWordlistId); setStatus(MetadataDbHelper.STATUS_DELETING); - UpdateHandler.markAsDeleting(context, mClientId, mWordlistId, mVersion, mStatus); } @Override @@ -215,16 +211,6 @@ public final class WordListPreference extends Preference { super.onBindView(view); ((ViewGroup)view).setLayoutTransition(null); - final DictionaryDownloadProgressBar progressBar = - (DictionaryDownloadProgressBar)view.findViewById(R.id.dictionary_line_progress_bar); - final TextView status = (TextView)view.findViewById(android.R.id.summary); - progressBar.setIds(mClientId, mWordlistId); - progressBar.setMax(mFilesize); - final boolean showProgressBar = (MetadataDbHelper.STATUS_DOWNLOADING == mStatus); - setSummary(getSummary(mStatus)); - status.setVisibility(showProgressBar ? View.INVISIBLE : View.VISIBLE); - progressBar.setVisibility(showProgressBar ? View.VISIBLE : View.INVISIBLE); - final ButtonSwitcher buttonSwitcher = (ButtonSwitcher)view.findViewById( R.id.wordlist_button_switcher); // We need to clear the state of the button switcher, because we reuse views; if we didn't diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/BinaryDictionaryFileDumper.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/BinaryDictionaryFileDumper.java index 578c7388..b2ee751d 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/BinaryDictionaryFileDumper.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/BinaryDictionaryFileDumper.java @@ -29,7 +29,6 @@ import android.util.Log; import org.dslul.openboard.inputmethod.dictionarypack.DictionaryPackConstants; import org.dslul.openboard.inputmethod.dictionarypack.MD5Calculator; -import org.dslul.openboard.inputmethod.dictionarypack.UpdateHandler; import org.dslul.openboard.inputmethod.latin.common.FileUtils; import org.dslul.openboard.inputmethod.latin.define.DecoderSpecificConstants; import org.dslul.openboard.inputmethod.latin.utils.DictionaryInfoUtils; @@ -221,152 +220,6 @@ public final class BinaryDictionaryFileDumper { } } - /** - * Stages a word list the id of which is passed as an argument. This will write the file - * to the cache file name designated by its id and locale, overwriting it if already present - * and creating it (and its containing directory) if necessary. - */ - private static void installWordListToStaging(final String wordlistId, final String locale, - final String rawChecksum, final ContentProviderClient providerClient, - final Context context) { - final int COMPRESSED_CRYPTED_COMPRESSED = 0; - final int CRYPTED_COMPRESSED = 1; - final int COMPRESSED_CRYPTED = 2; - final int COMPRESSED_ONLY = 3; - final int CRYPTED_ONLY = 4; - final int NONE = 5; - final int MODE_MIN = COMPRESSED_CRYPTED_COMPRESSED; - final int MODE_MAX = NONE; - - final String clientId = context.getString(R.string.dictionary_pack_client_id); - final Uri.Builder wordListUriBuilder; - try { - wordListUriBuilder = getContentUriBuilderForType(clientId, - providerClient, QUERY_PATH_DATAFILE, wordlistId /* extraPath */); - } catch (RemoteException e) { - Log.e(TAG, "Can't communicate with the dictionary pack", e); - return; - } - final String finalFileName = - DictionaryInfoUtils.getStagingFileName(wordlistId, locale, context); - String tempFileName; - try { - tempFileName = BinaryDictionaryGetter.getTempFileName(wordlistId, context); - } catch (IOException e) { - Log.e(TAG, "Can't open the temporary file", e); - return; - } - - for (int mode = MODE_MIN; mode <= MODE_MAX; ++mode) { - final InputStream originalSourceStream; - InputStream inputStream = null; - InputStream uncompressedStream = null; - InputStream decryptedStream = null; - BufferedInputStream bufferedInputStream = null; - File outputFile = null; - BufferedOutputStream bufferedOutputStream = null; - AssetFileDescriptor afd = null; - final Uri wordListUri = wordListUriBuilder.build(); - try { - // Open input. - afd = openAssetFileDescriptor(providerClient, wordListUri); - // If we can't open it at all, don't even try a number of times. - if (null == afd) return; - originalSourceStream = afd.createInputStream(); - // Open output. - outputFile = new File(tempFileName); - // Just to be sure, delete the file. This may fail silently, and return false: this - // is the right thing to do, as we just want to continue anyway. - outputFile.delete(); - // Get the appropriate decryption method for this try - switch (mode) { - case COMPRESSED_CRYPTED_COMPRESSED: - uncompressedStream = - FileTransforms.getUncompressedStream(originalSourceStream); - decryptedStream = FileTransforms.getDecryptedStream(uncompressedStream); - inputStream = FileTransforms.getUncompressedStream(decryptedStream); - break; - case CRYPTED_COMPRESSED: - decryptedStream = FileTransforms.getDecryptedStream(originalSourceStream); - inputStream = FileTransforms.getUncompressedStream(decryptedStream); - break; - case COMPRESSED_CRYPTED: - uncompressedStream = - FileTransforms.getUncompressedStream(originalSourceStream); - inputStream = FileTransforms.getDecryptedStream(uncompressedStream); - break; - case COMPRESSED_ONLY: - inputStream = FileTransforms.getUncompressedStream(originalSourceStream); - break; - case CRYPTED_ONLY: - inputStream = FileTransforms.getDecryptedStream(originalSourceStream); - break; - case NONE: - inputStream = originalSourceStream; - break; - } - bufferedInputStream = new BufferedInputStream(inputStream); - bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(outputFile)); - checkMagicAndCopyFileTo(bufferedInputStream, bufferedOutputStream); - bufferedOutputStream.flush(); - bufferedOutputStream.close(); - - if (SHOULD_VERIFY_CHECKSUM) { - final String actualRawChecksum = MD5Calculator.checksum( - new BufferedInputStream(new FileInputStream(outputFile))); - Log.i(TAG, "Computed checksum for downloaded dictionary. Expected = " - + rawChecksum + " ; actual = " + actualRawChecksum); - if (!TextUtils.isEmpty(rawChecksum) && !rawChecksum.equals(actualRawChecksum)) { - throw new IOException( - "Could not decode the file correctly : checksum differs"); - } - } - - // move the output file to the final staging file. - final File finalFile = new File(finalFileName); - if (!FileUtils.renameTo(outputFile, finalFile)) { - Log.e(TAG, String.format("Failed to rename from %s to %s.", - outputFile.getAbsoluteFile(), finalFile.getAbsoluteFile())); - } - - wordListUriBuilder.appendQueryParameter(QUERY_PARAMETER_DELETE_RESULT, - QUERY_PARAMETER_SUCCESS); - if (0 >= providerClient.delete(wordListUriBuilder.build(), null, null)) { - Log.e(TAG, "Could not have the dictionary pack delete a word list"); - } - Log.d(TAG, "Successfully copied file for wordlist ID " + wordlistId); - // Success! Close files (through the finally{} clause) and return. - return; - } catch (Exception e) { - if (DEBUG) { - Log.e(TAG, "Can't open word list in mode " + mode, e); - } - if (null != outputFile) { - // This may or may not fail. The file may not have been created if the - // exception was thrown before it could be. Hence, both failure and - // success are expected outcomes, so we don't check the return value. - outputFile.delete(); - } - // Try the next method. - } finally { - // Ignore exceptions while closing files. - closeAssetFileDescriptorAndReportAnyException(afd); - closeCloseableAndReportAnyException(inputStream); - closeCloseableAndReportAnyException(uncompressedStream); - closeCloseableAndReportAnyException(decryptedStream); - closeCloseableAndReportAnyException(bufferedInputStream); - closeCloseableAndReportAnyException(bufferedOutputStream); - } - } - - // We could not copy the file at all. This is very unexpected. - // I'd rather not print the word list ID to the log out of security concerns - Log.e(TAG, "Could not copy a word list. Will not be able to use it."); - // If we can't copy it we should warn the dictionary provider so that it can mark it - // as invalid. - reportBrokenFileToDictionaryProvider(providerClient, clientId, wordlistId); - } - public static boolean reportBrokenFileToDictionaryProvider( final ContentProviderClient providerClient, final String clientId, final String wordlistId) { @@ -405,53 +258,6 @@ public final class BinaryDictionaryFileDumper { } } - /** - * Queries a content provider for word list data for some locale and stage the returned files - * - * This will query a content provider for word list data for a given locale, and copy the - * files locally so that they can be mmap'ed. This may overwrite previously cached word lists - * with newer versions if a newer version is made available by the content provider. - * @throw FileNotFoundException if the provider returns non-existent data. - * @throw IOException if the provider-returned data could not be read. - */ - public static void installDictToStagingFromContentProvider(final Locale locale, - final Context context, final boolean hasDefaultWordList) { - final ContentProviderClient providerClient; - try { - providerClient = context.getContentResolver(). - acquireContentProviderClient(getProviderUriBuilder("").build()); - } catch (final SecurityException e) { - Log.e(TAG, "No permission to communicate with the dictionary provider", e); - return; - } - if (null == providerClient) { - Log.e(TAG, "Can't establish communication with the dictionary provider"); - return; - } - try { - final List idList = getWordListWordListInfos(locale, context, - hasDefaultWordList); - for (WordListInfo id : idList) { - installWordListToStaging(id.mId, id.mLocale, id.mRawChecksum, providerClient, - context); - } - } finally { - providerClient.release(); - } - } - - /** - * Downloads the dictionary if it was never requested/used. - * - * @param locale locale to download - * @param context the context for resources and providers. - * @param hasDefaultWordList whether the default wordlist exists in the resources. - */ - public static void downloadDictIfNeverRequested(final Locale locale, - final Context context, final boolean hasDefaultWordList) { - getWordListWordListInfos(locale, context, hasDefaultWordList); - } - /** * Copies the data in an input stream to a target file if the magic number matches. * @@ -533,7 +339,6 @@ public final class BinaryDictionaryFileDumper { InputStream inputStream = null; try { inputStream = context.getResources().openRawResource(metadataResourceId); - UpdateHandler.handleMetadata(context, inputStream, clientId); } catch (Exception e) { Log.w(TAG, "Failed to read metadata.json from resources", e); } finally { diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/BinaryDictionaryGetter.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/BinaryDictionaryGetter.java index 44b5d104..08c7026d 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/BinaryDictionaryGetter.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/BinaryDictionaryGetter.java @@ -241,19 +241,6 @@ final public class BinaryDictionaryGetter { */ public static ArrayList getDictionaryFiles(final Locale locale, final Context context, boolean notifyDictionaryPackForUpdates) { - if (notifyDictionaryPackForUpdates) { - final boolean hasDefaultWordList = DictionaryInfoUtils.isDictionaryAvailable( - context, locale); - // It makes sure that the first time keyboard comes up and the dictionaries are reset, - // the DB is populated with the appropriate values for each locale. Helps in downloading - // the dictionaries when the user enables and switches new languages before the - // DictionaryService runs. - BinaryDictionaryFileDumper.downloadDictIfNeverRequested( - locale, context, hasDefaultWordList); - - // Move a staging files to the cache ddirectories if any. - DictionaryInfoUtils.moveStagingFilesIfExists(context); - } final File[] cachedWordLists = getCachedWordLists(locale.toString(), context); final String mainDictId = DictionaryInfoUtils.getMainDictId(locale); final DictPackSettings dictPackSettings = new DictPackSettings(context); diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/SystemBroadcastReceiver.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/SystemBroadcastReceiver.java index 68eb57c6..d597b197 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/SystemBroadcastReceiver.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/SystemBroadcastReceiver.java @@ -32,7 +32,6 @@ import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; import org.dslul.openboard.inputmethod.dictionarypack.DictionaryPackConstants; -import org.dslul.openboard.inputmethod.dictionarypack.DownloadManagerWrapper; import org.dslul.openboard.inputmethod.keyboard.KeyboardLayoutSet; import org.dslul.openboard.inputmethod.latin.settings.Settings; import org.dslul.openboard.inputmethod.latin.setup.SetupActivity; @@ -77,12 +76,6 @@ public final class SystemBroadcastReceiver extends BroadcastReceiver { final InputMethodSubtype[] additionalSubtypes = richImm.getAdditionalSubtypes(); richImm.setAdditionalInputMethodSubtypes(additionalSubtypes); toggleAppIcon(context); - - // Remove all the previously scheduled downloads. This will also makes sure - // that any erroneously stuck downloads will get cleared. (b/21797386) - removeOldDownloads(context); - // b/21797386 - // downloadLatestDictionaries(context); } else if (Intent.ACTION_BOOT_COMPLETED.equals(intentAction)) { Log.i(TAG, "Boot has been completed"); toggleAppIcon(context); @@ -110,38 +103,6 @@ public final class SystemBroadcastReceiver extends BroadcastReceiver { } } - private void removeOldDownloads(Context context) { - try { - Log.i(TAG, "Removing the old downloads in progress of the previous keyboard version."); - final DownloadManagerWrapper downloadManagerWrapper = new DownloadManagerWrapper( - context); - final DownloadManager.Query q = new DownloadManager.Query(); - // Query all the download statuses except the succeeded ones. - q.setFilterByStatus(DownloadManager.STATUS_FAILED - | DownloadManager.STATUS_PAUSED - | DownloadManager.STATUS_PENDING - | DownloadManager.STATUS_RUNNING); - final Cursor c = downloadManagerWrapper.query(q); - if (c != null) { - for (c.moveToFirst(); !c.isAfterLast(); c.moveToNext()) { - final long downloadId = c - .getLong(c.getColumnIndex(DownloadManager.COLUMN_ID)); - downloadManagerWrapper.remove(downloadId); - Log.i(TAG, "Removed the download with Id: " + downloadId); - } - c.close(); - } - } catch (Exception e) { - Log.e(TAG, "Exception while removing old downloads."); - } - } - - private void downloadLatestDictionaries(Context context) { - final Intent updateIntent = new Intent( - DictionaryPackConstants.INIT_AND_UPDATE_NOW_INTENT_ACTION); - context.sendBroadcast(updateIntent); - } - public static void toggleAppIcon(final Context context) { final int appInfoFlags = context.getApplicationInfo().flags; final boolean isSystemApp = (appInfoFlags & ApplicationInfo.FLAG_SYSTEM) > 0; diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/accounts/AccountStateChangedListener.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/accounts/AccountStateChangedListener.java deleted file mode 100644 index 5a14862f..00000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/accounts/AccountStateChangedListener.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.dslul.openboard.inputmethod.latin.accounts; - -import androidx.annotation.NonNull; - -import javax.annotation.Nullable; - -/** - * Handles changes to account used to sign in to the keyboard. - * e.g. account switching/sign-in/sign-out from the keyboard - * user toggling the sync preference. - */ -public class AccountStateChangedListener { - - /** - * Called when the current account being used in keyboard is signed out. - * - * @param oldAccount the account that was signed out of. - */ - public static void onAccountSignedOut(@NonNull String oldAccount) { - } - - /** - * Called when the user signs-in to the keyboard. - * This may be called when the user switches accounts to sign in with a different account. - * - * @param oldAccount the previous account that was being used for sign-in. - * May be null for a fresh sign-in. - * @param newAccount the account being used for sign-in. - */ - public static void onAccountSignedIn(@Nullable String oldAccount, @NonNull String newAccount) { - } - - /** - * Called when the user toggles the sync preference. - * - * @param account the account being used for sync. - * @param syncEnabled indicates whether sync has been enabled or not. - */ - public static void onSyncPreferenceChanged(@Nullable String account, boolean syncEnabled) { - } - - /** - * Forces an immediate sync to happen. - * This should only be used for debugging purposes. - * - * @param account the account to use for sync. - */ - public static void forceSync(@Nullable String account) { - } - - /** - * Forces an immediate deletion of user's data. - * This should only be used for debugging purposes. - * - * @param account the account to use for sync. - */ - public static void forceDelete(@Nullable String account) { - } -} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/accounts/AccountsChangedReceiver.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/accounts/AccountsChangedReceiver.java deleted file mode 100644 index 7bdca052..00000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/accounts/AccountsChangedReceiver.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.dslul.openboard.inputmethod.latin.accounts; - -import android.accounts.AccountManager; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.text.TextUtils; -import android.util.Log; - -import org.dslul.openboard.inputmethod.annotations.UsedForTesting; -import org.dslul.openboard.inputmethod.latin.settings.LocalSettingsConstants; - -/** - * {@link BroadcastReceiver} for {@link AccountManager#LOGIN_ACCOUNTS_CHANGED_ACTION}. - */ -public class AccountsChangedReceiver extends BroadcastReceiver { - static final String TAG = "AccountsChangedReceiver"; - - @Override - public void onReceive(Context context, Intent intent) { - if (!AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION.equals(intent.getAction())) { - Log.w(TAG, "Received unknown broadcast: " + intent); - return; - } - - // Ideally the account preference could live in a different preferences file - // that wasn't being backed up and restored, however the preference fragments - // currently only deal with the default shared preferences which is why - // separating this out into a different file is not trivial currently. - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - final String currentAccount = prefs.getString( - LocalSettingsConstants.PREF_ACCOUNT_NAME, null); - removeUnknownAccountFromPreference(prefs, getAccountsForLogin(context), currentAccount); - } - - /** - * Helper method to help test this receiver. - */ - @UsedForTesting - protected String[] getAccountsForLogin(Context context) { - return LoginAccountUtils.getAccountsForLogin(context); - } - - /** - * Removes the currentAccount from preferences if it's not found - * in the list of current accounts. - */ - private static void removeUnknownAccountFromPreference(final SharedPreferences prefs, - final String[] accounts, final String currentAccount) { - if (currentAccount == null) { - return; - } - for (final String account : accounts) { - if (TextUtils.equals(currentAccount, account)) { - return; - } - } - Log.i(TAG, "The current account was removed from the system: " + currentAccount); - prefs.edit() - .remove(LocalSettingsConstants.PREF_ACCOUNT_NAME) - .apply(); - } -} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/accounts/AuthUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/accounts/AuthUtils.java deleted file mode 100644 index 12823b25..00000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/accounts/AuthUtils.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.dslul.openboard.inputmethod.latin.accounts; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.AccountManagerCallback; -import android.accounts.AccountManagerFuture; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; -import android.content.Context; -import android.os.Bundle; -import android.os.Handler; - -import java.io.IOException; - -/** - * Utility class that handles generation/invalidation of auth tokens in the app. - */ -public class AuthUtils { - private final AccountManager mAccountManager; - - public AuthUtils(Context context) { - mAccountManager = AccountManager.get(context); - } - - /** - * @see AccountManager#invalidateAuthToken(String, String) - */ - public void invalidateAuthToken(final String accountType, final String authToken) { - mAccountManager.invalidateAuthToken(accountType, authToken); - } - - /** - * @see AccountManager#getAuthToken( - * Account, String, Bundle, boolean, AccountManagerCallback, Handler) - */ - public AccountManagerFuture getAuthToken(final Account account, - final String authTokenType, final Bundle options, final boolean notifyAuthFailure, - final AccountManagerCallback callback, final Handler handler) { - return mAccountManager.getAuthToken(account, authTokenType, options, notifyAuthFailure, - callback, handler); - } - - /** - * @see AccountManager#blockingGetAuthToken(Account, String, boolean) - */ - public String blockingGetAuthToken(final Account account, final String authTokenType, - final boolean notifyAuthFailure) throws OperationCanceledException, - AuthenticatorException, IOException { - return mAccountManager.blockingGetAuthToken(account, authTokenType, notifyAuthFailure); - } -} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/accounts/LoginAccountUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/accounts/LoginAccountUtils.java deleted file mode 100644 index d4e27c42..00000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/accounts/LoginAccountUtils.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.dslul.openboard.inputmethod.latin.accounts; - -import android.content.Context; - -import javax.annotation.Nonnull; - -/** - * Utility class for retrieving accounts that may be used for login. - */ -public class LoginAccountUtils { - /** - * This defines the type of account this class deals with. - * This account type is used when listing the accounts available on the device for login. - */ - public static final String ACCOUNT_TYPE = ""; - - private LoginAccountUtils() { - // This utility class is not publicly instantiable. - } - - /** - * Get the accounts available for login. - * - * @return an array of accounts. Empty (never null) if no accounts are available for login. - */ - @Nonnull - @SuppressWarnings("unused") - public static String[] getAccountsForLogin(final Context context) { - return new String[0]; - } -} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/define/ProductionFlags.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/define/ProductionFlags.java index 2c7f4acb..76a7686c 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/define/ProductionFlags.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/define/ProductionFlags.java @@ -39,20 +39,4 @@ public final class ProductionFlags { */ public static final boolean IS_SPLIT_KEYBOARD_SUPPORTED = true; - /** - * When {@code false}, account sign-in in keyboard is not yet ready to be enabled. - */ - public static final boolean ENABLE_ACCOUNT_SIGN_IN = false; - - /** - * When {@code true}, user history dictionary sync feature is ready to be enabled. - */ - public static final boolean ENABLE_USER_HISTORY_DICTIONARY_SYNC = - ENABLE_ACCOUNT_SIGN_IN && false; - - /** - * When {@code true}, the IME maintains per account {@link UserHistoryDictionary}. - */ - public static final boolean ENABLE_PER_ACCOUNT_USER_HISTORY_DICTIONARY = - ENABLE_ACCOUNT_SIGN_IN && false; } diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/personalization/UserHistoryDictionary.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/personalization/UserHistoryDictionary.java index 66d75bf9..f78c5374 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/personalization/UserHistoryDictionary.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/personalization/UserHistoryDictionary.java @@ -58,25 +58,7 @@ public class UserHistoryDictionary extends ExpandableBinaryDictionary { @UsedForTesting static String getUserHistoryDictName(final String name, final Locale locale, @Nullable final File dictFile, @Nullable final String account) { - if (!ProductionFlags.ENABLE_PER_ACCOUNT_USER_HISTORY_DICTIONARY) { - return getDictName(name, locale, dictFile); - } - return getUserHistoryDictNamePerAccount(name, locale, dictFile, account); - } - - /** - * Uses the currently signed in account to determine the dictionary name. - */ - private static String getUserHistoryDictNamePerAccount(final String name, final Locale locale, - @Nullable final File dictFile, @Nullable final String account) { - if (dictFile != null) { - return dictFile.getName(); - } - String dictName = name + "." + locale.toString(); - if (account != null) { - dictName += "." + account; - } - return dictName; + return getDictName(name, locale, dictFile); } // Note: This method is called by {@link DictionaryFacilitator} using Java reflection. diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/AccountsSettingsFragment.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/AccountsSettingsFragment.java deleted file mode 100644 index 66eb917b..00000000 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/AccountsSettingsFragment.java +++ /dev/null @@ -1,508 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.dslul.openboard.inputmethod.latin.settings; - -import static org.dslul.openboard.inputmethod.latin.settings.LocalSettingsConstants.PREF_ACCOUNT_NAME; -import static org.dslul.openboard.inputmethod.latin.settings.LocalSettingsConstants.PREF_ENABLE_CLOUD_SYNC; - -import android.Manifest; -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnShowListener; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.os.AsyncTask; -import android.os.Bundle; -import android.preference.Preference; -import android.preference.Preference.OnPreferenceClickListener; -import android.preference.TwoStatePreference; -import android.text.TextUtils; -import android.text.method.LinkMovementMethod; -import android.widget.ListView; -import android.widget.TextView; - -import org.dslul.openboard.inputmethod.annotations.UsedForTesting; -import org.dslul.openboard.inputmethod.latin.R; -import org.dslul.openboard.inputmethod.latin.accounts.AccountStateChangedListener; -import org.dslul.openboard.inputmethod.latin.accounts.LoginAccountUtils; -import org.dslul.openboard.inputmethod.latin.define.ProductionFlags; -import org.dslul.openboard.inputmethod.latin.permissions.PermissionsUtil; -import org.dslul.openboard.inputmethod.latin.utils.ManagedProfileUtils; - -import java.util.concurrent.atomic.AtomicBoolean; - -import javax.annotation.Nullable; - -/** - * "Accounts & Privacy" settings sub screen. - * - * This settings sub screen handles the following preferences: - *
  • Account selection/management for IME
  • - *
  • Sync preferences
  • - *
  • Privacy preferences
  • - */ -public final class AccountsSettingsFragment extends SubScreenFragment { - private static final String PREF_ENABLE_SYNC_NOW = "pref_enable_cloud_sync"; - private static final String PREF_SYNC_NOW = "pref_sync_now"; - private static final String PREF_CLEAR_SYNC_DATA = "pref_clear_sync_data"; - - static final String PREF_ACCCOUNT_SWITCHER = "account_switcher"; - - /** - * Onclick listener for sync now pref. - */ - private final Preference.OnPreferenceClickListener mSyncNowListener = - new SyncNowListener(); - /** - * Onclick listener for delete sync pref. - */ - private final Preference.OnPreferenceClickListener mDeleteSyncDataListener = - new DeleteSyncDataListener(); - - /** - * Onclick listener for enable sync pref. - */ - private final Preference.OnPreferenceClickListener mEnableSyncClickListener = - new EnableSyncClickListener(); - - /** - * Enable sync checkbox pref. - */ - private TwoStatePreference mEnableSyncPreference; - - /** - * Enable sync checkbox pref. - */ - private Preference mSyncNowPreference; - - /** - * Clear sync data pref. - */ - private Preference mClearSyncDataPreference; - - /** - * Account switcher preference. - */ - private Preference mAccountSwitcher; - - /** - * Stores if we are currently detecting a managed profile. - */ - private AtomicBoolean mManagedProfileBeingDetected = new AtomicBoolean(true); - - /** - * Stores if we have successfully detected if the device has a managed profile. - */ - private AtomicBoolean mHasManagedProfile = new AtomicBoolean(false); - - @Override - public void onCreate(final Bundle icicle) { - super.onCreate(icicle); - addPreferencesFromResource(R.xml.prefs_screen_accounts); - - mAccountSwitcher = findPreference(PREF_ACCCOUNT_SWITCHER); - mEnableSyncPreference = (TwoStatePreference) findPreference(PREF_ENABLE_SYNC_NOW); - mSyncNowPreference = findPreference(PREF_SYNC_NOW); - mClearSyncDataPreference = findPreference(PREF_CLEAR_SYNC_DATA); - - if (ProductionFlags.IS_METRICS_LOGGING_SUPPORTED) { - final Preference enableMetricsLogging = - findPreference(Settings.PREF_ENABLE_METRICS_LOGGING); - final Resources res = getResources(); - if (enableMetricsLogging != null) { - final String enableMetricsLoggingTitle = res.getString( - R.string.enable_metrics_logging, getApplicationName()); - enableMetricsLogging.setTitle(enableMetricsLoggingTitle); - } - } else { - removePreference(Settings.PREF_ENABLE_METRICS_LOGGING); - } - - if (!ProductionFlags.ENABLE_USER_HISTORY_DICTIONARY_SYNC) { - removeSyncPreferences(); - } else { - // Disable by default till we are sure we can enable this. - disableSyncPreferences(); - new ManagedProfileCheckerTask(this).execute(); - } - } - - /** - * Task to check work profile. If found, it removes the sync prefs. If not, - * it enables them. - */ - private static class ManagedProfileCheckerTask extends AsyncTask { - private final AccountsSettingsFragment mFragment; - - private ManagedProfileCheckerTask(final AccountsSettingsFragment fragment) { - mFragment = fragment; - } - - @Override - protected void onPreExecute() { - mFragment.mManagedProfileBeingDetected.set(true); - } - @Override - protected Boolean doInBackground(Void... params) { - return ManagedProfileUtils.getInstance().hasWorkProfile(mFragment.getActivity()); - } - - @Override - protected void onPostExecute(final Boolean hasWorkProfile) { - mFragment.mHasManagedProfile.set(hasWorkProfile); - mFragment.mManagedProfileBeingDetected.set(false); - mFragment.refreshSyncSettingsUI(); - } - } - - private void enableSyncPreferences(final String[] accountsForLogin, - final String currentAccountName) { - if (!ProductionFlags.ENABLE_USER_HISTORY_DICTIONARY_SYNC) { - return; - } - mAccountSwitcher.setEnabled(true); - - mEnableSyncPreference.setEnabled(true); - mEnableSyncPreference.setOnPreferenceClickListener(mEnableSyncClickListener); - - mSyncNowPreference.setEnabled(true); - mSyncNowPreference.setOnPreferenceClickListener(mSyncNowListener); - - mClearSyncDataPreference.setEnabled(true); - mClearSyncDataPreference.setOnPreferenceClickListener(mDeleteSyncDataListener); - - if (currentAccountName != null) { - mAccountSwitcher.setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(final Preference preference) { - if (accountsForLogin.length > 0) { - // TODO: Add addition of account. - createAccountPicker(accountsForLogin, getSignedInAccountName(), - new AccountChangedListener(null)).show(); - } - return true; - } - }); - } - } - - /** - * Two reasons for disable - work profile or no accounts on device. - */ - private void disableSyncPreferences() { - if (!ProductionFlags.ENABLE_USER_HISTORY_DICTIONARY_SYNC) { - return; - } - - mAccountSwitcher.setEnabled(false); - mEnableSyncPreference.setEnabled(false); - mSyncNowPreference.setEnabled(false); - mClearSyncDataPreference.setEnabled(false); - } - - /** - * Called only when ProductionFlag is turned off. - */ - private void removeSyncPreferences() { - removePreference(PREF_ACCCOUNT_SWITCHER); - removePreference(PREF_ENABLE_CLOUD_SYNC); - removePreference(PREF_SYNC_NOW); - removePreference(PREF_CLEAR_SYNC_DATA); - } - - @Override - public void onResume() { - super.onResume(); - refreshSyncSettingsUI(); - } - - @Override - public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) { - if (TextUtils.equals(key, PREF_ACCOUNT_NAME)) { - refreshSyncSettingsUI(); - } else if (TextUtils.equals(key, PREF_ENABLE_CLOUD_SYNC)) { - mEnableSyncPreference = (TwoStatePreference) findPreference(PREF_ENABLE_SYNC_NOW); - final boolean syncEnabled = prefs.getBoolean(PREF_ENABLE_CLOUD_SYNC, false); - if (isSyncEnabled()) { - mEnableSyncPreference.setSummary(getString(R.string.cloud_sync_summary)); - } else { - mEnableSyncPreference.setSummary(getString(R.string.cloud_sync_summary_disabled)); - } - AccountStateChangedListener.onSyncPreferenceChanged(getSignedInAccountName(), - syncEnabled); - } - } - - /** - * Checks different states like whether account is present or managed profile is present - * and sets the sync settings accordingly. - */ - private void refreshSyncSettingsUI() { - if (!ProductionFlags.ENABLE_USER_HISTORY_DICTIONARY_SYNC) { - return; - } - boolean hasAccountsPermission = PermissionsUtil.checkAllPermissionsGranted( - getActivity(), Manifest.permission.READ_CONTACTS); - - final String[] accountsForLogin = hasAccountsPermission ? - LoginAccountUtils.getAccountsForLogin(getActivity()) : new String[0]; - final String currentAccount = hasAccountsPermission ? getSignedInAccountName() : null; - - if (hasAccountsPermission && !mManagedProfileBeingDetected.get() && - !mHasManagedProfile.get() && accountsForLogin.length > 0) { - // Sync can be used by user; enable all preferences. - enableSyncPreferences(accountsForLogin, currentAccount); - } else { - // Sync cannot be used by user; disable all preferences. - disableSyncPreferences(); - } - refreshSyncSettingsMessaging(hasAccountsPermission, mManagedProfileBeingDetected.get(), - mHasManagedProfile.get(), accountsForLogin.length > 0, - currentAccount); - } - - /** - * @param hasAccountsPermission whether the app has the permission to read accounts. - * @param managedProfileBeingDetected whether we are in process of determining work profile. - * @param hasManagedProfile whether the device has work profile. - * @param hasAccountsForLogin whether the device has enough accounts for login. - * @param currentAccount the account currently selected in the application. - */ - private void refreshSyncSettingsMessaging(boolean hasAccountsPermission, - boolean managedProfileBeingDetected, - boolean hasManagedProfile, - boolean hasAccountsForLogin, - String currentAccount) { - if (!ProductionFlags.ENABLE_USER_HISTORY_DICTIONARY_SYNC) { - return; - } - - if (!hasAccountsPermission) { - mEnableSyncPreference.setChecked(false); - mEnableSyncPreference.setSummary(getString(R.string.cloud_sync_summary_disabled)); - mAccountSwitcher.setSummary(""); - return; - } else if (managedProfileBeingDetected) { - // If we are determining eligiblity, we show empty summaries. - // Once we have some deterministic result, we set summaries based on different results. - mEnableSyncPreference.setSummary(""); - mAccountSwitcher.setSummary(""); - } else if (hasManagedProfile) { - mEnableSyncPreference.setSummary( - getString(R.string.cloud_sync_summary_disabled_work_profile)); - } else if (!hasAccountsForLogin) { - mEnableSyncPreference.setSummary(getString(R.string.add_account_to_enable_sync)); - } else if (isSyncEnabled()) { - mEnableSyncPreference.setSummary(getString(R.string.cloud_sync_summary)); - } else { - mEnableSyncPreference.setSummary(getString(R.string.cloud_sync_summary_disabled)); - } - - // Set some interdependent settings. - // No account automatically turns off sync. - if (!managedProfileBeingDetected && !hasManagedProfile) { - if (currentAccount != null) { - mAccountSwitcher.setSummary(getString(R.string.account_selected, currentAccount)); - } else { - mEnableSyncPreference.setChecked(false); - mAccountSwitcher.setSummary(getString(R.string.no_accounts_selected)); - } - } - } - - @Nullable - String getSignedInAccountName() { - return getSharedPreferences().getString(LocalSettingsConstants.PREF_ACCOUNT_NAME, null); - } - - boolean isSyncEnabled() { - return getSharedPreferences().getBoolean(PREF_ENABLE_CLOUD_SYNC, false); - } - - /** - * Creates an account picker dialog showing the given accounts in a list and selecting - * the selected account by default. The list of accounts must not be null/empty. - * - * Package-private for testing. - * - * @param accounts list of accounts on the device. - * @param selectedAccount currently selected account - * @param positiveButtonClickListener listener that gets called when positive button is - * clicked - */ - @UsedForTesting - AlertDialog createAccountPicker(final String[] accounts, - final String selectedAccount, - final DialogInterface.OnClickListener positiveButtonClickListener) { - if (accounts == null || accounts.length == 0) { - throw new IllegalArgumentException("List of accounts must not be empty"); - } - - // See if the currently selected account is in the list. - // If it is, the entry is selected, and a sign-out button is provided. - // If it isn't, select the 0th account by default which will get picked up - // if the user presses OK. - int index = 0; - boolean isSignedIn = false; - for (int i = 0; i < accounts.length; i++) { - if (TextUtils.equals(accounts[i], selectedAccount)) { - index = i; - isSignedIn = true; - break; - } - } - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) - .setTitle(R.string.account_select_title) - .setSingleChoiceItems(accounts, index, null) - .setPositiveButton(R.string.account_select_ok, positiveButtonClickListener) - .setNegativeButton(R.string.account_select_cancel, null); - if (isSignedIn) { - builder.setNeutralButton(R.string.account_select_sign_out, positiveButtonClickListener); - } - return builder.create(); - } - - /** - * Listener for a account selection changes from the picker. - * Persists/removes the account to/from shared preferences and sets up sync if required. - */ - class AccountChangedListener implements DialogInterface.OnClickListener { - /** - * Represents preference that should be changed based on account chosen. - */ - private TwoStatePreference mDependentPreference; - - AccountChangedListener(final TwoStatePreference dependentPreference) { - mDependentPreference = dependentPreference; - } - - @Override - public void onClick(final DialogInterface dialog, final int which) { - final String oldAccount = getSignedInAccountName(); - switch (which) { - case DialogInterface.BUTTON_POSITIVE: // Signed in - final ListView lv = ((AlertDialog)dialog).getListView(); - final String newAccount = - (String) lv.getItemAtPosition(lv.getCheckedItemPosition()); - getSharedPreferences() - .edit() - .putString(PREF_ACCOUNT_NAME, newAccount) - .apply(); - AccountStateChangedListener.onAccountSignedIn(oldAccount, newAccount); - if (mDependentPreference != null) { - mDependentPreference.setChecked(true); - } - break; - case DialogInterface.BUTTON_NEUTRAL: // Signed out - AccountStateChangedListener.onAccountSignedOut(oldAccount); - getSharedPreferences() - .edit() - .remove(PREF_ACCOUNT_NAME) - .apply(); - break; - } - } - } - - /** - * Listener that initiates the process of sync in the background. - */ - class SyncNowListener implements Preference.OnPreferenceClickListener { - @Override - public boolean onPreferenceClick(final Preference preference) { - AccountStateChangedListener.forceSync(getSignedInAccountName()); - return true; - } - } - - /** - * Listener that initiates the process of deleting user's data from the cloud. - */ - class DeleteSyncDataListener implements Preference.OnPreferenceClickListener { - @Override - public boolean onPreferenceClick(final Preference preference) { - final AlertDialog confirmationDialog = new AlertDialog.Builder(getActivity()) - .setTitle(R.string.clear_sync_data_title) - .setMessage(R.string.clear_sync_data_confirmation) - .setPositiveButton(R.string.clear_sync_data_ok, - new DialogInterface.OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, final int which) { - if (which == DialogInterface.BUTTON_POSITIVE) { - AccountStateChangedListener.forceDelete( - getSignedInAccountName()); - } - } - }) - .setNegativeButton(R.string.cloud_sync_cancel, null /* OnClickListener */) - .create(); - confirmationDialog.show(); - return true; - } - } - - /** - * Listens to events when user clicks on "Enable sync" feature. - */ - class EnableSyncClickListener implements OnShowListener, Preference.OnPreferenceClickListener { - // TODO(cvnguyen): Write tests. - @Override - public boolean onPreferenceClick(final Preference preference) { - final TwoStatePreference syncPreference = (TwoStatePreference) preference; - if (syncPreference.isChecked()) { - // Uncheck for now. - syncPreference.setChecked(false); - - // Show opt-in. - final AlertDialog optInDialog = new AlertDialog.Builder(getActivity()) - .setTitle(R.string.cloud_sync_title) - .setMessage(R.string.cloud_sync_opt_in_text) - .setPositiveButton(R.string.account_select_ok, - new DialogInterface.OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, - final int which) { - if (which == DialogInterface.BUTTON_POSITIVE) { - final Context context = getActivity(); - final String[] accountsForLogin = - LoginAccountUtils.getAccountsForLogin(context); - createAccountPicker(accountsForLogin, - getSignedInAccountName(), - new AccountChangedListener(syncPreference)) - .show(); - } - } - }) - .setNegativeButton(R.string.cloud_sync_cancel, null) - .create(); - optInDialog.setOnShowListener(this); - optInDialog.show(); - } - return true; - } - - @Override - public void onShow(DialogInterface dialog) { - TextView messageView = (TextView) ((AlertDialog) dialog).findViewById( - android.R.id.message); - if (messageView != null) { - messageView.setMovementMethod(LinkMovementMethod.getInstance()); - } - } - } -} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/Settings.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/Settings.java index 20c867ec..679f60e6 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/Settings.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/Settings.java @@ -46,7 +46,6 @@ import javax.annotation.Nonnull; public final class Settings implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = Settings.class.getSimpleName(); // Settings screens - public static final String SCREEN_ACCOUNTS = "screen_accounts"; public static final String SCREEN_THEME = "screen_theme"; public static final String SCREEN_DEBUG = "screen_debug"; public static final String SCREEN_GESTURE = "screen_gesture"; diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/SettingsFragment.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/SettingsFragment.java index 7256939b..b0cea4d5 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/SettingsFragment.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/SettingsFragment.java @@ -52,10 +52,6 @@ public final class SettingsFragment extends InputMethodSettingsFragment { final PreferenceScreen preferenceScreen = getPreferenceScreen(); preferenceScreen.setTitle( ApplicationUtils.getActivityTitleResId(getActivity(), SettingsActivity.class)); - if (!ProductionFlags.ENABLE_ACCOUNT_SIGN_IN) { - final Preference accountsPreference = findPreference(Settings.SCREEN_ACCOUNTS); - preferenceScreen.removePreference(accountsPreference); - } if (!JniUtils.sHaveGestureLib) { final Preference gesturePreference = findPreference(Settings.SCREEN_GESTURE); preferenceScreen.removePreference(gesturePreference); diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/DictionaryInfoUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/DictionaryInfoUtils.java index 883cbec6..2d83f47f 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/DictionaryInfoUtils.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/DictionaryInfoUtils.java @@ -25,7 +25,6 @@ import android.util.Log; import android.view.inputmethod.InputMethodSubtype; import org.dslul.openboard.inputmethod.annotations.UsedForTesting; -import org.dslul.openboard.inputmethod.dictionarypack.UpdateHandler; import org.dslul.openboard.inputmethod.latin.AssetFileAddress; import org.dslul.openboard.inputmethod.latin.BinaryDictionaryGetter; import org.dslul.openboard.inputmethod.latin.R; @@ -61,8 +60,6 @@ public class DictionaryInfoUtils { // 6 digits - unicode is limited to 21 bits private static final int MAX_HEX_DIGITS_FOR_CODEPOINT = 6; - private static final String TEMP_DICT_FILE_SUB = UpdateHandler.TEMP_DICT_FILE_SUB; - public static class DictionaryInfo { private static final String LOCALE_COLUMN = "locale"; private static final String WORDLISTID_COLUMN = "id"; @@ -207,17 +204,6 @@ public class DictionaryInfoUtils { return new File(DictionaryInfoUtils.getWordListStagingDirectory(context)).listFiles(); } - @Nullable - public static File[] getUnusedDictionaryList(final Context context) { - return context.getFilesDir().listFiles(new FilenameFilter() { - @Override - public boolean accept(File dir, String filename) { - return !TextUtils.isEmpty(filename) && filename.endsWith(".dict") - && filename.contains(TEMP_DICT_FILE_SUB); - } - }); - } - /** * Returns the category for a given file name. * @@ -253,75 +239,6 @@ public class DictionaryInfoUtils { return absoluteDirectoryName; } - /** - * Generates a file name for the id and locale passed as an argument. - * - * In the current implementation the file name returned will always be unique for - * any id/locale pair, but please do not expect that the id can be the same for - * different dictionaries with different locales. An id should be unique for any - * dictionary. - * The file name is pretty much an URL-encoded version of the id inside a directory - * named like the locale, except it will also escape characters that look dangerous - * to some file systems. - * @param id the id of the dictionary for which to get a file name - * @param locale the locale for which to get the file name as a string - * @param context the context to use for getting the directory - * @return the name of the file to be created - */ - public static String getCacheFileName(String id, String locale, Context context) { - final String fileName = replaceFileNameDangerousCharacters(id); - return getCacheDirectoryForLocale(locale, context) + File.separator + fileName; - } - - public static String getStagingFileName(String id, String locale, Context context) { - final String stagingDirectory = getWordListStagingDirectory(context); - // create the directory if it does not exist. - final File directory = new File(stagingDirectory); - if (!directory.exists()) { - if (!directory.mkdirs()) { - Log.e(TAG, "Could not create the staging directory."); - } - } - // e.g. id="main:en_in", locale ="en_IN" - final String fileName = replaceFileNameDangerousCharacters( - locale + TEMP_DICT_FILE_SUB + id); - return stagingDirectory + File.separator + fileName; - } - - public static void moveStagingFilesIfExists(Context context) { - final File[] stagingFiles = DictionaryInfoUtils.getStagingDirectoryList(context); - if (stagingFiles != null && stagingFiles.length > 0) { - for (final File stagingFile : stagingFiles) { - final String fileName = stagingFile.getName(); - final int index = fileName.indexOf(TEMP_DICT_FILE_SUB); - if (index == -1) { - // This should never happen. - Log.e(TAG, "Staging file does not have ___ substring."); - continue; - } - final String[] localeAndFileId = fileName.split(TEMP_DICT_FILE_SUB); - if (localeAndFileId.length != 2) { - Log.e(TAG, String.format("malformed staging file %s. Deleting.", - stagingFile.getAbsoluteFile())); - stagingFile.delete(); - continue; - } - - final String locale = localeAndFileId[0]; - // already escaped while moving to staging. - final String fileId = localeAndFileId[1]; - final String cacheDirectoryForLocale = getCacheDirectoryForLocale(locale, context); - final String cacheFilename = cacheDirectoryForLocale + File.separator + fileId; - final File cacheFile = new File(cacheFilename); - // move the staging file to cache file. - if (!FileUtils.renameTo(stagingFile, cacheFile)) { - Log.e(TAG, String.format("Failed to rename from %s to %s.", - stagingFile.getAbsoluteFile(), cacheFile.getAbsoluteFile())); - } - } - } - } - public static boolean isMainWordListId(final String id) { final String[] idArray = id.split(BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR); // An id is supposed to be in format category:locale, so splitting on the separator @@ -521,25 +438,6 @@ public class DictionaryInfoUtils { } } - // Retrieve downloaded dictionaries from the unused dictionaries. - File[] unusedDictionaryList = getUnusedDictionaryList(context); - if (unusedDictionaryList != null) { - for (File dictionaryFile : unusedDictionaryList) { - String fileName = dictionaryFile.getName(); - int index = fileName.indexOf(TEMP_DICT_FILE_SUB); - if (index == -1) { - continue; - } - String locale = fileName.substring(0, index); - DictionaryInfo dictionaryInfo = createDictionaryInfoForUnCachedFile( - AssetFileAddress.makeFromFile(dictionaryFile), - LocaleUtils.constructLocaleFromString(locale)); - if (dictionaryInfo != null) { - addOrUpdateDictInfo(dictList, dictionaryInfo); - } - } - } - // Retrieve files from assets final Resources resources = context.getResources(); final AssetManager assets = resources.getAssets(); diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/FragmentUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/FragmentUtils.java index 0a496539..9bfe5e51 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/FragmentUtils.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/FragmentUtils.java @@ -18,7 +18,6 @@ package org.dslul.openboard.inputmethod.latin.utils; import org.dslul.openboard.inputmethod.dictionarypack.DictionarySettingsFragment; import org.dslul.openboard.inputmethod.latin.about.AboutPreferences; -import org.dslul.openboard.inputmethod.latin.settings.AccountsSettingsFragment; import org.dslul.openboard.inputmethod.latin.settings.AdvancedSettingsFragment; import org.dslul.openboard.inputmethod.latin.settings.AppearanceSettingsFragment; import org.dslul.openboard.inputmethod.latin.settings.CorrectionSettingsFragment; @@ -42,7 +41,6 @@ public class FragmentUtils { sLatinImeFragments.add(DictionarySettingsFragment.class.getName()); sLatinImeFragments.add(AboutPreferences.class.getName()); sLatinImeFragments.add(PreferencesSettingsFragment.class.getName()); - sLatinImeFragments.add(AccountsSettingsFragment.class.getName()); sLatinImeFragments.add(AppearanceSettingsFragment.class.getName()); sLatinImeFragments.add(ThemeSettingsFragment.class.getName()); sLatinImeFragments.add(CustomInputStyleSettingsFragment.class.getName()); diff --git a/app/src/main/res/xml/prefs.xml b/app/src/main/res/xml/prefs.xml index 429e2342..6688c893 100644 --- a/app/src/main/res/xml/prefs.xml +++ b/app/src/main/res/xml/prefs.xml @@ -22,10 +22,6 @@ android:fragment="org.dslul.openboard.inputmethod.latin.settings.PreferencesSettingsFragment" android:title="@string/settings_screen_preferences" android:key="screen_preferences" /> - - - - - - - - - - - - - - - - - - - -