| |
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 } |