1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy 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,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.quicklaunch;
18 
19 import android.app.Activity;
20 import android.app.AlertDialog;
21 import android.app.Dialog;
22 import android.content.DialogInterface;
23 import android.content.Intent;
24 import android.content.pm.PackageManager;
25 import android.content.pm.ResolveInfo;
26 import android.database.ContentObserver;
27 import android.database.Cursor;
28 import android.os.Bundle;
29 import android.os.Handler;
30 import android.preference.Preference;
31 import android.preference.PreferenceGroup;
32 import android.preference.PreferenceScreen;
33 import android.provider.Settings.Bookmarks;
34 import android.util.Log;
35 import android.util.SparseArray;
36 import android.util.SparseBooleanArray;
37 import android.view.KeyCharacterMap;
38 import android.view.KeyEvent;
39 import android.view.View;
40 import android.widget.AdapterView;
41 
42 import com.android.settings.R;
43 import com.android.settings.SettingsPreferenceFragment;
44 
45 import java.net.URISyntaxException;
46 
47 /**
48  * Settings activity for quick launch.
49  * <p>
50  * Shows a list of possible shortcuts, the current application each is bound to,
51  * and allows choosing a new bookmark for a shortcut.
52  */
53 public class QuickLaunchSettings extends SettingsPreferenceFragment implements
54         AdapterView.OnItemLongClickListener, DialogInterface.OnClickListener {
55 
56     private static final String TAG = "QuickLaunchSettings";
57 
58     private static final String KEY_SHORTCUT_CATEGORY = "shortcut_category";
59 
60     private static final int DIALOG_CLEAR_SHORTCUT = 0;
61 
62     private static final int REQUEST_PICK_BOOKMARK = 1;
63 
64     private static final int COLUMN_SHORTCUT = 0;
65     private static final int COLUMN_TITLE = 1;
66     private static final int COLUMN_INTENT = 2;
67     private static final String[] sProjection = new String[] {
68             Bookmarks.SHORTCUT, Bookmarks.TITLE, Bookmarks.INTENT
69     };
70     private static final String sShortcutSelection = Bookmarks.SHORTCUT + "=?";
71 
72     private Handler mUiHandler = new Handler();
73 
74     private static final String DEFAULT_BOOKMARK_FOLDER = "@quicklaunch";
75     /** Cursor for Bookmarks provider. */
76     private Cursor mBookmarksCursor;
77     /** Listens for changes to Bookmarks provider. */
78     private BookmarksObserver mBookmarksObserver;
79     /** Used to keep track of which shortcuts have bookmarks. */
80     private SparseBooleanArray mBookmarkedShortcuts;
81 
82     /** Preference category to hold the shortcut preferences. */
83     private PreferenceGroup mShortcutGroup;
84     /** Mapping of a shortcut to its preference. */
85     private SparseArray<ShortcutPreference> mShortcutToPreference;
86 
87     /** The bookmark title of the shortcut that is being cleared. */
88     private CharSequence mClearDialogBookmarkTitle;
89     private static final String CLEAR_DIALOG_BOOKMARK_TITLE = "CLEAR_DIALOG_BOOKMARK_TITLE";
90     /** The shortcut that is being cleared. */
91     private char mClearDialogShortcut;
92     private static final String CLEAR_DIALOG_SHORTCUT = "CLEAR_DIALOG_SHORTCUT";
93 
94     @Override
onCreate(Bundle savedInstanceState)95     public void onCreate(Bundle savedInstanceState) {
96         super.onCreate(savedInstanceState);
97 
98         addPreferencesFromResource(R.xml.quick_launch_settings);
99 
100         mShortcutGroup = (PreferenceGroup) findPreference(KEY_SHORTCUT_CATEGORY);
101         mShortcutToPreference = new SparseArray<ShortcutPreference>();
102         mBookmarksObserver = new BookmarksObserver(mUiHandler);
103         initShortcutPreferences();
104         mBookmarksCursor = getActivity().getContentResolver().query(Bookmarks.CONTENT_URI,
105                 sProjection, null, null, null);
106     }
107 
108     @Override
onResume()109     public void onResume() {
110         super.onResume();
111         mBookmarksCursor = getActivity().getContentResolver().query(Bookmarks.CONTENT_URI,
112                 sProjection, null, null, null);
113         getContentResolver().registerContentObserver(Bookmarks.CONTENT_URI, true,
114                 mBookmarksObserver);
115         refreshShortcuts();
116     }
117 
118     @Override
onPause()119     public void onPause() {
120         super.onPause();
121         getContentResolver().unregisterContentObserver(mBookmarksObserver);
122     }
123 
124     @Override
onStop()125     public void onStop() {
126         super.onStop();
127         mBookmarksCursor.close();
128     }
129 
130     @Override
onActivityCreated(Bundle state)131     public void onActivityCreated(Bundle state) {
132         super.onActivityCreated(state);
133 
134         getListView().setOnItemLongClickListener(this);
135 
136         if (state != null) {
137             // Restore the clear dialog's info
138             mClearDialogBookmarkTitle = state.getString(CLEAR_DIALOG_BOOKMARK_TITLE);
139             mClearDialogShortcut = (char) state.getInt(CLEAR_DIALOG_SHORTCUT, 0);
140         }
141     }
142 
143     @Override
onSaveInstanceState(Bundle outState)144     public void onSaveInstanceState(Bundle outState) {
145         super.onSaveInstanceState(outState);
146 
147         // Save the clear dialog's info
148         outState.putCharSequence(CLEAR_DIALOG_BOOKMARK_TITLE, mClearDialogBookmarkTitle);
149         outState.putInt(CLEAR_DIALOG_SHORTCUT, mClearDialogShortcut);
150     }
151 
152     @Override
onCreateDialog(int id)153     public Dialog onCreateDialog(int id) {
154         switch (id) {
155 
156             case DIALOG_CLEAR_SHORTCUT: {
157                 // Create the dialog for clearing a shortcut
158                 return new AlertDialog.Builder(getActivity())
159                         .setTitle(getString(R.string.quick_launch_clear_dialog_title))
160                         .setMessage(getString(R.string.quick_launch_clear_dialog_message,
161                                 mClearDialogShortcut, mClearDialogBookmarkTitle))
162                         .setPositiveButton(R.string.quick_launch_clear_ok_button, this)
163                         .setNegativeButton(R.string.quick_launch_clear_cancel_button, this)
164                         .create();
165             }
166         }
167 
168         return super.onCreateDialog(id);
169     }
170 
showClearDialog(ShortcutPreference pref)171     private void showClearDialog(ShortcutPreference pref) {
172 
173         if (!pref.hasBookmark()) return;
174 
175         mClearDialogBookmarkTitle = pref.getTitle();
176         mClearDialogShortcut = pref.getShortcut();
177         showDialog(DIALOG_CLEAR_SHORTCUT);
178     }
179 
onClick(DialogInterface dialog, int which)180     public void onClick(DialogInterface dialog, int which) {
181         if (mClearDialogShortcut > 0 && which == AlertDialog.BUTTON_POSITIVE) {
182             // Clear the shortcut
183             clearShortcut(mClearDialogShortcut);
184         }
185         mClearDialogBookmarkTitle = null;
186         mClearDialogShortcut = 0;
187     }
188 
clearShortcut(char shortcut)189     private void clearShortcut(char shortcut) {
190         getContentResolver().delete(Bookmarks.CONTENT_URI, sShortcutSelection,
191                 new String[] { String.valueOf((int) shortcut) });
192     }
193 
194     @Override
onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference)195     public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
196         if (!(preference instanceof ShortcutPreference)) return false;
197 
198         // Open the screen to pick a bookmark for this shortcut
199         ShortcutPreference pref = (ShortcutPreference) preference;
200         Intent intent = new Intent(getActivity(), BookmarkPicker.class);
201         intent.putExtra(BookmarkPicker.EXTRA_SHORTCUT, pref.getShortcut());
202         startActivityForResult(intent, REQUEST_PICK_BOOKMARK);
203 
204         return true;
205     }
206 
onItemLongClick(AdapterView<?> parent, View view, int position, long id)207     public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
208 
209         // Open the clear shortcut dialog
210         Preference pref = (Preference) getPreferenceScreen().getRootAdapter().getItem(position);
211         if (!(pref instanceof ShortcutPreference)) return false;
212         showClearDialog((ShortcutPreference) pref);
213         return true;
214     }
215 
216     @Override
onActivityResult(int requestCode, int resultCode, Intent data)217     public void onActivityResult(int requestCode, int resultCode, Intent data) {
218         if (resultCode != Activity.RESULT_OK) {
219             return;
220         }
221 
222         if (requestCode == REQUEST_PICK_BOOKMARK) {
223 
224             // Returned from the 'pick bookmark for this shortcut' screen
225             if (data == null) {
226                 Log.w(TAG, "Result from bookmark picker does not have an intent.");
227                 return;
228             }
229 
230             char shortcut = data.getCharExtra(BookmarkPicker.EXTRA_SHORTCUT, (char) 0);
231             updateShortcut(shortcut, data);
232 
233         } else {
234             super.onActivityResult(requestCode, resultCode, data);
235         }
236     }
237 
updateShortcut(char shortcut, Intent intent)238     private void updateShortcut(char shortcut, Intent intent) {
239         // Update the bookmark for a shortcut
240         // Pass an empty title so it gets resolved each time this bookmark is
241         // displayed (since the locale could change after we insert into the provider).
242         Bookmarks.add(getContentResolver(), intent, "", DEFAULT_BOOKMARK_FOLDER, shortcut, 0);
243     }
244 
getOrCreatePreference(char shortcut)245     private ShortcutPreference getOrCreatePreference(char shortcut) {
246         ShortcutPreference pref = mShortcutToPreference.get(shortcut);
247         if (pref != null) {
248             return pref;
249         } else {
250             Log.w(TAG, "Unknown shortcut '" + shortcut + "', creating preference anyway");
251             return createPreference(shortcut);
252         }
253     }
254 
createPreference(char shortcut)255     private ShortcutPreference createPreference(char shortcut) {
256         ShortcutPreference pref = new ShortcutPreference(getActivity(), shortcut);
257         mShortcutGroup.addPreference(pref);
258         mShortcutToPreference.put(shortcut, pref);
259         return pref;
260     }
261 
initShortcutPreferences()262     private void initShortcutPreferences() {
263 
264         /** Whether the shortcut has been seen already.  The array index is the shortcut. */
265         SparseBooleanArray shortcutSeen = new SparseBooleanArray();
266         KeyCharacterMap keyMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
267 
268         // Go through all the key codes and create a preference for the appropriate keys
269         for (int keyCode = KeyEvent.getMaxKeyCode() - 1; keyCode >= 0; keyCode--) {
270             // Get the label for the primary char on the key that produces this key code
271             char shortcut = (char) Character.toLowerCase(keyMap.getDisplayLabel(keyCode));
272             if (shortcut == 0 || shortcutSeen.get(shortcut, false)) continue;
273             // TODO: need a to tell if the current keyboard can produce this key code, for now
274             // only allow the letter or digits
275             if (!Character.isLetterOrDigit(shortcut)) continue;
276             shortcutSeen.put(shortcut, true);
277 
278             createPreference(shortcut);
279         }
280     }
281 
refreshShortcuts()282     private synchronized void refreshShortcuts() {
283         Cursor c = mBookmarksCursor;
284         if (c == null) {
285             // Haven't finished querying yet
286             return;
287         }
288 
289         if (!c.requery()) {
290             Log.e(TAG, "Could not requery cursor when refreshing shortcuts.");
291             return;
292         }
293 
294         /**
295          * We use the previous bookmarked shortcuts array to filter out those
296          * shortcuts that had bookmarks before this method call, and don't after
297          * (so we can set the preferences to be without bookmarks).
298          */
299         SparseBooleanArray noLongerBookmarkedShortcuts = mBookmarkedShortcuts;
300         SparseBooleanArray newBookmarkedShortcuts = new SparseBooleanArray();
301         while (c.moveToNext()) {
302             char shortcut = Character.toLowerCase((char) c.getInt(COLUMN_SHORTCUT));
303             if (shortcut == 0) continue;
304 
305             ShortcutPreference pref = getOrCreatePreference(shortcut);
306             CharSequence title = Bookmarks.getTitle(getActivity(), c);
307 
308             /*
309              * The title retrieved from Bookmarks.getTitle() will be in
310              * the original boot locale, not the current locale.
311              * Try to look up a localized title from the PackageManager.
312              */
313             int intentColumn = c.getColumnIndex(Bookmarks.INTENT);
314             String intentUri = c.getString(intentColumn);
315             PackageManager packageManager = getPackageManager();
316             try {
317                 Intent intent = Intent.parseUri(intentUri, 0);
318                 ResolveInfo info = packageManager.resolveActivity(intent, 0);
319                 if (info != null) {
320                     title = info.loadLabel(packageManager);
321                 }
322             } catch (URISyntaxException e) {
323                 // Just use the non-localized title, then.
324             }
325 
326             pref.setTitle(title);
327             pref.setSummary(getString(R.string.quick_launch_shortcut,
328                     String.valueOf(shortcut)));
329             pref.setHasBookmark(true);
330 
331             newBookmarkedShortcuts.put(shortcut, true);
332             if (noLongerBookmarkedShortcuts != null) {
333                 // After this loop, the shortcuts with value true in this array
334                 // will no longer have bookmarks
335                 noLongerBookmarkedShortcuts.put(shortcut, false);
336             }
337         }
338 
339         if (noLongerBookmarkedShortcuts != null) {
340             for (int i = noLongerBookmarkedShortcuts.size() - 1; i >= 0; i--) {
341                 if (noLongerBookmarkedShortcuts.valueAt(i)) {
342                     // True, so there is no longer a bookmark for this shortcut
343                     char shortcut = (char) noLongerBookmarkedShortcuts.keyAt(i);
344                     ShortcutPreference pref = mShortcutToPreference.get(shortcut);
345                     if (pref != null) {
346                         pref.setHasBookmark(false);
347                     }
348                 }
349             }
350         }
351 
352         mBookmarkedShortcuts = newBookmarkedShortcuts;
353 
354         c.deactivate();
355     }
356 
357     private class BookmarksObserver extends ContentObserver {
358 
BookmarksObserver(Handler handler)359         public BookmarksObserver(Handler handler) {
360             super(handler);
361         }
362 
363         @Override
onChange(boolean selfChange)364         public void onChange(boolean selfChange) {
365             super.onChange(selfChange);
366 
367             refreshShortcuts();
368         }
369     }
370 }
371