1/**
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy
6 * of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations
14 * under the License.
15 */
16
17package com.android.inputmethod.dictionarypack;
18
19import android.content.Context;
20import android.content.SharedPreferences;
21import android.preference.Preference;
22import android.util.Log;
23import android.view.View;
24import android.view.ViewGroup;
25import android.view.ViewParent;
26import android.widget.ListView;
27import android.widget.TextView;
28
29import com.android.inputmethod.latin.R;
30
31import java.util.Locale;
32
33/**
34 * A preference for one word list.
35 *
36 * This preference refers to a single word list, as available in the dictionary
37 * pack. Upon being pressed, it displays a menu to allow the user to install, disable,
38 * enable or delete it as appropriate for the current state of the word list.
39 */
40public final class WordListPreference extends Preference {
41    private static final String TAG = WordListPreference.class.getSimpleName();
42
43    // What to display in the "status" field when we receive unknown data as a status from
44    // the content provider. Empty string sounds sensible.
45    private static final String NO_STATUS_MESSAGE = "";
46
47    /// Actions
48    private static final int ACTION_UNKNOWN = 0;
49    private static final int ACTION_ENABLE_DICT = 1;
50    private static final int ACTION_DISABLE_DICT = 2;
51    private static final int ACTION_DELETE_DICT = 3;
52
53    // Members
54    // The metadata word list id and version of this word list.
55    public final String mWordlistId;
56    public final int mVersion;
57    public final Locale mLocale;
58    public final String mDescription;
59
60    // The id of the client for which this preference is.
61    private final String mClientId;
62    // The status
63    private int mStatus;
64    // The size of the dictionary file
65    private final int mFilesize;
66
67    private final DictionaryListInterfaceState mInterfaceState;
68
69    public WordListPreference(final Context context,
70            final DictionaryListInterfaceState dictionaryListInterfaceState, final String clientId,
71            final String wordlistId, final int version, final Locale locale,
72            final String description, final int status, final int filesize) {
73        super(context, null);
74        mInterfaceState = dictionaryListInterfaceState;
75        mClientId = clientId;
76        mVersion = version;
77        mWordlistId = wordlistId;
78        mFilesize = filesize;
79        mLocale = locale;
80        mDescription = description;
81
82        setLayoutResource(R.layout.dictionary_line);
83
84        setTitle(description);
85        setStatus(status);
86        setKey(wordlistId);
87    }
88
89    public void setStatus(final int status) {
90        if (status == mStatus) return;
91        mStatus = status;
92        setSummary(getSummary(status));
93    }
94
95    public boolean hasStatus(final int status) {
96        return status == mStatus;
97    }
98
99    @Override
100    public View onCreateView(final ViewGroup parent) {
101        final View orphanedView = mInterfaceState.findFirstOrphanedView();
102        if (null != orphanedView) return orphanedView; // Will be sent to onBindView
103        final View newView = super.onCreateView(parent);
104        return mInterfaceState.addToCacheAndReturnView(newView);
105    }
106
107    public boolean hasPriorityOver(final int otherPrefStatus) {
108        // Both of these should be one of MetadataDbHelper.STATUS_*
109        return mStatus > otherPrefStatus;
110    }
111
112    private String getSummary(final int status) {
113        final Context context = getContext();
114        switch (status) {
115        // If we are deleting the word list, for the user it's like it's already deleted.
116        // It should be reinstallable. Exposing to the user the whole complexity of
117        // the delayed deletion process between the dictionary pack and Android Keyboard
118        // would only be confusing.
119        case MetadataDbHelper.STATUS_DELETING:
120        case MetadataDbHelper.STATUS_AVAILABLE:
121            return context.getString(R.string.dictionary_available);
122        case MetadataDbHelper.STATUS_DOWNLOADING:
123            return context.getString(R.string.dictionary_downloading);
124        case MetadataDbHelper.STATUS_INSTALLED:
125            return context.getString(R.string.dictionary_installed);
126        case MetadataDbHelper.STATUS_DISABLED:
127            return context.getString(R.string.dictionary_disabled);
128        default:
129            return NO_STATUS_MESSAGE;
130        }
131    }
132
133    // The table below needs to be kept in sync with MetadataDbHelper.STATUS_* since it uses
134    // the values as indices.
135    private static final int sStatusActionList[][] = {
136        // MetadataDbHelper.STATUS_UNKNOWN
137        {},
138        // MetadataDbHelper.STATUS_AVAILABLE
139        { ButtonSwitcher.STATUS_INSTALL, ACTION_ENABLE_DICT },
140        // MetadataDbHelper.STATUS_DOWNLOADING
141        { ButtonSwitcher.STATUS_CANCEL, ACTION_DISABLE_DICT },
142        // MetadataDbHelper.STATUS_INSTALLED
143        { ButtonSwitcher.STATUS_DELETE, ACTION_DELETE_DICT },
144        // MetadataDbHelper.STATUS_DISABLED
145        { ButtonSwitcher.STATUS_DELETE, ACTION_DELETE_DICT },
146        // MetadataDbHelper.STATUS_DELETING
147        // We show 'install' because the file is supposed to be deleted.
148        // The user may reinstall it.
149        { ButtonSwitcher.STATUS_INSTALL, ACTION_ENABLE_DICT }
150    };
151
152    static int getButtonSwitcherStatus(final int status) {
153        if (status >= sStatusActionList.length) {
154            Log.e(TAG, "Unknown status " + status);
155            return ButtonSwitcher.STATUS_NO_BUTTON;
156        }
157        return sStatusActionList[status][0];
158    }
159
160    static int getActionIdFromStatusAndMenuEntry(final int status) {
161        if (status >= sStatusActionList.length) {
162            Log.e(TAG, "Unknown status " + status);
163            return ACTION_UNKNOWN;
164        }
165        return sStatusActionList[status][1];
166    }
167
168    private void disableDict() {
169        final Context context = getContext();
170        final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context);
171        CommonPreferences.disable(prefs, mWordlistId);
172        UpdateHandler.markAsUnused(context, mClientId, mWordlistId, mVersion, mStatus);
173        if (MetadataDbHelper.STATUS_DOWNLOADING == mStatus) {
174            setStatus(MetadataDbHelper.STATUS_AVAILABLE);
175        } else if (MetadataDbHelper.STATUS_INSTALLED == mStatus) {
176            // Interface-wise, we should no longer be able to come here. However, this is still
177            // the right thing to do if we do come here.
178            setStatus(MetadataDbHelper.STATUS_DISABLED);
179        } else {
180            Log.e(TAG, "Unexpected state of the word list for disabling " + mStatus);
181        }
182    }
183
184    private void enableDict() {
185        final Context context = getContext();
186        final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context);
187        CommonPreferences.enable(prefs, mWordlistId);
188        // Explicit enabling by the user : allow downloading on metered data connection.
189        UpdateHandler.markAsUsed(context, mClientId, mWordlistId, mVersion, mStatus, true);
190        if (MetadataDbHelper.STATUS_AVAILABLE == mStatus) {
191            setStatus(MetadataDbHelper.STATUS_DOWNLOADING);
192        } else if (MetadataDbHelper.STATUS_DISABLED == mStatus
193                || MetadataDbHelper.STATUS_DELETING == mStatus) {
194            // If the status is DELETING, it means Android Keyboard
195            // has not deleted the word list yet, so we can safely
196            // turn it to 'installed'. The status DISABLED is still supported internally to
197            // avoid breaking older installations and all but there should not be a way to
198            // disable a word list through the interface any more.
199            setStatus(MetadataDbHelper.STATUS_INSTALLED);
200        } else {
201            Log.e(TAG, "Unexpected state of the word list for enabling " + mStatus);
202        }
203    }
204
205    private void deleteDict() {
206        final Context context = getContext();
207        final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context);
208        CommonPreferences.disable(prefs, mWordlistId);
209        setStatus(MetadataDbHelper.STATUS_DELETING);
210        UpdateHandler.markAsDeleting(context, mClientId, mWordlistId, mVersion, mStatus);
211    }
212
213    @Override
214    protected void onBindView(final View view) {
215        super.onBindView(view);
216        ((ViewGroup)view).setLayoutTransition(null);
217
218        final DictionaryDownloadProgressBar progressBar =
219                (DictionaryDownloadProgressBar)view.findViewById(R.id.dictionary_line_progress_bar);
220        final TextView status = (TextView)view.findViewById(android.R.id.summary);
221        progressBar.setIds(mClientId, mWordlistId);
222        progressBar.setMax(mFilesize);
223        final boolean showProgressBar = (MetadataDbHelper.STATUS_DOWNLOADING == mStatus);
224        setSummary(getSummary(mStatus));
225        status.setVisibility(showProgressBar ? View.INVISIBLE : View.VISIBLE);
226        progressBar.setVisibility(showProgressBar ? View.VISIBLE : View.INVISIBLE);
227
228        final ButtonSwitcher buttonSwitcher = (ButtonSwitcher)view.findViewById(
229                R.id.wordlist_button_switcher);
230        // We need to clear the state of the button switcher, because we reuse views; if we didn't
231        // reset it would animate from whatever its old state was.
232        buttonSwitcher.reset(mInterfaceState);
233        if (mInterfaceState.isOpen(mWordlistId)) {
234            // The button is open.
235            final int previousStatus = mInterfaceState.getStatus(mWordlistId);
236            buttonSwitcher.setStatusAndUpdateVisuals(getButtonSwitcherStatus(previousStatus));
237            if (previousStatus != mStatus) {
238                // We come here if the status has changed since last time. We need to animate
239                // the transition.
240                buttonSwitcher.setStatusAndUpdateVisuals(getButtonSwitcherStatus(mStatus));
241                mInterfaceState.setOpen(mWordlistId, mStatus);
242            }
243        } else {
244            // The button is closed.
245            buttonSwitcher.setStatusAndUpdateVisuals(ButtonSwitcher.STATUS_NO_BUTTON);
246        }
247        buttonSwitcher.setInternalOnClickListener(new View.OnClickListener() {
248            @Override
249            public void onClick(final View v) {
250                onActionButtonClicked();
251            }
252        });
253        view.setOnClickListener(new View.OnClickListener() {
254            @Override
255            public void onClick(final View v) {
256                onWordListClicked(v);
257            }
258        });
259    }
260
261    void onWordListClicked(final View v) {
262        // Note : v is the preference view
263        final ViewParent parent = v.getParent();
264        // Just in case something changed in the framework, test for the concrete class
265        if (!(parent instanceof ListView)) return;
266        final ListView listView = (ListView)parent;
267        final int indexToOpen;
268        // Close all first, we'll open back any item that needs to be open.
269        final boolean wasOpen = mInterfaceState.isOpen(mWordlistId);
270        mInterfaceState.closeAll();
271        if (wasOpen) {
272            // This button being shown. Take note that we don't want to open any button in the
273            // loop below.
274            indexToOpen = -1;
275        } else {
276            // This button was not being shown. Open it, and remember the index of this
277            // child as the one to open in the following loop.
278            mInterfaceState.setOpen(mWordlistId, mStatus);
279            indexToOpen = listView.indexOfChild(v);
280        }
281        final int lastDisplayedIndex =
282                listView.getLastVisiblePosition() - listView.getFirstVisiblePosition();
283        // The "lastDisplayedIndex" is actually displayed, hence the <=
284        for (int i = 0; i <= lastDisplayedIndex; ++i) {
285            final ButtonSwitcher buttonSwitcher = (ButtonSwitcher)listView.getChildAt(i)
286                    .findViewById(R.id.wordlist_button_switcher);
287            if (i == indexToOpen) {
288                buttonSwitcher.setStatusAndUpdateVisuals(getButtonSwitcherStatus(mStatus));
289            } else {
290                buttonSwitcher.setStatusAndUpdateVisuals(ButtonSwitcher.STATUS_NO_BUTTON);
291            }
292        }
293    }
294
295    void onActionButtonClicked() {
296        switch (getActionIdFromStatusAndMenuEntry(mStatus)) {
297        case ACTION_ENABLE_DICT:
298            enableDict();
299            break;
300        case ACTION_DISABLE_DICT:
301            disableDict();
302            break;
303        case ACTION_DELETE_DICT:
304            deleteDict();
305            break;
306        default:
307            Log.e(TAG, "Unknown menu item pressed");
308        }
309    }
310}
311