OLD | NEW |
| (Empty) |
1 // Copyright 2013 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 package org.chromium.chrome.browser.pageinfo; | |
6 | |
7 import android.animation.Animator; | |
8 import android.animation.AnimatorListenerAdapter; | |
9 import android.animation.AnimatorSet; | |
10 import android.animation.ObjectAnimator; | |
11 import android.app.Activity; | |
12 import android.app.Dialog; | |
13 import android.content.ActivityNotFoundException; | |
14 import android.content.ClipData; | |
15 import android.content.ClipboardManager; | |
16 import android.content.Context; | |
17 import android.content.DialogInterface; | |
18 import android.content.Intent; | |
19 import android.content.pm.PackageManager; | |
20 import android.graphics.Color; | |
21 import android.graphics.drawable.ColorDrawable; | |
22 import android.net.Uri; | |
23 import android.os.Bundle; | |
24 import android.provider.Settings; | |
25 import android.support.annotation.IntDef; | |
26 import android.support.v7.widget.AppCompatTextView; | |
27 import android.text.Layout; | |
28 import android.text.Spannable; | |
29 import android.text.SpannableString; | |
30 import android.text.SpannableStringBuilder; | |
31 import android.text.TextUtils; | |
32 import android.text.style.ForegroundColorSpan; | |
33 import android.text.style.StyleSpan; | |
34 import android.util.AttributeSet; | |
35 import android.view.Gravity; | |
36 import android.view.LayoutInflater; | |
37 import android.view.View; | |
38 import android.view.View.OnClickListener; | |
39 import android.view.View.OnLongClickListener; | |
40 import android.view.ViewGroup; | |
41 import android.view.Window; | |
42 import android.widget.Button; | |
43 import android.widget.ImageView; | |
44 import android.widget.LinearLayout; | |
45 import android.widget.ScrollView; | |
46 import android.widget.TextView; | |
47 | |
48 import org.chromium.base.ApiCompatibilityUtils; | |
49 import org.chromium.base.annotations.CalledByNative; | |
50 import org.chromium.base.metrics.RecordHistogram; | |
51 import org.chromium.base.metrics.RecordUserAction; | |
52 import org.chromium.chrome.R; | |
53 import org.chromium.chrome.browser.ContentSettingsType; | |
54 import org.chromium.chrome.browser.UrlConstants; | |
55 import org.chromium.chrome.browser.instantapps.InstantAppsHandler; | |
56 import org.chromium.chrome.browser.offlinepages.OfflinePageItem; | |
57 import org.chromium.chrome.browser.offlinepages.OfflinePageUtils; | |
58 import org.chromium.chrome.browser.omnibox.OmniboxUrlEmphasizer; | |
59 import org.chromium.chrome.browser.preferences.PrefServiceBridge; | |
60 import org.chromium.chrome.browser.preferences.Preferences; | |
61 import org.chromium.chrome.browser.preferences.PreferencesLauncher; | |
62 import org.chromium.chrome.browser.preferences.website.ContentSetting; | |
63 import org.chromium.chrome.browser.preferences.website.ContentSettingsResources; | |
64 import org.chromium.chrome.browser.preferences.website.SingleWebsitePreferences; | |
65 import org.chromium.chrome.browser.preferences.website.WebsitePreferenceBridge; | |
66 import org.chromium.chrome.browser.profiles.Profile; | |
67 import org.chromium.chrome.browser.ssl.SecurityStateModel; | |
68 import org.chromium.chrome.browser.tab.Tab; | |
69 import org.chromium.chrome.browser.util.UrlUtilities; | |
70 import org.chromium.components.location.LocationUtils; | |
71 import org.chromium.content_public.browser.WebContents; | |
72 import org.chromium.content_public.browser.WebContentsObserver; | |
73 import org.chromium.ui.base.DeviceFormFactor; | |
74 import org.chromium.ui.base.WindowAndroid; | |
75 import org.chromium.ui.base.WindowAndroid.PermissionCallback; | |
76 import org.chromium.ui.interpolators.BakedBezierInterpolator; | |
77 import org.chromium.ui.widget.Toast; | |
78 | |
79 import java.lang.annotation.Retention; | |
80 import java.lang.annotation.RetentionPolicy; | |
81 import java.net.URI; | |
82 import java.net.URISyntaxException; | |
83 import java.text.DateFormat; | |
84 import java.util.ArrayList; | |
85 import java.util.Date; | |
86 import java.util.List; | |
87 | |
88 /** | |
89 * Java side of Android implementation of the website settings UI. | |
90 * TODO(sashab): Rename this, and all its resources, to PageInfo* and page_info_
* instead of | |
91 * WebsiteSettings* and website_settings_*. Do this on the C++ sid
e as well. | |
92 */ | |
93 public class WebsiteSettingsPopup implements OnClickListener { | |
94 @Retention(RetentionPolicy.SOURCE) | |
95 @IntDef({OPENED_FROM_MENU, OPENED_FROM_TOOLBAR}) | |
96 private @interface OpenedFromSource {} | |
97 | |
98 public static final int OPENED_FROM_MENU = 1; | |
99 public static final int OPENED_FROM_TOOLBAR = 2; | |
100 | |
101 /** | |
102 * An entry in the settings dropdown for a given permission. There are two o
ptions for each | |
103 * permission: Allow and Block. | |
104 */ | |
105 private static final class PageInfoPermissionEntry { | |
106 public final String name; | |
107 public final int type; | |
108 public final ContentSetting setting; | |
109 | |
110 PageInfoPermissionEntry(String name, int type, ContentSetting setting) { | |
111 this.name = name; | |
112 this.type = type; | |
113 this.setting = setting; | |
114 } | |
115 | |
116 @Override | |
117 public String toString() { | |
118 return name; | |
119 } | |
120 } | |
121 | |
122 /** | |
123 * A TextView which truncates and displays a URL such that the origin is alw
ays visible. | |
124 * The URL can be expanded by clicking on the it. | |
125 */ | |
126 public static class ElidedUrlTextView extends AppCompatTextView { | |
127 // The number of lines to display when the URL is truncated. This number | |
128 // should still allow the origin to be displayed. NULL before | |
129 // setUrlAfterLayout() is called. | |
130 private Integer mTruncatedUrlLinesToDisplay; | |
131 | |
132 // The number of lines to display when the URL is expanded. This should
be enough to display | |
133 // at most two lines of the fragment if there is one in the URL. | |
134 private Integer mFullLinesToDisplay; | |
135 | |
136 // If true, the text view will show the truncated text. If false, it | |
137 // will show the full, expanded text. | |
138 private boolean mIsShowingTruncatedText = true; | |
139 | |
140 // The profile to use when getting the end index for the origin. | |
141 private Profile mProfile; | |
142 | |
143 // The maximum number of lines currently shown in the view | |
144 private int mCurrentMaxLines = Integer.MAX_VALUE; | |
145 | |
146 /** Constructor for inflating from XML. */ | |
147 public ElidedUrlTextView(Context context, AttributeSet attrs) { | |
148 super(context, attrs); | |
149 } | |
150 | |
151 @Override | |
152 public void setMaxLines(int maxlines) { | |
153 super.setMaxLines(maxlines); | |
154 mCurrentMaxLines = maxlines; | |
155 } | |
156 | |
157 /** | |
158 * Find the number of lines of text which must be shown in order to disp
lay the character at | |
159 * a given index. | |
160 */ | |
161 private int getLineForIndex(int index) { | |
162 Layout layout = getLayout(); | |
163 int endLine = 0; | |
164 while (endLine < layout.getLineCount() && layout.getLineEnd(endLine)
< index) { | |
165 endLine++; | |
166 } | |
167 // Since endLine is an index, add 1 to get the number of lines. | |
168 return endLine + 1; | |
169 } | |
170 | |
171 @Override | |
172 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { | |
173 setMaxLines(Integer.MAX_VALUE); | |
174 super.onMeasure(widthMeasureSpec, heightMeasureSpec); | |
175 assert mProfile != null : "setProfile() must be called before layout
."; | |
176 String urlText = getText().toString(); | |
177 | |
178 // Lay out the URL in a StaticLayout that is the same size as our fi
nal | |
179 // container. | |
180 int originEndIndex = OmniboxUrlEmphasizer.getOriginEndIndex(urlText,
mProfile); | |
181 | |
182 // Find the range of lines containing the origin. | |
183 int originEndLine = getLineForIndex(originEndIndex); | |
184 | |
185 // Display an extra line so we don't accidentally hide the origin wi
th | |
186 // ellipses | |
187 mTruncatedUrlLinesToDisplay = originEndLine + 1; | |
188 | |
189 // Find the line where the fragment starts. Since # is a reserved ch
aracter, it is safe | |
190 // to just search for the first # to appear in the url. | |
191 int fragmentStartIndex = urlText.indexOf('#'); | |
192 if (fragmentStartIndex == -1) fragmentStartIndex = urlText.length(); | |
193 | |
194 int fragmentStartLine = getLineForIndex(fragmentStartIndex); | |
195 mFullLinesToDisplay = fragmentStartLine + 1; | |
196 | |
197 // If there is no origin (according to OmniboxUrlEmphasizer), make s
ure the fragment is | |
198 // still hidden correctly. | |
199 if (mFullLinesToDisplay < mTruncatedUrlLinesToDisplay) { | |
200 mTruncatedUrlLinesToDisplay = mFullLinesToDisplay; | |
201 } | |
202 | |
203 if (updateMaxLines()) super.onMeasure(widthMeasureSpec, heightMeasur
eSpec); | |
204 } | |
205 | |
206 /** | |
207 * Sets the profile to use when calculating the end index of the origin. | |
208 * Must be called before layout. | |
209 * | |
210 * @param profile The profile to use when coloring the URL. | |
211 */ | |
212 public void setProfile(Profile profile) { | |
213 mProfile = profile; | |
214 } | |
215 | |
216 /** | |
217 * Toggles truncating/expanding the URL text. If the URL text is not | |
218 * truncated, has no effect. | |
219 */ | |
220 public void toggleTruncation() { | |
221 mIsShowingTruncatedText = !mIsShowingTruncatedText; | |
222 updateMaxLines(); | |
223 } | |
224 | |
225 private boolean updateMaxLines() { | |
226 int maxLines = mFullLinesToDisplay; | |
227 if (mIsShowingTruncatedText) maxLines = mTruncatedUrlLinesToDisplay; | |
228 if (maxLines != mCurrentMaxLines) { | |
229 setMaxLines(maxLines); | |
230 return true; | |
231 } | |
232 return false; | |
233 } | |
234 } | |
235 | |
236 // Delay enter to allow the triggering button to animate before we cover it. | |
237 private static final int ENTER_START_DELAY = 100; | |
238 private static final int FADE_DURATION = 200; | |
239 private static final int FADE_IN_BASE_DELAY = 150; | |
240 private static final int FADE_IN_DELAY_OFFSET = 20; | |
241 private static final int CLOSE_CLEANUP_DELAY = 10; | |
242 | |
243 private static final int MAX_TABLET_DIALOG_WIDTH_DP = 400; | |
244 | |
245 private final Context mContext; | |
246 private final WindowAndroid mWindowAndroid; | |
247 private final Tab mTab; | |
248 | |
249 // A pointer to the C++ object for this UI. | |
250 private long mNativeWebsiteSettingsPopup; | |
251 | |
252 // The outer container, filled with the layout from website_settings.xml. | |
253 private final LinearLayout mContainer; | |
254 | |
255 // UI elements in the dialog. | |
256 private final ElidedUrlTextView mUrlTitle; | |
257 private final TextView mConnectionSummary; | |
258 private final TextView mConnectionMessage; | |
259 private final LinearLayout mPermissionsList; | |
260 private final Button mInstantAppButton; | |
261 private final Button mSiteSettingsButton; | |
262 private final Button mOpenOnlineButton; | |
263 | |
264 // The dialog the container is placed in. | |
265 private final Dialog mDialog; | |
266 | |
267 // Animation which is currently running, if there is one. | |
268 private AnimatorSet mCurrentAnimation; | |
269 | |
270 private boolean mDismissWithoutAnimation; | |
271 | |
272 // The full URL from the URL bar, which is copied to the user's clipboard wh
en they select 'Copy | |
273 // URL'. | |
274 private String mFullUrl; | |
275 | |
276 // A parsed version of mFullUrl. Is null if the URL is invalid/cannot be | |
277 // parsed. | |
278 private URI mParsedUrl; | |
279 | |
280 // Whether or not this page is an internal chrome page (e.g. the | |
281 // chrome://settings page). | |
282 private boolean mIsInternalPage; | |
283 | |
284 // The security level of the page (a valid ConnectionSecurityLevel). | |
285 private int mSecurityLevel; | |
286 | |
287 // Permissions available to be displayed in mPermissionsList. | |
288 private List<PageInfoPermissionEntry> mDisplayedPermissions; | |
289 | |
290 // Creation date of an offline copy, if web contents contains an offline pag
e. | |
291 private String mOfflinePageCreationDate; | |
292 | |
293 // The name of the content publisher, if any. | |
294 private String mContentPublisher; | |
295 | |
296 // The intent associated with the instant app for this URL (or null if one d
oes not exist). | |
297 private Intent mInstantAppIntent; | |
298 | |
299 /** | |
300 * Creates the WebsiteSettingsPopup, but does not display it. Also initializ
es the corresponding | |
301 * C++ object and saves a pointer to it. | |
302 * @param activity Activity which is used for showing a popu
p. | |
303 * @param tab Tab for which the pop up is shown. | |
304 * @param offlinePageCreationDate Date when the offline page was created. | |
305 * @param publisher The name of the content publisher, if any
. | |
306 */ | |
307 private WebsiteSettingsPopup(Activity activity, Tab tab, String offlinePageC
reationDate, | |
308 String publisher) { | |
309 mContext = activity; | |
310 mTab = tab; | |
311 if (offlinePageCreationDate != null) { | |
312 mOfflinePageCreationDate = offlinePageCreationDate; | |
313 } | |
314 mWindowAndroid = mTab.getWebContents().getTopLevelNativeWindow(); | |
315 mContentPublisher = publisher; | |
316 | |
317 // Find the container and all it's important subviews. | |
318 mContainer = (LinearLayout) LayoutInflater.from(mContext).inflate( | |
319 R.layout.website_settings, null); | |
320 mContainer.setVisibility(View.INVISIBLE); | |
321 mContainer.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { | |
322 @Override | |
323 public void onLayoutChange( | |
324 View v, int l, int t, int r, int b, int ol, int ot, int or,
int ob) { | |
325 // Trigger the entrance animations once the main container has b
een laid out and has | |
326 // a height. | |
327 mContainer.removeOnLayoutChangeListener(this); | |
328 mContainer.setVisibility(View.VISIBLE); | |
329 createAllAnimations(true).start(); | |
330 } | |
331 }); | |
332 | |
333 mUrlTitle = (ElidedUrlTextView) mContainer.findViewById(R.id.website_set
tings_url); | |
334 mUrlTitle.setProfile(mTab.getProfile()); | |
335 mUrlTitle.setOnClickListener(this); | |
336 // Long press the url text to copy it to the clipboard. | |
337 mUrlTitle.setOnLongClickListener(new OnLongClickListener() { | |
338 @Override | |
339 public boolean onLongClick(View v) { | |
340 ClipboardManager clipboard = (ClipboardManager) mContext | |
341 .getSystemService(Context.CLIPBOARD_SERVICE); | |
342 ClipData clip = ClipData.newPlainText("url", mFullUrl); | |
343 clipboard.setPrimaryClip(clip); | |
344 Toast.makeText(mContext, R.string.url_copied, Toast.LENGTH_SHORT
).show(); | |
345 return true; | |
346 } | |
347 }); | |
348 | |
349 mConnectionSummary = (TextView) mContainer | |
350 .findViewById(R.id.website_settings_connection_summary); | |
351 mConnectionMessage = (TextView) mContainer | |
352 .findViewById(R.id.website_settings_connection_message); | |
353 mPermissionsList = (LinearLayout) mContainer | |
354 .findViewById(R.id.website_settings_permissions_list); | |
355 | |
356 mInstantAppButton = | |
357 (Button) mContainer.findViewById(R.id.website_settings_instant_a
pp_button); | |
358 mInstantAppButton.setOnClickListener(this); | |
359 | |
360 mSiteSettingsButton = | |
361 (Button) mContainer.findViewById(R.id.website_settings_site_sett
ings_button); | |
362 mSiteSettingsButton.setOnClickListener(this); | |
363 | |
364 mOpenOnlineButton = | |
365 (Button) mContainer.findViewById(R.id.website_settings_open_onli
ne_button); | |
366 mOpenOnlineButton.setOnClickListener(this); | |
367 | |
368 mDisplayedPermissions = new ArrayList<PageInfoPermissionEntry>(); | |
369 | |
370 // Hide the permissions list for sites with no permissions. | |
371 setVisibilityOfPermissionsList(false); | |
372 | |
373 // Work out the URL and connection message and status visibility. | |
374 mFullUrl = mTab.getWebContents().getVisibleUrl(); | |
375 if (isShowingOfflinePage()) { | |
376 mFullUrl = OfflinePageUtils.stripSchemeFromOnlineUrl(mFullUrl); | |
377 } | |
378 | |
379 try { | |
380 mParsedUrl = new URI(mFullUrl); | |
381 mIsInternalPage = UrlUtilities.isInternalScheme(mParsedUrl); | |
382 } catch (URISyntaxException e) { | |
383 mParsedUrl = null; | |
384 mIsInternalPage = false; | |
385 } | |
386 mSecurityLevel = SecurityStateModel.getSecurityLevelForWebContents(mTab.
getWebContents()); | |
387 | |
388 SpannableStringBuilder urlBuilder = new SpannableStringBuilder(mFullUrl)
; | |
389 OmniboxUrlEmphasizer.emphasizeUrl(urlBuilder, mContext.getResources(), m
Tab.getProfile(), | |
390 mSecurityLevel, mIsInternalPage, true, true); | |
391 mUrlTitle.setText(urlBuilder); | |
392 | |
393 if (mParsedUrl == null || mParsedUrl.getScheme() == null | |
394 || !(mParsedUrl.getScheme().equals(UrlConstants.HTTP_SCHEME) | |
395 || mParsedUrl.getScheme().equals(UrlConstants.HTTPS_S
CHEME))) { | |
396 mSiteSettingsButton.setVisibility(View.GONE); | |
397 } | |
398 | |
399 if (isShowingOfflinePage()) { | |
400 boolean isConnected = OfflinePageUtils.isConnected(); | |
401 RecordHistogram.recordBooleanHistogram( | |
402 "OfflinePages.WebsiteSettings.OpenOnlineButtonVisible", isCo
nnected); | |
403 if (!isConnected) mOpenOnlineButton.setVisibility(View.GONE); | |
404 } else { | |
405 mOpenOnlineButton.setVisibility(View.GONE); | |
406 } | |
407 | |
408 mInstantAppIntent = (mIsInternalPage || isShowingOfflinePage()) ? null | |
409 : InstantAppsHandler.getInstance().getInstantAppIntentForUrl(mFu
llUrl); | |
410 if (mInstantAppIntent == null) mInstantAppButton.setVisibility(View.GONE
); | |
411 | |
412 // Create the dialog. | |
413 mDialog = new Dialog(mContext) { | |
414 private void superDismiss() { | |
415 super.dismiss(); | |
416 } | |
417 | |
418 @Override | |
419 public void dismiss() { | |
420 if (DeviceFormFactor.isTablet(mContext) || mDismissWithoutAnimat
ion) { | |
421 // Dismiss the dialog without any custom animations on table
t. | |
422 super.dismiss(); | |
423 } else { | |
424 Animator animator = createAllAnimations(false); | |
425 animator.addListener(new AnimatorListenerAdapter() { | |
426 @Override | |
427 public void onAnimationEnd(Animator animation) { | |
428 // onAnimationEnd is called during the final frame o
f the animation. | |
429 // Delay the cleanup by a tiny amount to give this f
rame a chance to be | |
430 // displayed before we destroy the dialog. | |
431 mContainer.postDelayed(new Runnable() { | |
432 @Override | |
433 public void run() { | |
434 superDismiss(); | |
435 } | |
436 }, CLOSE_CLEANUP_DELAY); | |
437 } | |
438 }); | |
439 animator.start(); | |
440 } | |
441 } | |
442 }; | |
443 mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); | |
444 mDialog.setCanceledOnTouchOutside(true); | |
445 | |
446 // On smaller screens, place the dialog at the top of the screen, and re
move its border. | |
447 if (!DeviceFormFactor.isTablet(mContext)) { | |
448 Window window = mDialog.getWindow(); | |
449 window.setGravity(Gravity.TOP); | |
450 window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); | |
451 } | |
452 | |
453 // This needs to come after other member initialization. | |
454 mNativeWebsiteSettingsPopup = nativeInit(this, mTab.getWebContents()); | |
455 final WebContentsObserver webContentsObserver = | |
456 new WebContentsObserver(mTab.getWebContents()) { | |
457 @Override | |
458 public void navigationEntryCommitted() { | |
459 // If a navigation is committed (e.g. from in-page redirect), th
e data we're showing | |
460 // is stale so dismiss the dialog. | |
461 mDialog.dismiss(); | |
462 } | |
463 | |
464 @Override | |
465 public void wasHidden() { | |
466 // The web contents were hidden (potentially by loading another
URL via an intent), | |
467 // so dismiss the dialog). | |
468 mDialog.dismiss(); | |
469 } | |
470 | |
471 @Override | |
472 public void destroy() { | |
473 super.destroy(); | |
474 // Force the dialog to close immediately in case the destroy was
from Chrome | |
475 // quitting. | |
476 mDismissWithoutAnimation = true; | |
477 mDialog.dismiss(); | |
478 } | |
479 }; | |
480 mDialog.setOnDismissListener(new DialogInterface.OnDismissListener() { | |
481 @Override | |
482 public void onDismiss(DialogInterface dialog) { | |
483 assert mNativeWebsiteSettingsPopup != 0; | |
484 webContentsObserver.destroy(); | |
485 nativeDestroy(mNativeWebsiteSettingsPopup); | |
486 mNativeWebsiteSettingsPopup = 0; | |
487 } | |
488 }); | |
489 | |
490 showDialog(); | |
491 } | |
492 | |
493 /** | |
494 * Sets the visibility of the permissions list, which contains padding and b
orders that should | |
495 * not be shown if a site has no permissions. | |
496 * | |
497 * @param isVisible Whether to show or hide the dialog area. | |
498 */ | |
499 private void setVisibilityOfPermissionsList(boolean isVisible) { | |
500 int visibility = isVisible ? View.VISIBLE : View.GONE; | |
501 mPermissionsList.setVisibility(visibility); | |
502 } | |
503 | |
504 /** | |
505 * Finds the Image resource of the icon to use for the given permission. | |
506 * | |
507 * @param permission A valid ContentSettingsType that can be displayed in th
e PageInfo dialog to | |
508 * retrieve the image for. | |
509 * @return The resource ID of the icon to use for that permission. | |
510 */ | |
511 private int getImageResourceForPermission(int permission) { | |
512 int icon = ContentSettingsResources.getIcon(permission); | |
513 assert icon != 0 : "Icon requested for invalid permission: " + permissio
n; | |
514 return icon; | |
515 } | |
516 | |
517 /** | |
518 * Whether to show a 'Details' link to the connection info popup. The link i
s only shown for | |
519 * HTTPS connections. | |
520 */ | |
521 private boolean isConnectionDetailsLinkVisible() { | |
522 return mContentPublisher == null && !isShowingOfflinePage() && mParsedUr
l != null | |
523 && mParsedUrl.getScheme() != null | |
524 && mParsedUrl.getScheme().equals(UrlConstants.HTTPS_SCHE
ME); | |
525 } | |
526 | |
527 private boolean hasAndroidPermission(int contentSettingType) { | |
528 String[] androidPermissions = | |
529 PrefServiceBridge.getAndroidPermissionsForContentSetting(content
SettingType); | |
530 if (androidPermissions == null) return true; | |
531 for (int i = 0; i < androidPermissions.length; i++) { | |
532 if (!mWindowAndroid.hasPermission(androidPermissions[i])) { | |
533 return false; | |
534 } | |
535 } | |
536 return true; | |
537 } | |
538 | |
539 /** | |
540 * Adds a new row for the given permission. | |
541 * | |
542 * @param name The title of the permission to display to the user. | |
543 * @param type The ContentSettingsType of the permission. | |
544 * @param currentSettingValue The ContentSetting value of the currently sele
cted setting. | |
545 */ | |
546 @CalledByNative | |
547 private void addPermissionSection(String name, int type, int currentSettingV
alue) { | |
548 // We have at least one permission, so show the lower permissions area. | |
549 setVisibilityOfPermissionsList(true); | |
550 mDisplayedPermissions.add(new PageInfoPermissionEntry(name, type, Conten
tSetting | |
551 .fromInt(currentSettingValue))); | |
552 } | |
553 | |
554 /** | |
555 * Update the permissions view based on the contents of mDisplayedPermission
s. | |
556 */ | |
557 @CalledByNative | |
558 private void updatePermissionDisplay() { | |
559 mPermissionsList.removeAllViews(); | |
560 for (PageInfoPermissionEntry permission : mDisplayedPermissions) { | |
561 addReadOnlyPermissionSection(permission); | |
562 } | |
563 } | |
564 | |
565 private void addReadOnlyPermissionSection(PageInfoPermissionEntry permission
) { | |
566 View permissionRow = LayoutInflater.from(mContext).inflate( | |
567 R.layout.website_settings_permission_row, null); | |
568 | |
569 ImageView permissionIcon = (ImageView) permissionRow.findViewById( | |
570 R.id.website_settings_permission_icon); | |
571 permissionIcon.setImageResource(getImageResourceForPermission(permission
.type)); | |
572 | |
573 if (permission.setting == ContentSetting.ALLOW) { | |
574 int warningTextResource = 0; | |
575 | |
576 // If warningTextResource is non-zero, then the view must be tagged
with either | |
577 // permission_intent_override or permission_type. | |
578 LocationUtils locationUtils = LocationUtils.getInstance(); | |
579 if (permission.type == ContentSettingsType.CONTENT_SETTINGS_TYPE_GEO
LOCATION | |
580 && !locationUtils.isSystemLocationSettingEnabled()) { | |
581 warningTextResource = R.string.page_info_android_location_blocke
d; | |
582 permissionRow.setTag(R.id.permission_intent_override, | |
583 locationUtils.getSystemLocationSettingsIntent()); | |
584 } else if (!hasAndroidPermission(permission.type)) { | |
585 warningTextResource = R.string.page_info_android_permission_bloc
ked; | |
586 permissionRow.setTag(R.id.permission_type, | |
587 PrefServiceBridge.getAndroidPermissionsForContentSetting
(permission.type)); | |
588 } | |
589 | |
590 if (warningTextResource != 0) { | |
591 TextView permissionUnavailable = (TextView) permissionRow.findVi
ewById( | |
592 R.id.website_settings_permission_unavailable_message); | |
593 permissionUnavailable.setVisibility(View.VISIBLE); | |
594 permissionUnavailable.setText(warningTextResource); | |
595 | |
596 permissionIcon.setImageResource(R.drawable.exclamation_triangle)
; | |
597 permissionIcon.setColorFilter(ApiCompatibilityUtils.getColor( | |
598 mContext.getResources(), R.color.website_settings_popup_
text_link)); | |
599 | |
600 permissionRow.setOnClickListener(this); | |
601 } | |
602 } | |
603 | |
604 TextView permissionStatus = (TextView) permissionRow.findViewById( | |
605 R.id.website_settings_permission_status); | |
606 SpannableStringBuilder builder = new SpannableStringBuilder(); | |
607 SpannableString nameString = new SpannableString(permission.name); | |
608 final StyleSpan boldSpan = new StyleSpan(android.graphics.Typeface.BOLD)
; | |
609 nameString.setSpan(boldSpan, 0, nameString.length(), Spannable.SPAN_INCL
USIVE_EXCLUSIVE); | |
610 | |
611 builder.append(nameString); | |
612 builder.append(" – "); // en-dash. | |
613 String status_text = ""; | |
614 switch (permission.setting) { | |
615 case ALLOW: | |
616 status_text = mContext.getString(R.string.page_info_permission_a
llowed); | |
617 break; | |
618 case BLOCK: | |
619 status_text = mContext.getString(R.string.page_info_permission_b
locked); | |
620 break; | |
621 default: | |
622 assert false : "Invalid setting " + permission.setting + " for p
ermission " | |
623 + permission.type; | |
624 } | |
625 if (permission.type == ContentSettingsType.CONTENT_SETTINGS_TYPE_GEOLOCA
TION | |
626 && WebsitePreferenceBridge.shouldUseDSEGeolocationSetting(mFullU
rl, false)) { | |
627 status_text = statusTextForDSEPermission(permission); | |
628 } | |
629 builder.append(status_text); | |
630 permissionStatus.setText(builder); | |
631 mPermissionsList.addView(permissionRow); | |
632 } | |
633 | |
634 /** | |
635 * Update the permission string for the Default Search Engine. | |
636 */ | |
637 private String statusTextForDSEPermission(PageInfoPermissionEntry permission
) { | |
638 if (permission.setting == ContentSetting.ALLOW) { | |
639 return mContext.getString(R.string.page_info_dse_permission_allowed)
; | |
640 } | |
641 | |
642 return mContext.getString(R.string.page_info_dse_permission_blocked); | |
643 } | |
644 | |
645 /** | |
646 * Sets the connection security summary and detailed description strings. Th
ese strings may be | |
647 * overridden based on the state of the Android UI. | |
648 */ | |
649 @CalledByNative | |
650 private void setSecurityDescription(String summary, String details) { | |
651 // Display the appropriate connection message. | |
652 SpannableStringBuilder messageBuilder = new SpannableStringBuilder(); | |
653 if (mContentPublisher != null) { | |
654 messageBuilder.append( | |
655 mContext.getString(R.string.page_info_domain_hidden, mConten
tPublisher)); | |
656 } else if (isShowingOfflinePage()) { | |
657 messageBuilder.append(String.format( | |
658 mContext.getString(R.string.page_info_connection_offline), | |
659 mOfflinePageCreationDate)); | |
660 } else { | |
661 if (!TextUtils.equals(summary, details)) { | |
662 mConnectionSummary.setVisibility(View.VISIBLE); | |
663 mConnectionSummary.setText(summary); | |
664 } | |
665 messageBuilder.append(details); | |
666 } | |
667 | |
668 if (isConnectionDetailsLinkVisible()) { | |
669 messageBuilder.append(" "); | |
670 SpannableString detailsText = new SpannableString( | |
671 mContext.getString(R.string.page_info_details_link)); | |
672 final ForegroundColorSpan blueSpan = new ForegroundColorSpan( | |
673 ApiCompatibilityUtils.getColor(mContext.getResources(), | |
674 R.color.website_settings_popup_text_link)); | |
675 detailsText.setSpan( | |
676 blueSpan, 0, detailsText.length(), Spannable.SPAN_INCLUSIVE_
EXCLUSIVE); | |
677 messageBuilder.append(detailsText); | |
678 } | |
679 mConnectionMessage.setText(messageBuilder); | |
680 if (isConnectionDetailsLinkVisible()) mConnectionMessage.setOnClickListe
ner(this); | |
681 } | |
682 | |
683 /** | |
684 * Displays the WebsiteSettingsPopup. | |
685 */ | |
686 private void showDialog() { | |
687 if (!DeviceFormFactor.isTablet(mContext)) { | |
688 // On smaller screens, make the dialog fill the width of the screen. | |
689 ScrollView scrollView = new ScrollView(mContext); | |
690 scrollView.addView(mContainer); | |
691 mDialog.addContentView(scrollView, new LinearLayout.LayoutParams( | |
692 LinearLayout.LayoutParams.MATCH_PARENT, | |
693 LinearLayout.LayoutParams.MATCH_PARENT)); | |
694 | |
695 // This must be called after addContentView, or it won't fully fill
to the edge. | |
696 Window window = mDialog.getWindow(); | |
697 window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, | |
698 ViewGroup.LayoutParams.WRAP_CONTENT); | |
699 } else { | |
700 // On larger screens, make the dialog centered in the screen and hav
e a maximum width. | |
701 ScrollView scrollView = new ScrollView(mContext) { | |
702 @Override | |
703 protected void onMeasure(int widthMeasureSpec, int heightMeasure
Spec) { | |
704 final int maxDialogWidthInPx = (int) (MAX_TABLET_DIALOG_WIDT
H_DP | |
705 * mContext.getResources().getDisplayMetrics().densit
y); | |
706 if (MeasureSpec.getSize(widthMeasureSpec) > maxDialogWidthIn
Px) { | |
707 widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxDialog
WidthInPx, | |
708 MeasureSpec.EXACTLY); | |
709 } | |
710 super.onMeasure(widthMeasureSpec, heightMeasureSpec); | |
711 } | |
712 }; | |
713 | |
714 scrollView.addView(mContainer); | |
715 mDialog.addContentView(scrollView, new LinearLayout.LayoutParams( | |
716 LinearLayout.LayoutParams.WRAP_CONTENT, | |
717 LinearLayout.LayoutParams.MATCH_PARENT)); | |
718 } | |
719 | |
720 mDialog.show(); | |
721 } | |
722 | |
723 /** | |
724 * Dismiss the popup, and then run a task after the animation has completed
(if there is one). | |
725 */ | |
726 private void runAfterDismiss(Runnable task) { | |
727 mDialog.dismiss(); | |
728 if (DeviceFormFactor.isTablet(mContext)) { | |
729 task.run(); | |
730 } else { | |
731 mContainer.postDelayed(task, FADE_DURATION + CLOSE_CLEANUP_DELAY); | |
732 } | |
733 } | |
734 | |
735 @Override | |
736 public void onClick(View view) { | |
737 if (view == mSiteSettingsButton) { | |
738 // Delay while the WebsiteSettingsPopup closes. | |
739 runAfterDismiss(new Runnable() { | |
740 @Override | |
741 public void run() { | |
742 recordAction(WebsiteSettingsAction.WEBSITE_SETTINGS_SITE_SET
TINGS_OPENED); | |
743 Bundle fragmentArguments = | |
744 SingleWebsitePreferences.createFragmentArgsForSite(m
FullUrl); | |
745 fragmentArguments.putParcelable(SingleWebsitePreferences.EXT
RA_WEB_CONTENTS, | |
746 mTab.getWebContents()); | |
747 Intent preferencesIntent = PreferencesLauncher.createIntentF
orSettingsPage( | |
748 mContext, SingleWebsitePreferences.class.getName()); | |
749 preferencesIntent.putExtra( | |
750 Preferences.EXTRA_SHOW_FRAGMENT_ARGUMENTS, fragmentA
rguments); | |
751 mContext.startActivity(preferencesIntent); | |
752 } | |
753 }); | |
754 } else if (view == mInstantAppButton) { | |
755 try { | |
756 mContext.startActivity(mInstantAppIntent); | |
757 RecordUserAction.record("Android.InstantApps.LaunchedFromWebsite
SettingsPopup"); | |
758 } catch (ActivityNotFoundException e) { | |
759 mInstantAppButton.setEnabled(false); | |
760 } | |
761 } else if (view == mUrlTitle) { | |
762 // Expand/collapse the displayed URL title. | |
763 mUrlTitle.toggleTruncation(); | |
764 } else if (view == mConnectionMessage) { | |
765 runAfterDismiss(new Runnable() { | |
766 @Override | |
767 public void run() { | |
768 if (!mTab.getWebContents().isDestroyed()) { | |
769 recordAction( | |
770 WebsiteSettingsAction.WEBSITE_SETTINGS_SECURITY_
DETAILS_OPENED); | |
771 ConnectionInfoPopup.show(mContext, mTab.getWebContents()
); | |
772 } | |
773 } | |
774 }); | |
775 } else if (view.getId() == R.id.website_settings_permission_row) { | |
776 final Object intentOverride = view.getTag(R.id.permission_intent_ove
rride); | |
777 | |
778 if (intentOverride == null && mWindowAndroid != null) { | |
779 // Try and immediately request missing Android permissions where
possible. | |
780 final String[] permissionType = (String[]) view.getTag(R.id.perm
ission_type); | |
781 for (int i = 0; i < permissionType.length; i++) { | |
782 if (!mWindowAndroid.canRequestPermission(permissionType[i]))
continue; | |
783 | |
784 // If any permissions can be requested, attempt to request t
hem all. | |
785 mWindowAndroid.requestPermissions(permissionType, new Permis
sionCallback() { | |
786 @Override | |
787 public void onRequestPermissionsResult( | |
788 String[] permissions, int[] grantResults) { | |
789 boolean allGranted = true; | |
790 for (int i = 0; i < grantResults.length; i++) { | |
791 if (grantResults[i] != PackageManager.PERMISSION
_GRANTED) { | |
792 allGranted = false; | |
793 break; | |
794 } | |
795 } | |
796 if (allGranted) updatePermissionDisplay(); | |
797 } | |
798 }); | |
799 return; | |
800 } | |
801 } | |
802 | |
803 runAfterDismiss(new Runnable() { | |
804 @Override | |
805 public void run() { | |
806 Intent settingsIntent; | |
807 if (intentOverride != null) { | |
808 settingsIntent = (Intent) intentOverride; | |
809 } else { | |
810 settingsIntent = new Intent(Settings.ACTION_APPLICATION_
DETAILS_SETTINGS); | |
811 settingsIntent.setData(Uri.parse("package:" + mContext.g
etPackageName())); | |
812 } | |
813 settingsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | |
814 mContext.startActivity(settingsIntent); | |
815 } | |
816 }); | |
817 } else if (view == mOpenOnlineButton) { | |
818 runAfterDismiss(new Runnable() { | |
819 @Override | |
820 public void run() { | |
821 // Attempt to reload to an online version of the viewed offl
ine web page. This | |
822 // attempt might fail if the user is offline, in which case
an offline copy will | |
823 // be reloaded. | |
824 RecordHistogram.recordBooleanHistogram( | |
825 "OfflinePages.WebsiteSettings.ConnectedWhenOpenOnlin
eButtonClicked", | |
826 OfflinePageUtils.isConnected()); | |
827 OfflinePageUtils.reload(mTab); | |
828 } | |
829 }); | |
830 } | |
831 } | |
832 | |
833 /** | |
834 * Create a list of all the views which we want to individually fade in. | |
835 */ | |
836 private List<View> collectAnimatableViews() { | |
837 List<View> animatableViews = new ArrayList<View>(); | |
838 animatableViews.add(mUrlTitle); | |
839 if (mConnectionSummary.getVisibility() == View.VISIBLE) { | |
840 animatableViews.add(mConnectionSummary); | |
841 } | |
842 animatableViews.add(mConnectionMessage); | |
843 animatableViews.add(mInstantAppButton); | |
844 for (int i = 0; i < mPermissionsList.getChildCount(); i++) { | |
845 animatableViews.add(mPermissionsList.getChildAt(i)); | |
846 } | |
847 animatableViews.add(mSiteSettingsButton); | |
848 | |
849 return animatableViews; | |
850 } | |
851 | |
852 /** | |
853 * Create an animator to fade an individual dialog element. | |
854 */ | |
855 private Animator createInnerFadeAnimator(final View view, int position, bool
ean isEnter) { | |
856 ObjectAnimator alphaAnim; | |
857 | |
858 if (isEnter) { | |
859 view.setAlpha(0f); | |
860 alphaAnim = ObjectAnimator.ofFloat(view, View.ALPHA, 1f); | |
861 alphaAnim.setStartDelay(FADE_IN_BASE_DELAY + FADE_IN_DELAY_OFFSET *
position); | |
862 } else { | |
863 alphaAnim = ObjectAnimator.ofFloat(view, View.ALPHA, 0f); | |
864 } | |
865 | |
866 alphaAnim.setDuration(FADE_DURATION); | |
867 return alphaAnim; | |
868 } | |
869 | |
870 /** | |
871 * Create an animator to slide in the entire dialog from the top of the scre
en. | |
872 */ | |
873 private Animator createDialogSlideAnimator(boolean isEnter) { | |
874 final float animHeight = -1f * mContainer.getHeight(); | |
875 ObjectAnimator translateAnim; | |
876 if (isEnter) { | |
877 mContainer.setTranslationY(animHeight); | |
878 translateAnim = ObjectAnimator.ofFloat(mContainer, View.TRANSLATION_
Y, 0f); | |
879 translateAnim.setInterpolator(BakedBezierInterpolator.FADE_IN_CURVE)
; | |
880 } else { | |
881 translateAnim = ObjectAnimator.ofFloat(mContainer, View.TRANSLATION_
Y, animHeight); | |
882 translateAnim.setInterpolator(BakedBezierInterpolator.FADE_OUT_CURVE
); | |
883 } | |
884 translateAnim.setDuration(FADE_DURATION); | |
885 return translateAnim; | |
886 } | |
887 | |
888 /** | |
889 * Create animations for showing/hiding the popup. | |
890 * | |
891 * Tablets use the default Dialog fade-in instead of sliding in manually. | |
892 */ | |
893 private Animator createAllAnimations(boolean isEnter) { | |
894 AnimatorSet animation = new AnimatorSet(); | |
895 AnimatorSet.Builder builder = null; | |
896 Animator startAnim; | |
897 | |
898 if (DeviceFormFactor.isTablet(mContext)) { | |
899 // The start time of the entire AnimatorSet is the start time of the
first animation | |
900 // added to the Builder. We use a blank AnimatorSet on tablet as an
easy way to | |
901 // co-ordinate this start time. | |
902 startAnim = new AnimatorSet(); | |
903 } else { | |
904 startAnim = createDialogSlideAnimator(isEnter); | |
905 } | |
906 | |
907 if (isEnter) startAnim.setStartDelay(ENTER_START_DELAY); | |
908 builder = animation.play(startAnim); | |
909 | |
910 List<View> animatableViews = collectAnimatableViews(); | |
911 for (int i = 0; i < animatableViews.size(); i++) { | |
912 View view = animatableViews.get(i); | |
913 Animator anim = createInnerFadeAnimator(view, i, isEnter); | |
914 builder.with(anim); | |
915 } | |
916 | |
917 animation.addListener(new AnimatorListenerAdapter() { | |
918 @Override | |
919 public void onAnimationEnd(Animator animation) { | |
920 mCurrentAnimation = null; | |
921 } | |
922 }); | |
923 if (mCurrentAnimation != null) mCurrentAnimation.cancel(); | |
924 mCurrentAnimation = animation; | |
925 return animation; | |
926 } | |
927 | |
928 private void recordAction(int action) { | |
929 if (mNativeWebsiteSettingsPopup != 0) { | |
930 nativeRecordWebsiteSettingsAction(mNativeWebsiteSettingsPopup, actio
n); | |
931 } | |
932 } | |
933 | |
934 /** | |
935 * Whether website dialog is displayed for an offline page. | |
936 */ | |
937 private boolean isShowingOfflinePage() { | |
938 return mOfflinePageCreationDate != null; | |
939 } | |
940 | |
941 /** | |
942 * Shows a WebsiteSettings dialog for the provided Tab. The popup adds itsel
f to the view | |
943 * hierarchy which owns the reference while it's visible. | |
944 * | |
945 * @param activity Activity which is used for launching a dialog. | |
946 * @param tab The tab hosting the web contents for which to show Website inf
ormation. This | |
947 * information is retrieved for the visible entry. | |
948 * @param contentPublisher The name of the publisher of the content. | |
949 * @param source Determines the source that triggered the popup. | |
950 */ | |
951 public static void show(final Activity activity, final Tab tab, final String
contentPublisher, | |
952 @OpenedFromSource int source) { | |
953 if (source == OPENED_FROM_MENU) { | |
954 RecordUserAction.record("MobileWebsiteSettingsOpenedFromMenu"); | |
955 } else if (source == OPENED_FROM_TOOLBAR) { | |
956 RecordUserAction.record("MobileWebsiteSettingsOpenedFromToolbar"); | |
957 } else { | |
958 assert false : "Invalid source passed"; | |
959 } | |
960 | |
961 String offlinePageCreationDate = null; | |
962 | |
963 OfflinePageItem offlinePage = tab.getOfflinePage(); | |
964 if (offlinePage != null) { | |
965 // Get formatted creation date of the offline page. | |
966 Date creationDate = new Date(offlinePage.getCreationTimeMs()); | |
967 DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM); | |
968 offlinePageCreationDate = df.format(creationDate); | |
969 } | |
970 | |
971 new WebsiteSettingsPopup(activity, tab, offlinePageCreationDate, content
Publisher); | |
972 } | |
973 | |
974 private static native long nativeInit(WebsiteSettingsPopup popup, WebContent
s webContents); | |
975 | |
976 private native void nativeDestroy(long nativeWebsiteSettingsPopupAndroid); | |
977 | |
978 private native void nativeRecordWebsiteSettingsAction( | |
979 long nativeWebsiteSettingsPopupAndroid, int action); | |
980 } | |
OLD | NEW |