diff options
Diffstat (limited to 'src/com/android/browser/preferences/WebsiteSettingsFragment.java')
-rw-r--r-- | src/com/android/browser/preferences/WebsiteSettingsFragment.java | 672 |
1 files changed, 672 insertions, 0 deletions
diff --git a/src/com/android/browser/preferences/WebsiteSettingsFragment.java b/src/com/android/browser/preferences/WebsiteSettingsFragment.java new file mode 100644 index 0000000..1965ffe --- /dev/null +++ b/src/com/android/browser/preferences/WebsiteSettingsFragment.java @@ -0,0 +1,672 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.browser.preferences; + +import com.android.browser.R; +import com.android.browser.WebStorageSizeManager; + +import android.app.AlertDialog; +import android.app.FragmentTransaction; +import android.app.ListFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.preference.PreferenceActivity; +import android.preference.PreferenceFragment; +import android.provider.BrowserContract.Bookmarks; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.webkit.GeolocationPermissions; +import android.webkit.ValueCallback; +import android.webkit.WebStorage; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * Manage the settings for an origin. + * We use it to keep track of the 'HTML5' settings, i.e. database (webstorage) + * and Geolocation. + */ +public class WebsiteSettingsFragment extends ListFragment implements OnClickListener { + + private static final String EXTRA_SITE = "site"; + private String LOGTAG = "WebsiteSettingsActivity"; + private static String sMBStored = null; + private SiteAdapter mAdapter = null; + private Site mSite = null; + + static class Site implements Serializable { + private String mOrigin; + private String mTitle; + private Bitmap mIcon; + private int mFeatures; + + // These constants provide the set of features that a site may support + // They must be consecutive. To add a new feature, add a new FEATURE_XXX + // variable with value equal to the current value of FEATURE_COUNT, then + // increment FEATURE_COUNT. + private final static int FEATURE_WEB_STORAGE = 0; + private final static int FEATURE_GEOLOCATION = 1; + // The number of features available. + private final static int FEATURE_COUNT = 2; + + public Site(String origin) { + mOrigin = origin; + mTitle = null; + mIcon = null; + mFeatures = 0; + } + + public void addFeature(int feature) { + mFeatures |= (1 << feature); + } + + public void removeFeature(int feature) { + mFeatures &= ~(1 << feature); + } + + public boolean hasFeature(int feature) { + return (mFeatures & (1 << feature)) != 0; + } + + /** + * Gets the number of features supported by this site. + */ + public int getFeatureCount() { + int count = 0; + for (int i = 0; i < FEATURE_COUNT; ++i) { + count += hasFeature(i) ? 1 : 0; + } + return count; + } + + /** + * Gets the ID of the nth (zero-based) feature supported by this site. + * The return value is a feature ID - one of the FEATURE_XXX values. + * This is required to determine which feature is displayed at a given + * position in the list of features for this site. This is used both + * when populating the view and when responding to clicks on the list. + */ + public int getFeatureByIndex(int n) { + int j = -1; + for (int i = 0; i < FEATURE_COUNT; ++i) { + j += hasFeature(i) ? 1 : 0; + if (j == n) { + return i; + } + } + return -1; + } + + public String getOrigin() { + return mOrigin; + } + + public void setTitle(String title) { + mTitle = title; + } + + public void setIcon(Bitmap icon) { + mIcon = icon; + } + + public Bitmap getIcon() { + return mIcon; + } + + public String getPrettyOrigin() { + return mTitle == null ? null : hideHttp(mOrigin); + } + + public String getPrettyTitle() { + return mTitle == null ? hideHttp(mOrigin) : mTitle; + } + + private String hideHttp(String str) { + Uri uri = Uri.parse(str); + return "http".equals(uri.getScheme()) ? str.substring(7) : str; + } + } + + class SiteAdapter extends ArrayAdapter<Site> + implements AdapterView.OnItemClickListener { + private int mResource; + private LayoutInflater mInflater; + private Bitmap mDefaultIcon; + private Bitmap mUsageEmptyIcon; + private Bitmap mUsageLowIcon; + private Bitmap mUsageHighIcon; + private Bitmap mLocationAllowedIcon; + private Bitmap mLocationDisallowedIcon; + private Site mCurrentSite; + + public SiteAdapter(Context context, int rsc) { + this(context, rsc, null); + } + + public SiteAdapter(Context context, int rsc, Site site) { + super(context, rsc); + mResource = rsc; + mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mDefaultIcon = BitmapFactory.decodeResource(getResources(), + R.drawable.app_web_browser_sm); + mUsageEmptyIcon = BitmapFactory.decodeResource(getResources(), + R.drawable.ic_list_data_off); + mUsageLowIcon = BitmapFactory.decodeResource(getResources(), + R.drawable.ic_list_data_small); + mUsageHighIcon = BitmapFactory.decodeResource(getResources(), + R.drawable.ic_list_data_large); + mLocationAllowedIcon = BitmapFactory.decodeResource(getResources(), + R.drawable.ic_list_gps_on); + mLocationDisallowedIcon = BitmapFactory.decodeResource(getResources(), + R.drawable.ic_list_gps_denied); + mCurrentSite = site; + if (mCurrentSite == null) { + askForOrigins(); + } + } + + /** + * Adds the specified feature to the site corresponding to supplied + * origin in the map. Creates the site if it does not already exist. + */ + private void addFeatureToSite(Map<String, Site> sites, String origin, int feature) { + Site site = null; + if (sites.containsKey(origin)) { + site = (Site) sites.get(origin); + } else { + site = new Site(origin); + sites.put(origin, site); + } + site.addFeature(feature); + } + + public void askForOrigins() { + // Get the list of origins we want to display. + // All 'HTML 5 modules' (Database, Geolocation etc) form these + // origin strings using WebCore::SecurityOrigin::toString(), so it's + // safe to group origins here. Note that WebCore::SecurityOrigin + // uses 0 (which is not printed) for the port if the port is the + // default for the protocol. Eg http://www.google.com and + // http://www.google.com:80 both record a port of 0 and hence + // toString() == 'http://www.google.com' for both. + + WebStorage.getInstance().getOrigins(new ValueCallback<Map>() { + public void onReceiveValue(Map origins) { + Map<String, Site> sites = new HashMap<String, Site>(); + if (origins != null) { + Iterator<String> iter = origins.keySet().iterator(); + while (iter.hasNext()) { + addFeatureToSite(sites, iter.next(), Site.FEATURE_WEB_STORAGE); + } + } + askForGeolocation(sites); + } + }); + } + + public void askForGeolocation(final Map<String, Site> sites) { + GeolocationPermissions.getInstance().getOrigins(new ValueCallback<Set<String> >() { + public void onReceiveValue(Set<String> origins) { + if (origins != null) { + Iterator<String> iter = origins.iterator(); + while (iter.hasNext()) { + addFeatureToSite(sites, iter.next(), Site.FEATURE_GEOLOCATION); + } + } + populateIcons(sites); + populateOrigins(sites); + } + }); + } + + public void populateIcons(Map<String, Site> sites) { + // Create a map from host to origin. This is used to add metadata + // (title, icon) for this origin from the bookmarks DB. We must do + // the DB access on a background thread. + new UpdateFromBookmarksDbTask(this.getContext(), sites).execute(); + } + + private class UpdateFromBookmarksDbTask extends AsyncTask<Void, Void, Void> { + + private Context mContext; + private boolean mDataSetChanged; + private Map<String, Site> mSites; + + public UpdateFromBookmarksDbTask(Context ctx, Map<String, Site> sites) { + mContext = ctx; + mSites = sites; + } + + protected Void doInBackground(Void... unused) { + HashMap<String, Set<Site>> hosts = new HashMap<String, Set<Site>>(); + Set<Map.Entry<String, Site>> elements = mSites.entrySet(); + Iterator<Map.Entry<String, Site>> originIter = elements.iterator(); + while (originIter.hasNext()) { + Map.Entry<String, Site> entry = originIter.next(); + Site site = entry.getValue(); + String host = Uri.parse(entry.getKey()).getHost(); + Set<Site> hostSites = null; + if (hosts.containsKey(host)) { + hostSites = (Set<Site>)hosts.get(host); + } else { + hostSites = new HashSet<Site>(); + hosts.put(host, hostSites); + } + hostSites.add(site); + } + + // Check the bookmark DB. If we have data for a host used by any of + // our origins, use it to set their title and favicon + Cursor c = mContext.getContentResolver().query(Bookmarks.CONTENT_URI, + new String[] { Bookmarks.URL, Bookmarks.TITLE, Bookmarks.FAVICON }, + Bookmarks.IS_FOLDER + " == 0", null, null); + + if (c != null) { + if (c.moveToFirst()) { + int urlIndex = c.getColumnIndex(Bookmarks.URL); + int titleIndex = c.getColumnIndex(Bookmarks.TITLE); + int faviconIndex = c.getColumnIndex(Bookmarks.FAVICON); + do { + String url = c.getString(urlIndex); + String host = Uri.parse(url).getHost(); + if (hosts.containsKey(host)) { + String title = c.getString(titleIndex); + Bitmap bmp = null; + byte[] data = c.getBlob(faviconIndex); + if (data != null) { + bmp = BitmapFactory.decodeByteArray(data, 0, data.length); + } + Set matchingSites = (Set) hosts.get(host); + Iterator<Site> sitesIter = matchingSites.iterator(); + while (sitesIter.hasNext()) { + Site site = sitesIter.next(); + // We should only set the title if the bookmark is for the root + // (i.e. www.google.com), as website settings act on the origin + // as a whole rather than a single page under that origin. If the + // user has bookmarked a page under the root but *not* the root, + // then we risk displaying the title of that page which may or + // may not have any relevance to the origin. + if (url.equals(site.getOrigin()) || + (new String(site.getOrigin()+"/")).equals(url)) { + mDataSetChanged = true; + site.setTitle(title); + } + + if (bmp != null) { + mDataSetChanged = true; + site.setIcon(bmp); + } + } + } + } while (c.moveToNext()); + } + c.close(); + } + return null; + } + + protected void onPostExecute(Void unused) { + if (mDataSetChanged) { + notifyDataSetChanged(); + } + } + } + + + public void populateOrigins(Map<String, Site> sites) { + clear(); + + // We can now simply populate our array with Site instances + Set<Map.Entry<String, Site>> elements = sites.entrySet(); + Iterator<Map.Entry<String, Site>> entryIterator = elements.iterator(); + while (entryIterator.hasNext()) { + Map.Entry<String, Site> entry = entryIterator.next(); + Site site = entry.getValue(); + add(site); + } + + notifyDataSetChanged(); + + if (getCount() == 0) { + finish(); // we close the screen + } + } + + public int getCount() { + if (mCurrentSite == null) { + return super.getCount(); + } + return mCurrentSite.getFeatureCount(); + } + + public String sizeValueToString(long bytes) { + // We display the size in MB, to 1dp, rounding up to the next 0.1MB. + // bytes should always be greater than zero. + if (bytes <= 0) { + Log.e(LOGTAG, "sizeValueToString called with non-positive value: " + bytes); + return "0"; + } + float megabytes = (float) bytes / (1024.0F * 1024.0F); + int truncated = (int) Math.ceil(megabytes * 10.0F); + float result = (float) (truncated / 10.0F); + return String.valueOf(result); + } + + /* + * If we receive the back event and are displaying + * site's settings, we want to go back to the main + * list view. If not, we just do nothing (see + * dispatchKeyEvent() below). + */ + public boolean backKeyPressed() { + if (mCurrentSite != null) { + mCurrentSite = null; + askForOrigins(); + return true; + } + return false; + } + + /** + * @hide + * Utility function + * Set the icon according to the usage + */ + public void setIconForUsage(ImageView usageIcon, long usageInBytes) { + float usageInMegabytes = (float) usageInBytes / (1024.0F * 1024.0F); + // We set the correct icon: + // 0 < empty < 0.1MB + // 0.1MB < low < 5MB + // 5MB < high + if (usageInMegabytes <= 0.1) { + usageIcon.setImageBitmap(mUsageEmptyIcon); + } else if (usageInMegabytes > 0.1 && usageInMegabytes <= 5) { + usageIcon.setImageBitmap(mUsageLowIcon); + } else if (usageInMegabytes > 5) { + usageIcon.setImageBitmap(mUsageHighIcon); + } + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view; + final TextView title; + final TextView subtitle; + final ImageView icon; + final ImageView usageIcon; + final ImageView locationIcon; + final ImageView featureIcon; + + if (convertView == null) { + view = mInflater.inflate(mResource, parent, false); + } else { + view = convertView; + } + + title = (TextView) view.findViewById(R.id.title); + subtitle = (TextView) view.findViewById(R.id.subtitle); + icon = (ImageView) view.findViewById(R.id.icon); + featureIcon = (ImageView) view.findViewById(R.id.feature_icon); + usageIcon = (ImageView) view.findViewById(R.id.usage_icon); + locationIcon = (ImageView) view.findViewById(R.id.location_icon); + usageIcon.setVisibility(View.GONE); + locationIcon.setVisibility(View.GONE); + + if (mCurrentSite == null) { + + Site site = getItem(position); + title.setText(site.getPrettyTitle()); + String subtitleText = site.getPrettyOrigin(); + if (subtitleText != null) { + title.setMaxLines(1); + title.setSingleLine(true); + subtitle.setVisibility(View.VISIBLE); + subtitle.setText(subtitleText); + } else { + subtitle.setVisibility(View.GONE); + title.setMaxLines(2); + title.setSingleLine(false); + } + + icon.setVisibility(View.VISIBLE); + usageIcon.setVisibility(View.INVISIBLE); + locationIcon.setVisibility(View.INVISIBLE); + featureIcon.setVisibility(View.GONE); + Bitmap bmp = site.getIcon(); + if (bmp == null) { + bmp = mDefaultIcon; + } + icon.setImageBitmap(bmp); + // We set the site as the view's tag, + // so that we can get it in onItemClick() + view.setTag(site); + + String origin = site.getOrigin(); + if (site.hasFeature(Site.FEATURE_WEB_STORAGE)) { + WebStorage.getInstance().getUsageForOrigin(origin, new ValueCallback<Long>() { + public void onReceiveValue(Long value) { + if (value != null) { + setIconForUsage(usageIcon, value.longValue()); + usageIcon.setVisibility(View.VISIBLE); + } + } + }); + } + + if (site.hasFeature(Site.FEATURE_GEOLOCATION)) { + locationIcon.setVisibility(View.VISIBLE); + GeolocationPermissions.getInstance().getAllowed(origin, new ValueCallback<Boolean>() { + public void onReceiveValue(Boolean allowed) { + if (allowed != null) { + if (allowed.booleanValue()) { + locationIcon.setImageBitmap(mLocationAllowedIcon); + } else { + locationIcon.setImageBitmap(mLocationDisallowedIcon); + } + } + } + }); + } + } else { + icon.setVisibility(View.GONE); + locationIcon.setVisibility(View.GONE); + usageIcon.setVisibility(View.GONE); + featureIcon.setVisibility(View.VISIBLE); + String origin = mCurrentSite.getOrigin(); + switch (mCurrentSite.getFeatureByIndex(position)) { + case Site.FEATURE_WEB_STORAGE: + WebStorage.getInstance().getUsageForOrigin(origin, new ValueCallback<Long>() { + public void onReceiveValue(Long value) { + if (value != null) { + String usage = sizeValueToString(value.longValue()) + " " + sMBStored; + title.setText(R.string.webstorage_clear_data_title); + subtitle.setText(usage); + subtitle.setVisibility(View.VISIBLE); + setIconForUsage(featureIcon, value.longValue()); + } + } + }); + break; + case Site.FEATURE_GEOLOCATION: + title.setText(R.string.geolocation_settings_page_title); + GeolocationPermissions.getInstance().getAllowed(origin, new ValueCallback<Boolean>() { + public void onReceiveValue(Boolean allowed) { + if (allowed != null) { + if (allowed.booleanValue()) { + subtitle.setText(R.string.geolocation_settings_page_summary_allowed); + featureIcon.setImageBitmap(mLocationAllowedIcon); + } else { + subtitle.setText(R.string.geolocation_settings_page_summary_not_allowed); + featureIcon.setImageBitmap(mLocationDisallowedIcon); + } + subtitle.setVisibility(View.VISIBLE); + } + } + }); + break; + } + } + + return view; + } + + public void onItemClick(AdapterView<?> parent, + View view, + int position, + long id) { + if (mCurrentSite != null) { + switch (mCurrentSite.getFeatureByIndex(position)) { + case Site.FEATURE_WEB_STORAGE: + new AlertDialog.Builder(getContext()) + .setTitle(R.string.webstorage_clear_data_dialog_title) + .setMessage(R.string.webstorage_clear_data_dialog_message) + .setPositiveButton(R.string.webstorage_clear_data_dialog_ok_button, + new AlertDialog.OnClickListener() { + public void onClick(DialogInterface dlg, int which) { + WebStorage.getInstance().deleteOrigin(mCurrentSite.getOrigin()); + // If this site has no more features, then go back to the + // origins list. + mCurrentSite.removeFeature(Site.FEATURE_WEB_STORAGE); + if (mCurrentSite.getFeatureCount() == 0) { + finish(); + } + askForOrigins(); + notifyDataSetChanged(); + }}) + .setNegativeButton(R.string.webstorage_clear_data_dialog_cancel_button, null) + .setIcon(android.R.drawable.ic_dialog_alert) + .show(); + break; + case Site.FEATURE_GEOLOCATION: + new AlertDialog.Builder(getContext()) + .setTitle(R.string.geolocation_settings_page_dialog_title) + .setMessage(R.string.geolocation_settings_page_dialog_message) + .setPositiveButton(R.string.geolocation_settings_page_dialog_ok_button, + new AlertDialog.OnClickListener() { + public void onClick(DialogInterface dlg, int which) { + GeolocationPermissions.getInstance().clear(mCurrentSite.getOrigin()); + mCurrentSite.removeFeature(Site.FEATURE_GEOLOCATION); + if (mCurrentSite.getFeatureCount() == 0) { + finish(); + } + askForOrigins(); + notifyDataSetChanged(); + }}) + .setNegativeButton(R.string.geolocation_settings_page_dialog_cancel_button, null) + .setIcon(android.R.drawable.ic_dialog_alert) + .show(); + break; + } + } else { + Site site = (Site) view.getTag(); + PreferenceActivity activity = (PreferenceActivity) getActivity(); + if (activity != null) { + Bundle args = new Bundle(); + args.putSerializable(EXTRA_SITE, site); + activity.startPreferencePanel(WebsiteSettingsFragment.class.getName(), args, 0, + site.getPrettyTitle(), null, 0); + } + } + } + + public Site currentSite() { + return mCurrentSite; + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.website_settings, container, false); + Bundle args = getArguments(); + if (args != null) { + mSite = (Site) args.getSerializable(EXTRA_SITE); + } + if (mSite == null) { + View clear = view.findViewById(R.id.clear_all_button); + clear.setVisibility(View.VISIBLE); + clear.setOnClickListener(this); + } + return view; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if (sMBStored == null) { + sMBStored = getString(R.string.webstorage_origin_summary_mb_stored); + } + mAdapter = new SiteAdapter(getActivity(), R.layout.website_settings_row); + if (mSite != null) { + mAdapter.mCurrentSite = mSite; + } + getListView().setAdapter(mAdapter); + getListView().setOnItemClickListener(mAdapter); + } + + private void finish() { + PreferenceActivity activity = (PreferenceActivity) getActivity(); + if (activity != null) { + activity.finishPreferencePanel(this, 0, null); + } + } + + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.clear_all_button: + // Show the prompt to clear all origins of their data and geolocation permissions. + new AlertDialog.Builder(getActivity()) + .setTitle(R.string.website_settings_clear_all_dialog_title) + .setMessage(R.string.website_settings_clear_all_dialog_message) + .setPositiveButton(R.string.website_settings_clear_all_dialog_ok_button, + new AlertDialog.OnClickListener() { + public void onClick(DialogInterface dlg, int which) { + WebStorage.getInstance().deleteAllData(); + GeolocationPermissions.getInstance().clearAll(); + WebStorageSizeManager.resetLastOutOfSpaceNotificationTime(); + mAdapter.askForOrigins(); + finish(); + }}) + .setNegativeButton(R.string.website_settings_clear_all_dialog_cancel_button, null) + .setIcon(android.R.drawable.ic_dialog_alert) + .show(); + break; + } + } +} |