/*
WhitelistFragment.java
Copyright (c) 2015 NTT DOCOMO,INC.
Released under the MIT license
http://opensource.org/licenses/mit-license.php
*/
package org.deviceconnect.android.manager.policy;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.deviceconnect.android.manager.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Fragment;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
/**
* Whitelist fragment.
*
* @author NTT DOCOMO, INC.
*/
public class WhitelistFragment extends Fragment {
/** The logger. */
private Logger mLogger = Logger.getLogger("dconnect.manager");
/** The whitelist of origins. */
private Whitelist mWhitelist;
/** The root view. */
private View mRootView;
/** The instance of {@link OriginListAdapter}. */
private OriginListAdapter mListAdapter;
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
mWhitelist = new Whitelist(getActivity());
}
@Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
mListAdapter = new OriginListAdapter(getActivity(), mWhitelist.getOrigins());
mRootView = inflater.inflate(R.layout.fragment_whitelist, container, false);
ListView listView = (ListView) mRootView.findViewById(R.id.listview_whitelist);
listView.setAdapter(mListAdapter);
refreshView();
return mRootView;
}
/**
* Refreshes views.
*/
private void refreshView() {
View commentView = mRootView.findViewById(R.id.view_no_origin);
if (mWhitelist.getOrigins().size() == 0) {
commentView.setVisibility(View.VISIBLE);
} else {
commentView.setVisibility(View.GONE);
}
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
if (android.R.id.home == item.getItemId()) {
getActivity().finish();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
menu.clear();
final MenuItem menuItem = menu.add(R.string.menu_add_origin);
menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
menuItem.setOnMenuItemClickListener(new OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(final MenuItem item) {
if (item.getTitle().equals(menuItem.getTitle())) {
openAddDialog();
}
return true;
}
});
}
/**
* Origin Pattern List Adapter.
*/
private class OriginListAdapter extends ArrayAdapter<OriginInfo> {
/** The instance of {@link LayoutInflater}. */
LayoutInflater mInflater;
/** The list of origins. */
List<OriginInfo> mPatternList;
/**
* Constructor.
*
* @param context Context
* @param origins The list of origins.
*/
OriginListAdapter(final Context context, final List<OriginInfo> origins) {
super(context, 0, origins);
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mPatternList = origins;
}
/**
* Sets item at the specified index.
*
* @param index an index in this list
* @param item an instance of {@link OriginInfo}
*/
void setItem(final int index, final OriginInfo item) {
mPatternList.set(index, item);
}
@Override
public int getCount() {
return mPatternList.size();
}
@Override
public View getView(final int position, final View convertView, final ViewGroup parent) {
final OriginInfo info = (OriginInfo) getItem(position);
View view = convertView;
if (view == null) {
view = mInflater.inflate(R.layout.item_whitelist_origin, (ViewGroup) null);
}
final TextView textViewTitle = (TextView) view.findViewById(R.id.text_origin_title);
textViewTitle.setText(info.getTitle());
final TextView textViewOrigin = (TextView) view.findViewById(R.id.text_origin);
textViewOrigin.setText(info.getOrigin().toString());
Button buttonDelete = (Button) view.findViewById(R.id.button_delete_origin);
buttonDelete.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
openDeleteDialog(info);
}
});
view.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(final View v) {
Context context = getActivity();
if (context == null) {
return false;
}
ManualEntryDialogBuilder builder = new ManualEntryDialogBuilder(context);
builder.mDialogTitle = getString(R.string.dialog_update_origin_title);
builder.mDefaultOrigin = info.getOrigin().toString();
builder.mDefaultOriginTitle = info.getTitle();
builder.mPositiveButtonName = getString(R.string.dialog_update_origin_positive);
builder.mNegativeButtonName = getString(R.string.dialog_update_origin_negative);
builder.mListener = new OnEntryListener() {
@Override
public void onEntry(final String newOriginExp, final String newTitle) {
try {
Origin newOrigin = OriginParser.parse(newOriginExp);
OriginInfo newItem = new OriginInfo(info.mId, newOrigin, newTitle, info.mDate);
updateOrigin(position, newItem);
mLogger.info("Updated origin=" + newOrigin.toString() + " title=" + newTitle);
} catch (WhitelistException e) {
mLogger.log(Level.WARNING, "Failed to update origin.", e);
showPopup(e.getMessage());
}
}
};
builder.create().show();
return true;
}
});
return view;
}
}
/**
* Shows a popup on the screen of Android device.
*
* @param text the text message.
*/
private void showPopup(final String text) {
final Activity activity = getActivity();
if (activity == null) {
return;
}
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(activity, text, Toast.LENGTH_LONG).show();
}
});
}
/**
* Deletes an origin.
*
* @param origin an origin.
* @throws WhitelistException if the origin can not be removed.
*/
private void deleteOrigin(final OriginInfo origin) throws WhitelistException {
mWhitelist.removeOrigin(origin);
mListAdapter.remove(origin);
mListAdapter.notifyDataSetChanged();
refreshView();
}
/**
* Adds an origin to be allowed.
*
* @param originExp an string expression of an origin to be allowed.
* @param title the title of origin.
* @throws WhitelistException if the origin can not be stored.
*/
private void addOrigin(final String originExp, final String title) throws WhitelistException {
Origin origin = OriginParser.parse(originExp);
OriginInfo info = mWhitelist.addOrigin(origin, title);
mListAdapter.add(info);
mListAdapter.notifyDataSetChanged();
refreshView();
}
/**
* Updates an origin to be allowed.
*
* @param index an index in whitelist
* @param info the information of an origin
* @throws WhitelistException if the origin can not be stored.
*/
private void updateOrigin(final int index, final OriginInfo info) throws WhitelistException {
mWhitelist.updateOrigin(info);
mListAdapter.setItem(index, info);
mListAdapter.notifyDataSetChanged();
refreshView();
}
/**
* Opens the origin deletion dialog.
*
* @param origin an origin to be deleted.
*/
private void openDeleteDialog(final OriginInfo origin) {
String strGuidance = getString(R.string.dialog_delete_origin_message);
String strPositive = getString(R.string.dialog_delete_origin_positive);
String strNegative = getString(R.string.dialog_delete_origin_negative);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(origin.getTitle());
builder.setMessage(strGuidance).setPositiveButton(strPositive, new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
try {
deleteOrigin(origin);
} catch (WhitelistException e) {
showPopup(e.getMessage());
}
}
}).setNegativeButton(strNegative, new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
}
}).setCancelable(true);
builder.create().show();
}
/**
* Opens the origin addition dialog.
*/
private void openAddDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setCancelable(true);
builder.setTitle(R.string.dialog_add_origin_title);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
builder.setItems(R.array.whitelist_menu_origin_pre_marshmallow, new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
dialog.dismiss();
Context context = getActivity();
if (context == null) {
return;
}
switch (which) {
case 0:
openListDialog(context, getAllBookmarksOfChrome());
break;
case 1:
openListDialog(context, getInstalledNativeApplications());
break;
case 2:
ManualEntryDialogBuilder builder = new ManualEntryDialogBuilder(context);
builder.mDialogTitle = getString(R.string.dialog_add_origin_title);
builder.mDefaultOrigin = "";
builder.mDefaultOriginTitle = "";
builder.mPositiveButtonName = getString(R.string.dialog_add_origin_positive);
builder.mNegativeButtonName = getString(R.string.dialog_add_origin_negative);
builder.mListener = new OnEntryListener() {
@Override
public void onEntry(final String origin, final String title) {
try {
addOrigin(origin, title);
mLogger.info("Updated origin=" + origin + " title=" + title);
} catch (WhitelistException e) {
mLogger.log(Level.WARNING, "Failed to add origin.", e);
showPopup(e.getMessage());
}
}
};
builder.create().show();
break;
default:
// nothing to do
break;
}
}
});
} else {
builder.setItems(R.array.whitelist_menu_origin_post_marshmallow, new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
dialog.dismiss();
Context context = getActivity();
if (context == null) {
return;
}
switch (which) {
case 0:
openListDialog(context, getInstalledNativeApplications());
break;
case 1:
ManualEntryDialogBuilder builder = new ManualEntryDialogBuilder(context);
builder.mDialogTitle = getString(R.string.dialog_add_origin_title);
builder.mDefaultOrigin = "";
builder.mDefaultOriginTitle = "";
builder.mPositiveButtonName = getString(R.string.dialog_add_origin_positive);
builder.mNegativeButtonName = getString(R.string.dialog_add_origin_negative);
builder.mListener = new OnEntryListener() {
@Override
public void onEntry(final String origin, final String title) {
try {
addOrigin(origin, title);
mLogger.info("Updated origin=" + origin + " title=" + title);
} catch (WhitelistException e) {
mLogger.log(Level.WARNING, "Failed to add origin.", e);
showPopup(e.getMessage());
}
}
};
builder.create().show();
break;
default:
// nothing to do
break;
}
}
});
}
builder.create().show();
}
/**
* Opens a dialog for the list of known application information.
*
* @param context the context
* @param list the list of known application information.
*/
private void openListDialog(final Context context, final List<KnownApplicationInfo> list) {
ApplicationListAdapter adapter = new ApplicationListAdapter(getActivity(), list);
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.dialog_add_origin_title);
builder.setSingleChoiceItems(adapter, 0, new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
try {
KnownApplicationInfo appInfo = list.get(which);
addOrigin(appInfo.getOrigin(), appInfo.getName());
mLogger.info("Updated origin=" + appInfo.getOrigin() + " title=" + appInfo.getName());
} catch (WhitelistException e) {
mLogger.log(Level.WARNING, "Failed to add origin.", e);
showPopup(e.getMessage());
} finally {
dialog.dismiss();
}
}
});
builder.setCancelable(true);
builder.create().show();
};
/**
* Gets all bookmarks of Chrome for Android.
*
* This method works properly only in SDK version 22 or below.
*
* @return all bookmarks of Chrome for Android
*/
private List<KnownApplicationInfo> getAllBookmarksOfChrome() {
String columnTitle = "title";
String columnURL = "url";
String columnFavicon = "favicon";
String selectionBookmark = "bookmark";
ContentResolver resolver = getActivity().getContentResolver();
Uri uri = Uri.parse(getString(R.string.chrome_bookmark_provider_url));
Cursor c = resolver.query(uri, new String[] { columnTitle, columnURL, columnFavicon }, selectionBookmark, null,
null);
List<KnownApplicationInfo> bookmarks = new ArrayList<WhitelistFragment.KnownApplicationInfo>();
if (c.moveToFirst()) {
do {
final int titleIndex = c.getColumnIndex(columnTitle);
final int urlIndex = c.getColumnIndex(columnURL);
final int iconIndex = c.getColumnIndex(columnFavicon);
if (titleIndex < 0 || urlIndex < 0 || iconIndex < 0) {
continue;
}
final String title = c.getString(titleIndex);
final String url = c.getString(urlIndex);
final byte[] icon = c.getBlob(iconIndex);
KnownApplicationInfo info = new BookmarkInfo(title, url, icon);
if (info.getOrigin() != null) {
bookmarks.add(info);
}
} while (c.moveToNext());
}
c.close();
return bookmarks;
}
/**
* Gets information of all applications installed in the Android device.
*
* @return information of all applications installed
*/
private List<KnownApplicationInfo> getInstalledNativeApplications() {
PackageManager pm = getActivity().getPackageManager();
List<ApplicationInfo> apps = pm.getInstalledApplications(0);
List<KnownApplicationInfo> result = new ArrayList<KnownApplicationInfo>();
for (ApplicationInfo app : apps) {
result.add(new InstalledNativeApplicationInfo(app));
}
return result;
}
/**
* Information of known application by the device.
*/
private interface KnownApplicationInfo {
/**
* Gets the name of the application.
*
* @return the name of the application
*/
String getName();
/**
* Gets the origin of the application.
*
* @return the origin of the application. if origin cannot be derived,
* returns <code>null</code>
*/
String getOrigin();
/**
* Gets binary data of the icon.
*
* @return binary data of the icon. if the icon is not registered,
* returns <code>null</code>
*/
Drawable getIcon();
}
/**
* Information of book-mark.
*/
private class BookmarkInfo implements KnownApplicationInfo {
/** The title of the web site. */
final String mTitle;
/** The URL of the web site. */
final String mUrl;
/** The binary data of the favicon. */
byte[] mIcon;
/**
* Constructor.
*
* @param title The title of the web site
* @param url The URL of the web site
* @param icon The binary data of the favicon
*/
BookmarkInfo(final String title, final String url, final byte[] icon) {
mTitle = title;
mUrl = url;
mIcon = icon;
}
@Override
public String getName() {
return mTitle;
}
@Override
public String getOrigin() {
try {
URI uri = new URI(mUrl);
StringBuilder origin = new StringBuilder();
origin.append(uri.getScheme());
origin.append("://");
if (uri.getHost() != null) {
origin.append(uri.getHost());
}
if (uri.getPort() > -1) {
origin.append(":");
origin.append(uri.getPort());
}
return origin.toString();
} catch (URISyntaxException e) {
return null;
}
}
@Override
public Drawable getIcon() {
if (mIcon == null) {
return null;
}
return new BitmapDrawable(BitmapFactory.decodeByteArray(mIcon, 0, mIcon.length));
}
}
/**
* Information of installed native application in the Android device.
*/
private class InstalledNativeApplicationInfo implements KnownApplicationInfo {
/** The Information of the installed native application. */
final ApplicationInfo mAppInfo;
/**
* Constructor.
*
* @param info The Information of the installed native application
*/
InstalledNativeApplicationInfo(final ApplicationInfo info) {
mAppInfo = info;
}
@Override
public String getName() {
return mAppInfo.loadLabel(getActivity().getPackageManager()).toString();
}
@Override
public String getOrigin() {
return mAppInfo.packageName;
}
@Override
public Drawable getIcon() {
return getActivity().getPackageManager().getApplicationIcon(mAppInfo);
}
}
/**
* Application List Adapter.
*/
private class ApplicationListAdapter extends ArrayAdapter<KnownApplicationInfo> {
/** The instance of {@link LayoutInflater}. */
LayoutInflater mInflater;
/** The list of application. */
List<KnownApplicationInfo> mList;
/**
* Constructor.
*
* @param context Context
* @param list The list of book-marks
*/
ApplicationListAdapter(final Context context, final List<KnownApplicationInfo> list) {
super(context, 0, list);
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mList = list;
}
@Override
public int getCount() {
return mList.size();
}
@Override
public View getView(final int position, final View convertView, final ViewGroup parent) {
final KnownApplicationInfo info = (KnownApplicationInfo) getItem(position);
View view = convertView;
if (view == null) {
view = mInflater.inflate(R.layout.item_known_applications, (ViewGroup) null);
}
ImageView imageIcon = (ImageView) view.findViewById(R.id.image_app_icon);
Drawable icon = info.getIcon();
if (icon == null) {
icon = new BitmapDrawable(BitmapFactory.decodeByteArray(new byte[] {}, 0, 0));
}
imageIcon.setImageDrawable(icon);
TextView textTitle = (TextView) view.findViewById(R.id.text_app_title);
textTitle.setText(info.getName());
TextView textOrigin = (TextView) view.findViewById(R.id.text_app_orgin);
textOrigin.setText(info.getOrigin());
return view;
}
}
/**
* A builder of manual entry dialog.
*/
private static class ManualEntryDialogBuilder {
/** An instance of {@link AlertDialog.Builder}. */
final AlertDialog.Builder mBuilder;
/** An instance of {@link Context}. */
final Context mContext;
/** The title of dialog. */
String mDialogTitle;
/** The default origin. */
String mDefaultOrigin;
/** The default title of origin. */
String mDefaultOriginTitle;
/** The name of positive button on dialog. */
String mPositiveButtonName;
/** The name of negative button on dialog. */
String mNegativeButtonName;
/** An instance of {@link OnEntryListener}. */
OnEntryListener mListener;
/**
* Constructor.
*
* @param context an instance of {@link Context}
*/
ManualEntryDialogBuilder(final Context context) {
mBuilder = new AlertDialog.Builder(context);
mContext = context;
}
/**
* Creates an dialog.
*
* @return an instance of {@link AlertDialog}
*/
AlertDialog create() {
final int layoutId = R.layout.dialog_origin_manual_entry;
final View root = LayoutInflater.from(mContext).inflate(layoutId, null);
final EditText editOrigin = (EditText) root.findViewById(R.id.dialog_origin_manual_entry_edit_origin);
final EditText editTitle = (EditText) root.findViewById(R.id.dialog_origin_manual_entry_edit_title);
editOrigin.setText(mDefaultOrigin);
editTitle.setText(mDefaultOriginTitle);
mBuilder.setTitle(mDialogTitle);
mBuilder.setView(root);
mBuilder.setPositiveButton(mPositiveButtonName, new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
String origin = editOrigin.getText().toString();
String title = editTitle.getText().toString();
if (TextUtils.isEmpty(origin)) {
create().show();
return;
}
if (TextUtils.isEmpty(title)) {
title = origin;
}
if (mListener != null) {
mListener.onEntry(origin, title);
}
}
});
mBuilder.setNegativeButton(mNegativeButtonName, new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
}
});
mBuilder.setCancelable(true);
return mBuilder.create();
}
}
/**
* A listener for manual entry events.
*/
private interface OnEntryListener {
/**
* Receives a manual entry event.
*
* @param origin the string expression of an origin
* @param title the title of an origin
*/
void onEntry(String origin, String title);
}
}