Java tutorial
/** * <p/> * Copyright (c) 2017 Viktor Rudometkin * Copyright (c) 2012-2015 Frederic Julian * <p/> * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * <p/> * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * <p/> * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * <p/> * <p/> * Some parts of this software are based on "Sparse rss" under the MIT license (see * below). Please refers to the original project to identify which parts are under the * MIT license. * <p/> * Copyright (c) 2010-2012 Stefan Handschuh * <p/> * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * <p/> * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * <p/> * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.viktorrudometkin.burramys.fragment; import android.Manifest; import android.app.Activity; import android.app.AlertDialog; import android.content.ContentResolver; import android.content.ContentValues; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.Environment; import android.support.v4.app.ActivityCompat; import android.support.v4.app.ListFragment; import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; import android.support.v7.view.ActionMode; import android.util.Pair; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemLongClickListener; import android.widget.EditText; import android.widget.ExpandableListView; import android.widget.ListView; import android.widget.TextView; import com.viktorrudometkin.burramys.MainApplication; import com.viktorrudometkin.burramys.R; import com.viktorrudometkin.burramys.activity.AddGoogleNewsActivity; import com.viktorrudometkin.burramys.adapter.FeedsCursorAdapter; import com.viktorrudometkin.burramys.parser.OPML; import com.viktorrudometkin.burramys.provider.FeedData.FeedColumns; import com.viktorrudometkin.burramys.utils.UiUtils; import com.viktorrudometkin.burramys.view.DragNDropExpandableListView; import com.viktorrudometkin.burramys.view.DragNDropListener; import java.io.File; import java.io.FilenameFilter; import java.util.regex.Matcher; import java.util.regex.Pattern; public class EditFeedsListFragment extends ListFragment { private static final int REQUEST_PICK_OPML_FILE = 1; private static final int PERMISSIONS_REQUEST_IMPORT_FROM_OPML = 1; private static final int PERMISSIONS_REQUEST_EXPORT_TO_OPML = 2; private final ActionMode.Callback mFeedActionModeCallback = new ActionMode.Callback() { // Called when the action mode is created; startActionMode() was called @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { // Inflate a menu resource providing context menu items MenuInflater inflater = mode.getMenuInflater(); inflater.inflate(R.menu.feed_context_menu, menu); return true; } // Called each time the action mode is shown. Always called after onCreateActionMode, but // may be called multiple times if the mode is invalidated. @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { return false; // Return false if nothing is done } // Called when the user selects a contextual menu item @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { @SuppressWarnings("unchecked") Pair<Long, String> tag = (Pair<Long, String>) mode.getTag(); final long feedId = tag.first; final String title = tag.second; switch (item.getItemId()) { case R.id.menu_edit: startActivity(new Intent(Intent.ACTION_EDIT).setData(FeedColumns.CONTENT_URI(feedId))); mode.finish(); // Action picked, so close the CAB return true; case R.id.menu_delete: new AlertDialog.Builder(getActivity()) // .setIcon(android.R.drawable.ic_dialog_alert) // .setTitle(title) // .setMessage(R.string.question_delete_feed) // .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { new Thread() { @Override public void run() { ContentResolver cr = getActivity().getContentResolver(); cr.delete(FeedColumns.CONTENT_URI(feedId), null, null); } }.start(); } }).setNegativeButton(android.R.string.no, null).show(); mode.finish(); // Action picked, so close the CAB return true; default: return false; } } // Called when the user exits the action mode @Override public void onDestroyActionMode(ActionMode mode) { for (int i = 0; i < mListView.getCount(); i++) { mListView.setItemChecked(i, false); } } }; private final ActionMode.Callback mGroupActionModeCallback = new ActionMode.Callback() { // Called when the action mode is created; startActionMode() was called @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { // Inflate a menu resource providing context menu items MenuInflater inflater = mode.getMenuInflater(); inflater.inflate(R.menu.edit_context_menu, menu); return true; } // Called each time the action mode is shown. Always called after onCreateActionMode, but // may be called multiple times if the mode is invalidated. @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { return false; // Return false if nothing is done } // Called when the user selects a contextual menu item @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { @SuppressWarnings("unchecked") Pair<Long, String> tag = (Pair<Long, String>) mode.getTag(); final long groupId = tag.first; final String title = tag.second; switch (item.getItemId()) { case R.id.menu_edit: final EditText input = new EditText(getActivity()); input.setSingleLine(true); input.setText(title); new AlertDialog.Builder(getActivity()) // .setTitle(R.string.edit_group_title) // .setView(input) // .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { new Thread() { @Override public void run() { String groupName = input.getText().toString(); if (!groupName.isEmpty()) { ContentResolver cr = getActivity().getContentResolver(); ContentValues values = new ContentValues(); values.put(FeedColumns.NAME, groupName); cr.update(FeedColumns.CONTENT_URI(groupId), values, null, null); } } }.start(); } }).setNegativeButton(android.R.string.cancel, null).show(); mode.finish(); // Action picked, so close the CAB return true; case R.id.menu_delete: new AlertDialog.Builder(getActivity()) // .setIcon(android.R.drawable.ic_dialog_alert) // .setTitle(title) // .setMessage(R.string.question_delete_group) // .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { new Thread() { @Override public void run() { ContentResolver cr = getActivity().getContentResolver(); cr.delete(FeedColumns.GROUPS_CONTENT_URI(groupId), null, null); } }.start(); } }).setNegativeButton(android.R.string.no, null).show(); mode.finish(); // Action picked, so close the CAB return true; default: return false; } } // Called when the user exits the action mode @Override public void onDestroyActionMode(ActionMode mode) { for (int i = 0; i < mListView.getCount(); i++) { mListView.setItemChecked(i, false); } } }; private DragNDropExpandableListView mListView; @Override public void onCreate(Bundle savedInstanceState) { setHasOptionsMenu(true); super.onCreate(savedInstanceState); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_edit_feed_list, container, false); mListView = (DragNDropExpandableListView) rootView.findViewById(android.R.id.list); mListView.setFastScrollEnabled(true); mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); mListView.setOnChildClickListener(new ExpandableListView.OnChildClickListener() { @Override public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { startActivity(new Intent(Intent.ACTION_EDIT).setData(FeedColumns.CONTENT_URI(id))); return true; } }); mListView.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() { @Override public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id) { if (v.findViewById(R.id.indicator).getVisibility() != View.VISIBLE) { // This is no a real group startActivity(new Intent(Intent.ACTION_EDIT).setData(FeedColumns.CONTENT_URI(id))); return true; } return false; } }); mListView.setOnItemLongClickListener(new OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { AppCompatActivity activity = (AppCompatActivity) getActivity(); if (activity != null) { String title = ((TextView) view.findViewById(android.R.id.text1)).getText().toString(); Matcher m = Pattern.compile("(.*) \\([0-9]+\\)$").matcher(title); if (m.matches()) { title = m.group(1); } long feedId = mListView.getItemIdAtPosition(position); ActionMode actionMode; if (view.findViewById(R.id.indicator).getVisibility() == View.VISIBLE) { // This is a group actionMode = activity.startSupportActionMode(mGroupActionModeCallback); } else { // This is a feed actionMode = activity.startSupportActionMode(mFeedActionModeCallback); } actionMode.setTag(new Pair<>(feedId, title)); mListView.setItemChecked(position, true); } return true; } }); mListView.setAdapter(new FeedsCursorAdapter(getActivity(), FeedColumns.GROUPS_CONTENT_URI)); mListView.setDragNDropListener(new DragNDropListener() { boolean fromHasGroupIndicator = false; @Override public void onStopDrag(View itemView) { } @Override public void onStartDrag(View itemView) { fromHasGroupIndicator = itemView.findViewById(R.id.indicator).getVisibility() == View.VISIBLE; } @Override public void onDrop(final int flatPosFrom, final int flatPosTo) { final boolean fromIsGroup = ExpandableListView.getPackedPositionType(mListView .getExpandableListPosition(flatPosFrom)) == ExpandableListView.PACKED_POSITION_TYPE_GROUP; final boolean toIsGroup = ExpandableListView.getPackedPositionType(mListView .getExpandableListPosition(flatPosTo)) == ExpandableListView.PACKED_POSITION_TYPE_GROUP; final boolean fromIsFeedWithoutGroup = fromIsGroup && !fromHasGroupIndicator; View toView = mListView.getChildAt(flatPosTo - mListView.getFirstVisiblePosition()); boolean toIsFeedWithoutGroup = toIsGroup && toView.findViewById(R.id.indicator).getVisibility() != View.VISIBLE; final long packedPosTo = mListView.getExpandableListPosition(flatPosTo); final int packedGroupPosTo = ExpandableListView.getPackedPositionGroup(packedPosTo); if ((fromIsFeedWithoutGroup || !fromIsGroup) && toIsGroup && !toIsFeedWithoutGroup) { new AlertDialog.Builder(getActivity()) // .setTitle(R.string.to_group_title) // .setMessage(R.string.to_group_message) // .setPositiveButton(R.string.to_group_into, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { ContentValues values = new ContentValues(); values.put(FeedColumns.PRIORITY, 1); values.put(FeedColumns.GROUP_ID, mListView.getItemIdAtPosition(flatPosTo)); ContentResolver cr = getActivity().getContentResolver(); cr.update(FeedColumns.CONTENT_URI(mListView.getItemIdAtPosition(flatPosFrom)), values, null, null); cr.notifyChange(FeedColumns.GROUPS_CONTENT_URI, null); } }).setNegativeButton(R.string.to_group_above, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { moveItem(fromIsGroup, toIsGroup, fromIsFeedWithoutGroup, packedPosTo, packedGroupPosTo, flatPosFrom); } }).show(); } else { moveItem(fromIsGroup, toIsGroup, fromIsFeedWithoutGroup, packedPosTo, packedGroupPosTo, flatPosFrom); } } @Override public void onDrag(int x, int y, ListView listView) { } }); return rootView; } private void moveItem(boolean fromIsGroup, boolean toIsGroup, boolean fromIsFeedWithoutGroup, long packedPosTo, int packedGroupPosTo, int flatPosFrom) { ContentValues values = new ContentValues(); ContentResolver cr = getActivity().getContentResolver(); if (fromIsGroup && toIsGroup) { values.put(FeedColumns.PRIORITY, packedGroupPosTo + 1); cr.update(FeedColumns.CONTENT_URI(mListView.getItemIdAtPosition(flatPosFrom)), values, null, null); } else if (!fromIsGroup && toIsGroup) { values.put(FeedColumns.PRIORITY, packedGroupPosTo + 1); values.putNull(FeedColumns.GROUP_ID); cr.update(FeedColumns.CONTENT_URI(mListView.getItemIdAtPosition(flatPosFrom)), values, null, null); } else if ((!fromIsGroup && !toIsGroup) || (fromIsFeedWithoutGroup && !toIsGroup)) { int groupPrio = ExpandableListView.getPackedPositionChild(packedPosTo) + 1; values.put(FeedColumns.PRIORITY, groupPrio); int flatGroupPosTo = mListView .getFlatListPosition(ExpandableListView.getPackedPositionForGroup(packedGroupPosTo)); values.put(FeedColumns.GROUP_ID, mListView.getItemIdAtPosition(flatGroupPosTo)); cr.update(FeedColumns.CONTENT_URI(mListView.getItemIdAtPosition(flatPosFrom)), values, null, null); } } @Override public void onDestroy() { getLoaderManager().destroyLoader(0); // This is needed to avoid an activity leak! super.onDestroy(); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.feed_list, menu); super.onCreateOptionsMenu(menu, inflater); } @Override public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.menu_add_feed: { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle(R.string.menu_add_feed).setItems(new CharSequence[] { getString(R.string.add_custom_feed), getString(R.string.google_news_title) }, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { if (which == 0) { startActivity(new Intent(Intent.ACTION_INSERT).setData(FeedColumns.CONTENT_URI)); } else { startActivity(new Intent(getActivity(), AddGoogleNewsActivity.class)); } } }); builder.show(); return true; } case R.id.menu_add_group: { final EditText input = new EditText(getActivity()); input.setSingleLine(true); new AlertDialog.Builder(getActivity()) // .setTitle(R.string.add_group_title) // .setView(input) // // .setMessage(R.string.add_group_sentence) // .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { new Thread() { @Override public void run() { String groupName = input.getText().toString(); if (!groupName.isEmpty()) { ContentResolver cr = getActivity().getContentResolver(); ContentValues values = new ContentValues(); values.put(FeedColumns.IS_GROUP, true); values.put(FeedColumns.NAME, groupName); cr.insert(FeedColumns.GROUPS_CONTENT_URI, values); } } }.start(); } }).setNegativeButton(android.R.string.cancel, null).show(); return true; } case R.id.menu_export: case R.id.menu_import: { if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // Should we show an explanation? if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE)) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setMessage(R.string.storage_request_explanation) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { if (item.getItemId() == R.id.menu_export) { ActivityCompat.requestPermissions(getActivity(), new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, PERMISSIONS_REQUEST_EXPORT_TO_OPML); } else if (item.getItemId() == R.id.menu_import) { ActivityCompat.requestPermissions(getActivity(), new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, PERMISSIONS_REQUEST_IMPORT_FROM_OPML); } } }).setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { // User cancelled the dialog } }); builder.show(); } else { // No explanation needed, we can request the permission. if (item.getItemId() == R.id.menu_export) { ActivityCompat.requestPermissions(getActivity(), new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, PERMISSIONS_REQUEST_EXPORT_TO_OPML); } else if (item.getItemId() == R.id.menu_import) { ActivityCompat.requestPermissions(getActivity(), new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, PERMISSIONS_REQUEST_IMPORT_FROM_OPML); } } } else { if (item.getItemId() == R.id.menu_export) { exportToOpml(); } else if (item.getItemId() == R.id.menu_import) { importFromOpml(); } } return true; } } return super.onOptionsItemSelected(item); } @Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { case PERMISSIONS_REQUEST_EXPORT_TO_OPML: { // If request is cancelled, the result arrays are empty. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { exportToOpml(); } return; } case PERMISSIONS_REQUEST_IMPORT_FROM_OPML: { // If request is cancelled, the result arrays are empty. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { importFromOpml(); } return; } } } @Override public void onActivityResult(int requestCode, int resultCode, final Intent data) { if (requestCode == REQUEST_PICK_OPML_FILE) { if (resultCode == Activity.RESULT_OK) { new Thread(new Runnable() { // To not block the UI @Override public void run() { try { OPML.importFromFile(data.getData().getPath()); // Try to read it by its path } catch (Exception e) { try { // Try to read it directly as an InputStream (for Google Drive) OPML.importFromFile(MainApplication.getContext().getContentResolver() .openInputStream(data.getData())); } catch (Exception unused) { getActivity().runOnUiThread(new Runnable() { @Override public void run() { UiUtils.showMessage(getActivity(), R.string.error_feed_import); } }); } } } }).start(); } else { displayCustomFilePicker(); } } super.onActivityResult(requestCode, resultCode, data); } private void displayCustomFilePicker() { final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle(R.string.select_file); try { final String[] fileNames = Environment.getExternalStorageDirectory().list(new FilenameFilter() { @Override public boolean accept(File dir, String filename) { return new File(dir, filename).isFile(); } }); builder.setItems(fileNames, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, final int which) { new Thread(new Runnable() { // To not block the UI @Override public void run() { try { OPML.importFromFile(Environment.getExternalStorageDirectory().toString() + File.separator + fileNames[which]); } catch (Exception e) { getActivity().runOnUiThread(new Runnable() { @Override public void run() { UiUtils.showMessage(getActivity(), R.string.error_feed_import); } }); } } }).start(); } }); builder.show(); } catch (Exception unused) { UiUtils.showMessage(getActivity(), R.string.error_feed_import); } } private void importFromOpml() { if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) || Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED_READ_ONLY)) { // First, try to use a file app try { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("text/*"); startActivityForResult(intent, REQUEST_PICK_OPML_FILE); } catch (Exception unused) { // Else use a custom file selector displayCustomFilePicker(); } } else { UiUtils.showMessage(getActivity(), R.string.error_external_storage_not_available); } } private void exportToOpml() { if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) || Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED_READ_ONLY)) { new Thread(new Runnable() { // To not block the UI @Override public void run() { try { final String filename = Environment.getExternalStorageDirectory().toString() + "/Burramys_" + System.currentTimeMillis() + ".opml"; OPML.exportToFile(filename); getActivity().runOnUiThread(new Runnable() { @Override public void run() { UiUtils.showMessage(getActivity(), String.format(getString(R.string.message_exported_to), filename)); } }); } catch (Exception e) { getActivity().runOnUiThread(new Runnable() { @Override public void run() { UiUtils.showMessage(getActivity(), R.string.error_feed_export); } }); } } }).start(); } else { UiUtils.showMessage(getActivity(), R.string.error_external_storage_not_available); } } }