|
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- |
|
2 * This Source Code Form is subject to the terms of the Mozilla Public |
|
3 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
5 |
|
6 package org.mozilla.gecko.home; |
|
7 |
|
8 import org.mozilla.gecko.EditBookmarkDialog; |
|
9 import org.mozilla.gecko.GeckoAppShell; |
|
10 import org.mozilla.gecko.GeckoEvent; |
|
11 import org.mozilla.gecko.GeckoProfile; |
|
12 import org.mozilla.gecko.R; |
|
13 import org.mozilla.gecko.ReaderModeUtils; |
|
14 import org.mozilla.gecko.Tabs; |
|
15 import org.mozilla.gecko.Telemetry; |
|
16 import org.mozilla.gecko.TelemetryContract; |
|
17 import org.mozilla.gecko.db.BrowserContract.Combined; |
|
18 import org.mozilla.gecko.db.BrowserDB; |
|
19 import org.mozilla.gecko.favicons.Favicons; |
|
20 import org.mozilla.gecko.util.ThreadUtils; |
|
21 import org.mozilla.gecko.util.UiAsyncTask; |
|
22 |
|
23 import android.content.ContentResolver; |
|
24 import android.content.Context; |
|
25 import android.content.Intent; |
|
26 import android.content.res.Configuration; |
|
27 import android.net.Uri; |
|
28 import android.os.Bundle; |
|
29 import android.support.v4.app.Fragment; |
|
30 import android.util.Log; |
|
31 import android.view.ContextMenu; |
|
32 import android.view.ContextMenu.ContextMenuInfo; |
|
33 import android.view.MenuInflater; |
|
34 import android.view.MenuItem; |
|
35 import android.view.View; |
|
36 import android.widget.Toast; |
|
37 |
|
38 /** |
|
39 * HomeFragment is an empty fragment that can be added to the HomePager. |
|
40 * Subclasses can add their own views. |
|
41 */ |
|
42 abstract class HomeFragment extends Fragment { |
|
43 // Log Tag. |
|
44 private static final String LOGTAG="GeckoHomeFragment"; |
|
45 |
|
46 // Share MIME type. |
|
47 protected static final String SHARE_MIME_TYPE = "text/plain"; |
|
48 |
|
49 // Default value for "can load" hint |
|
50 static final boolean DEFAULT_CAN_LOAD_HINT = false; |
|
51 |
|
52 // Whether the fragment can load its content or not |
|
53 // This is used to defer data loading until the editing |
|
54 // mode animation ends. |
|
55 private boolean mCanLoadHint; |
|
56 |
|
57 // Whether the fragment has loaded its content |
|
58 private boolean mIsLoaded; |
|
59 |
|
60 @Override |
|
61 public void onCreate(Bundle savedInstanceState) { |
|
62 super.onCreate(savedInstanceState); |
|
63 |
|
64 final Bundle args = getArguments(); |
|
65 if (args != null) { |
|
66 mCanLoadHint = args.getBoolean(HomePager.CAN_LOAD_ARG, DEFAULT_CAN_LOAD_HINT); |
|
67 } else { |
|
68 mCanLoadHint = DEFAULT_CAN_LOAD_HINT; |
|
69 } |
|
70 |
|
71 mIsLoaded = false; |
|
72 } |
|
73 |
|
74 @Override |
|
75 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { |
|
76 if (menuInfo == null || !(menuInfo instanceof HomeContextMenuInfo)) { |
|
77 return; |
|
78 } |
|
79 |
|
80 HomeContextMenuInfo info = (HomeContextMenuInfo) menuInfo; |
|
81 |
|
82 // Don't show the context menu for folders. |
|
83 if (info.isFolder) { |
|
84 return; |
|
85 } |
|
86 |
|
87 MenuInflater inflater = new MenuInflater(view.getContext()); |
|
88 inflater.inflate(R.menu.home_contextmenu, menu); |
|
89 |
|
90 menu.setHeaderTitle(info.getDisplayTitle()); |
|
91 |
|
92 // Hide ununsed menu items. |
|
93 menu.findItem(R.id.top_sites_edit).setVisible(false); |
|
94 menu.findItem(R.id.top_sites_pin).setVisible(false); |
|
95 menu.findItem(R.id.top_sites_unpin).setVisible(false); |
|
96 |
|
97 // Hide the "Edit" menuitem if this item isn't a bookmark, |
|
98 // or if this is a reading list item. |
|
99 if (!info.hasBookmarkId() || info.isInReadingList()) { |
|
100 menu.findItem(R.id.home_edit_bookmark).setVisible(false); |
|
101 } |
|
102 |
|
103 // Hide the "Remove" menuitem if this item not removable. |
|
104 if (!info.canRemove()) { |
|
105 menu.findItem(R.id.home_remove).setVisible(false); |
|
106 } |
|
107 |
|
108 menu.findItem(R.id.home_share).setVisible(!GeckoProfile.get(getActivity()).inGuestMode()); |
|
109 |
|
110 final boolean canOpenInReader = (info.display == Combined.DISPLAY_READER); |
|
111 menu.findItem(R.id.home_open_in_reader).setVisible(canOpenInReader); |
|
112 } |
|
113 |
|
114 @Override |
|
115 public boolean onContextItemSelected(MenuItem item) { |
|
116 // onContextItemSelected() is first dispatched to the activity and |
|
117 // then dispatched to its fragments. Since fragments cannot "override" |
|
118 // menu item selection handling, it's better to avoid menu id collisions |
|
119 // between the activity and its fragments. |
|
120 |
|
121 ContextMenuInfo menuInfo = item.getMenuInfo(); |
|
122 if (menuInfo == null || !(menuInfo instanceof HomeContextMenuInfo)) { |
|
123 return false; |
|
124 } |
|
125 |
|
126 final HomeContextMenuInfo info = (HomeContextMenuInfo) menuInfo; |
|
127 final Context context = getActivity(); |
|
128 |
|
129 final int itemId = item.getItemId(); |
|
130 |
|
131 // Track the menu action. We don't know much about the context, but we can use this to determine |
|
132 // the frequency of use for various actions. |
|
133 Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, getResources().getResourceEntryName(itemId)); |
|
134 |
|
135 if (itemId == R.id.home_share) { |
|
136 if (info.url == null) { |
|
137 Log.e(LOGTAG, "Can't share because URL is null"); |
|
138 return false; |
|
139 } else { |
|
140 GeckoAppShell.openUriExternal(info.url, SHARE_MIME_TYPE, "", "", |
|
141 Intent.ACTION_SEND, info.getDisplayTitle()); |
|
142 |
|
143 // Context: Sharing via chrome homepage contextmenu list (home session should be active) |
|
144 Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST); |
|
145 return true; |
|
146 } |
|
147 } |
|
148 |
|
149 if (itemId == R.id.home_add_to_launcher) { |
|
150 if (info.url == null) { |
|
151 Log.e(LOGTAG, "Can't add to home screen because URL is null"); |
|
152 return false; |
|
153 } |
|
154 |
|
155 // Fetch an icon big enough for use as a home screen icon. |
|
156 Favicons.getPreferredSizeFaviconForPage(info.url, new GeckoAppShell.CreateShortcutFaviconLoadedListener(info.url, info.getDisplayTitle())); |
|
157 return true; |
|
158 } |
|
159 |
|
160 if (itemId == R.id.home_open_private_tab || itemId == R.id.home_open_new_tab) { |
|
161 if (info.url == null) { |
|
162 Log.e(LOGTAG, "Can't open in new tab because URL is null"); |
|
163 return false; |
|
164 } |
|
165 |
|
166 int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_BACKGROUND; |
|
167 if (item.getItemId() == R.id.home_open_private_tab) |
|
168 flags |= Tabs.LOADURL_PRIVATE; |
|
169 |
|
170 Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.CONTEXT_MENU); |
|
171 |
|
172 final String url = (info.isInReadingList() ? ReaderModeUtils.getAboutReaderForUrl(info.url) : info.url); |
|
173 |
|
174 // Some pinned site items have "user-entered" urls. URLs entered in the PinSiteDialog are wrapped in |
|
175 // a special URI until we can get a valid URL. If the url is a user-entered url, decode the URL before loading it. |
|
176 Tabs.getInstance().loadUrl(decodeUserEnteredUrl(url), flags); |
|
177 Toast.makeText(context, R.string.new_tab_opened, Toast.LENGTH_SHORT).show(); |
|
178 return true; |
|
179 } |
|
180 |
|
181 if (itemId == R.id.home_edit_bookmark) { |
|
182 // UI Dialog associates to the activity context, not the applications'. |
|
183 new EditBookmarkDialog(context).show(info.url); |
|
184 return true; |
|
185 } |
|
186 |
|
187 if (itemId == R.id.home_open_in_reader) { |
|
188 final String url = ReaderModeUtils.getAboutReaderForUrl(info.url); |
|
189 Tabs.getInstance().loadUrl(url, Tabs.LOADURL_NONE); |
|
190 return true; |
|
191 } |
|
192 |
|
193 if (itemId == R.id.home_remove) { |
|
194 // Prioritize removing a history entry over a bookmark in the case of a combined item. |
|
195 if (info.hasHistoryId()) { |
|
196 new RemoveHistoryTask(context, info.historyId).execute(); |
|
197 return true; |
|
198 } |
|
199 |
|
200 if (info.hasBookmarkId()) { |
|
201 new RemoveBookmarkTask(context, info.bookmarkId).execute(); |
|
202 return true; |
|
203 } |
|
204 |
|
205 if (info.isInReadingList()) { |
|
206 (new RemoveReadingListItemTask(context, info.readingListItemId, info.url)).execute(); |
|
207 return true; |
|
208 } |
|
209 } |
|
210 |
|
211 return false; |
|
212 } |
|
213 |
|
214 @Override |
|
215 public void setUserVisibleHint (boolean isVisibleToUser) { |
|
216 if (isVisibleToUser == getUserVisibleHint()) { |
|
217 return; |
|
218 } |
|
219 |
|
220 super.setUserVisibleHint(isVisibleToUser); |
|
221 loadIfVisible(); |
|
222 } |
|
223 |
|
224 @Override |
|
225 public void onConfigurationChanged(Configuration newConfig) { |
|
226 super.onConfigurationChanged(newConfig); |
|
227 } |
|
228 |
|
229 void setCanLoadHint(boolean canLoadHint) { |
|
230 if (mCanLoadHint == canLoadHint) { |
|
231 return; |
|
232 } |
|
233 |
|
234 mCanLoadHint = canLoadHint; |
|
235 loadIfVisible(); |
|
236 } |
|
237 |
|
238 boolean getCanLoadHint() { |
|
239 return mCanLoadHint; |
|
240 } |
|
241 |
|
242 /** |
|
243 * Given a url with a user-entered scheme, extract the |
|
244 * scheme-specific component. For e.g, given "user-entered://www.google.com", |
|
245 * this method returns "//www.google.com". If the passed url |
|
246 * does not have a user-entered scheme, the same url will be returned. |
|
247 * |
|
248 * @param url to be decoded |
|
249 * @return url component entered by user |
|
250 */ |
|
251 public static String decodeUserEnteredUrl(String url) { |
|
252 Uri uri = Uri.parse(url); |
|
253 if ("user-entered".equals(uri.getScheme())) { |
|
254 return uri.getSchemeSpecificPart(); |
|
255 } |
|
256 return url; |
|
257 } |
|
258 |
|
259 protected abstract void load(); |
|
260 |
|
261 protected boolean canLoad() { |
|
262 return (mCanLoadHint && isVisible() && getUserVisibleHint()); |
|
263 } |
|
264 |
|
265 protected void loadIfVisible() { |
|
266 if (!canLoad() || mIsLoaded) { |
|
267 return; |
|
268 } |
|
269 |
|
270 load(); |
|
271 mIsLoaded = true; |
|
272 } |
|
273 |
|
274 private static class RemoveBookmarkTask extends UiAsyncTask<Void, Void, Void> { |
|
275 private final Context mContext; |
|
276 private final int mId; |
|
277 |
|
278 public RemoveBookmarkTask(Context context, int id) { |
|
279 super(ThreadUtils.getBackgroundHandler()); |
|
280 |
|
281 mContext = context; |
|
282 mId = id; |
|
283 } |
|
284 |
|
285 @Override |
|
286 public Void doInBackground(Void... params) { |
|
287 ContentResolver cr = mContext.getContentResolver(); |
|
288 BrowserDB.removeBookmark(cr, mId); |
|
289 return null; |
|
290 } |
|
291 |
|
292 @Override |
|
293 public void onPostExecute(Void result) { |
|
294 Toast.makeText(mContext, R.string.bookmark_removed, Toast.LENGTH_SHORT).show(); |
|
295 } |
|
296 } |
|
297 |
|
298 |
|
299 private static class RemoveReadingListItemTask extends UiAsyncTask<Void, Void, Void> { |
|
300 private final int mId; |
|
301 private final String mUrl; |
|
302 private final Context mContext; |
|
303 |
|
304 public RemoveReadingListItemTask(Context context, int id, String url) { |
|
305 super(ThreadUtils.getBackgroundHandler()); |
|
306 mId = id; |
|
307 mUrl = url; |
|
308 mContext = context; |
|
309 } |
|
310 |
|
311 @Override |
|
312 public Void doInBackground(Void... params) { |
|
313 ContentResolver cr = mContext.getContentResolver(); |
|
314 BrowserDB.removeReadingListItem(cr, mId); |
|
315 |
|
316 GeckoEvent e = GeckoEvent.createBroadcastEvent("Reader:Remove", mUrl); |
|
317 GeckoAppShell.sendEventToGecko(e); |
|
318 |
|
319 return null; |
|
320 } |
|
321 } |
|
322 |
|
323 private static class RemoveHistoryTask extends UiAsyncTask<Void, Void, Void> { |
|
324 private final Context mContext; |
|
325 private final int mId; |
|
326 |
|
327 public RemoveHistoryTask(Context context, int id) { |
|
328 super(ThreadUtils.getBackgroundHandler()); |
|
329 |
|
330 mContext = context; |
|
331 mId = id; |
|
332 } |
|
333 |
|
334 @Override |
|
335 public Void doInBackground(Void... params) { |
|
336 BrowserDB.removeHistoryEntry(mContext.getContentResolver(), mId); |
|
337 return null; |
|
338 } |
|
339 |
|
340 @Override |
|
341 public void onPostExecute(Void result) { |
|
342 Toast.makeText(mContext, R.string.history_removed, Toast.LENGTH_SHORT).show(); |
|
343 } |
|
344 } |
|
345 } |