Java tutorial
/* * Copyright (C) 2014-2015 Andrew Gunnerson <andrewgunnerson@gmail.com> * * 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. * * 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. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.github.chenxiaolong.dualbootpatcher.patcher; import android.app.Activity; import android.app.Fragment; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.design.widget.Snackbar; import android.support.v13.app.FragmentCompat; import android.support.v4.content.ContextCompat; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView.ItemAnimator; import android.support.v7.widget.SimpleItemAnimator; import android.support.v7.widget.helper.ItemTouchHelper; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.ProgressBar; import android.widget.TextView; import com.github.chenxiaolong.dualbootpatcher.FileUtils; import com.github.chenxiaolong.dualbootpatcher.FileUtils.UriMetadata; import com.github.chenxiaolong.dualbootpatcher.MenuUtils; import com.github.chenxiaolong.dualbootpatcher.PermissionUtils; import com.github.chenxiaolong.dualbootpatcher.R; import com.github.chenxiaolong.dualbootpatcher.SnackbarUtils; import com.github.chenxiaolong.dualbootpatcher.ThreadPoolService.ThreadPoolServiceBinder; import com.github.chenxiaolong.dualbootpatcher.dialogs.GenericConfirmDialog; import com.github.chenxiaolong.dualbootpatcher.dialogs.GenericProgressDialog; import com.github.chenxiaolong.dualbootpatcher.dialogs.GenericYesNoDialog; import com.github.chenxiaolong.dualbootpatcher.dialogs.GenericYesNoDialog.GenericYesNoDialogListener; import com.github.chenxiaolong.dualbootpatcher.nativelib.LibMbDevice.Device; import com.github.chenxiaolong.dualbootpatcher.patcher.PatchFileItemAdapter.PatchFileItemClickListener; import com.github.chenxiaolong.dualbootpatcher.patcher.PatcherOptionsDialog.PatcherOptionsDialogListener; import com.github.chenxiaolong.dualbootpatcher.patcher.PatcherService.PatcherEventListener; import com.github.chenxiaolong.dualbootpatcher.views.DragSwipeItemTouchCallback; import com.github.chenxiaolong.dualbootpatcher.views.DragSwipeItemTouchCallback.OnItemMovedOrDismissedListener; import com.github.clans.fab.FloatingActionButton; import com.github.clans.fab.FloatingActionMenu; import org.apache.commons.io.FilenameUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; public class PatchFileFragment extends Fragment implements ServiceConnection, PatcherOptionsDialogListener, OnItemMovedOrDismissedListener, PatchFileItemClickListener, FragmentCompat.OnRequestPermissionsResultCallback, GenericYesNoDialogListener { public static final String TAG = PatchFileFragment.class.getSimpleName(); private static final String DIALOG_PATCHER_OPTIONS = PatchFileFragment.class.getCanonicalName() + ".patcher_options"; private static final String YES_NO_DIALOG_PERMISSIONS = PatchFileFragment.class.getCanonicalName() + ".yes_no.permissions"; private static final String CONFIRM_DIALOG_PERMISSIONS = PatchFileFragment.class.getCanonicalName() + ".confirm.permissions"; private static final String PROGRESS_DIALOG_QUERYING_METADATA = PatchFileFragment.class.getCanonicalName() + ".progress.querying_metadata"; private static final String EXTRA_SELECTED_PATCHER_ID = "selected_patcher_id"; private static final String EXTRA_SELECTED_INPUT_URI = "selected_input_file"; private static final String EXTRA_SELECTED_OUTPUT_URI = "selected_output_file"; private static final String EXTRA_SELECTED_INPUT_FILE_NAME = "selected_input_file_name"; private static final String EXTRA_SELECTED_INPUT_FILE_SIZE = "selected_input_file_size"; private static final String EXTRA_SELECTED_TASK_ID = "selected_task_id"; private static final String EXTRA_SELECTED_DEVICE = "selected_device"; private static final String EXTRA_SELECTED_ROM_ID = "selected_rom_id"; private static final String EXTRA_QUERYING_METADATA = "querying_metadata"; /** Request code for choosing input file */ private static final int ACTIVITY_REQUEST_INPUT_FILE = 1000; /** Request code for choosing output file */ private static final int ACTIVITY_REQUEST_OUTPUT_FILE = 1001; /** * Request code for storage permissions request * (used in {@link #onRequestPermissionsResult(int, String[], int[])}) */ private static final int PERMISSIONS_REQUEST_STORAGE = 1; /** Whether we should show the progress bar (true by default for obvious reasons) */ private boolean mShowingProgress = true; /** Main files list */ private RecyclerView mRecycler; /** FAB */ private FloatingActionMenu mFAB; private FloatingActionButton mFABAddZip; private FloatingActionButton mFABAddOdin; /** Loading progress spinner */ private ProgressBar mProgressBar; /** Add zip message */ private TextView mAddZipMessage; /** Check icon in the toolbar */ private MenuItem mCheckItem; /** Cancel icon in the toolbar */ private MenuItem mCancelItem; /** Adapter for the list of files to patch */ private PatchFileItemAdapter mAdapter; /** Our patcher service */ private PatcherService mService; /** Callback for events from the service */ private PatcherEventCallback mCallback = new PatcherEventCallback(); /** Handler for processing events from the service on the UI thread */ private final Handler mHandler = new Handler(Looper.getMainLooper()); /** {@link Runnable}s to process once the service has been connected */ private ArrayList<Runnable> mExecOnConnect = new ArrayList<>(); /** Whether we're initialized */ private boolean mInitialized; /** List of patcher items (pending, in progress, or complete) */ private ArrayList<PatchFileItem> mItems = new ArrayList<>(); /** Map task IDs to item indexes */ private HashMap<Integer, Integer> mItemsMap = new HashMap<>(); /** Item touch callback for dragging and swiping */ DragSwipeItemTouchCallback mItemTouchCallback; /** Selected patcher ID */ private String mSelectedPatcherId; /** Selected input file */ private Uri mSelectedInputUri; /** Selected output file */ private Uri mSelectedOutputUri; /** Selected input file's name */ private String mSelectedInputFileName; /** Selected input file's size */ private long mSelectedInputFileSize; /** Task ID of selected patcher item */ private int mSelectedTaskId; /** Target device */ private Device mSelectedDevice; /** Target ROM ID */ private String mSelectedRomId; /** Whether we're querying the URI metadata */ private boolean mQueryingMetadata; /** Task for querying the metadata of URIs */ private GetUriMetadataTask mQueryMetadataTask; public static PatchFileFragment newInstance() { return new PatchFileFragment(); } /** * {@inheritDoc} */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); } /** * {@inheritDoc} */ @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); if (savedInstanceState != null) { mSelectedPatcherId = savedInstanceState.getString(EXTRA_SELECTED_PATCHER_ID); mSelectedInputUri = savedInstanceState.getParcelable(EXTRA_SELECTED_INPUT_URI); mSelectedOutputUri = savedInstanceState.getParcelable(EXTRA_SELECTED_OUTPUT_URI); mSelectedInputFileName = savedInstanceState.getString(EXTRA_SELECTED_INPUT_FILE_NAME); mSelectedInputFileSize = savedInstanceState.getLong(EXTRA_SELECTED_INPUT_FILE_SIZE); mSelectedTaskId = savedInstanceState.getInt(EXTRA_SELECTED_TASK_ID); mSelectedDevice = savedInstanceState.getParcelable(EXTRA_SELECTED_DEVICE); mSelectedRomId = savedInstanceState.getString(EXTRA_SELECTED_ROM_ID); mQueryingMetadata = savedInstanceState.getBoolean(EXTRA_QUERYING_METADATA); } // Initialize UI elements mRecycler = (RecyclerView) getActivity().findViewById(R.id.files_list); mFAB = (FloatingActionMenu) getActivity().findViewById(R.id.fab); mFABAddZip = (FloatingActionButton) getActivity().findViewById(R.id.fab_add_flashable_zip); mFABAddOdin = (FloatingActionButton) getActivity().findViewById(R.id.fab_add_odin_image); mProgressBar = (ProgressBar) getActivity().findViewById(R.id.loading); mAddZipMessage = (TextView) getActivity().findViewById(R.id.add_zip_message); mItemTouchCallback = new DragSwipeItemTouchCallback(this); ItemTouchHelper itemTouchHelper = new ItemTouchHelper(mItemTouchCallback); itemTouchHelper.attachToRecyclerView(mRecycler); // Disable change animation since we frequently update the progress, which makes the // animation very ugly ItemAnimator animator = mRecycler.getItemAnimator(); if (animator instanceof SimpleItemAnimator) { ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false); } // Set up listener for the FAB mFABAddZip.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { startFileSelection(PatcherUtils.PATCHER_ID_MULTIBOOTPATCHER); mFAB.close(true); } }); mFABAddOdin.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { startFileSelection(PatcherUtils.PATCHER_ID_ODINPATCHER); mFAB.close(true); } }); // Set up adapter for the files list mAdapter = new PatchFileItemAdapter(getActivity(), mItems, this); mRecycler.setHasFixedSize(true); mRecycler.setAdapter(mAdapter); LinearLayoutManager llm = new LinearLayoutManager(getActivity()); llm.setOrientation(LinearLayoutManager.VERTICAL); mRecycler.setLayoutManager(llm); // Hide FAB initially mFAB.hideMenuButton(false); // Show loading progress bar updateLoadingStatus(); // Initialize the patcher once the service is connected executeNeedsService(new Runnable() { @Override public void run() { mService.initializePatcher(); } }); // NOTE: No further loading should be done here. All initialization should be done in // onPatcherLoaded(), which is called once the patcher's data files have been extracted and // loaded. } /** * {@inheritDoc} */ @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString(EXTRA_SELECTED_PATCHER_ID, mSelectedPatcherId); outState.putParcelable(EXTRA_SELECTED_INPUT_URI, mSelectedInputUri); outState.putParcelable(EXTRA_SELECTED_OUTPUT_URI, mSelectedOutputUri); outState.putString(EXTRA_SELECTED_INPUT_FILE_NAME, mSelectedInputFileName); outState.putLong(EXTRA_SELECTED_INPUT_FILE_SIZE, mSelectedInputFileSize); outState.putInt(EXTRA_SELECTED_TASK_ID, mSelectedTaskId); outState.putParcelable(EXTRA_SELECTED_DEVICE, mSelectedDevice); outState.putString(EXTRA_SELECTED_ROM_ID, mSelectedRomId); outState.putBoolean(EXTRA_QUERYING_METADATA, mQueryingMetadata); } /** * {@inheritDoc} */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_patcher, container, false); } /** * {@inheritDoc} */ @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.actionbar_check_cancel, menu); // NOTE: May crash on some versions of Android due to a bug where getActivity() returns null // after onAttach() has been called, but before onDetach() has been called. It's similar to // this bug report, except it happens with android.app.Fragment: // https://code.google.com/p/android/issues/detail?id=67519 int primary = ContextCompat.getColor(getActivity(), R.color.text_color_primary); MenuUtils.tintAllMenuIcons(menu, primary); mCheckItem = menu.findItem(R.id.check_item); mCancelItem = menu.findItem(R.id.cancel_item); updateToolbarIcons(); super.onCreateOptionsMenu(menu, inflater); } /** * {@inheritDoc} */ @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.check_item: executeNeedsService(new Runnable() { @Override public void run() { for (int i = 0; i < mItems.size(); i++) { PatchFileItem item = mItems.get(i); if (item.state == PatchFileState.QUEUED) { item.state = PatchFileState.PENDING; mService.startPatching(item.taskId); mAdapter.notifyItemChanged(i); } } } }); return true; case R.id.cancel_item: executeNeedsService(new Runnable() { @Override public void run() { // Cancel the tasks in reverse order since there's a chance that the next task // will start when the previous one is cancelled for (int i = mItems.size() - 1; i >= 0; i--) { PatchFileItem item = mItems.get(i); if (item.state == PatchFileState.IN_PROGRESS || item.state == PatchFileState.PENDING) { mService.cancelPatching(item.taskId); if (item.state == PatchFileState.PENDING) { item.state = PatchFileState.QUEUED; mAdapter.notifyItemChanged(i); } } } } }); return true; } return super.onOptionsItemSelected(item); } /** * {@inheritDoc} */ @Override public void onStart() { super.onStart(); // Bind to our service. We start the service so it doesn't get killed when all the clients // unbind from the service. The service will automatically stop once all clients have // unbinded and all tasks have completed. Intent intent = new Intent(getActivity(), PatcherService.class); getActivity().bindService(intent, this, Context.BIND_AUTO_CREATE); getActivity().startService(intent); } /** * {@inheritDoc} */ @Override public void onStop() { super.onStop(); // Cancel metadata query task cancelQueryUriMetadata(); // If we connected to the service and registered the callback, now we unregister it if (mService != null) { mService.unregisterCallback(mCallback); } // Unbind from our service getActivity().unbindService(this); mService = null; // At this point, the mCallback will not get called anymore by the service. Now we just need // to remove all pending Runnables that were posted to mHandler. mHandler.removeCallbacksAndMessages(null); } /** * {@inheritDoc} */ @Override public void onServiceConnected(ComponentName name, IBinder service) { // Save a reference to the service so we can interact with it ThreadPoolServiceBinder binder = (ThreadPoolServiceBinder) service; mService = (PatcherService) binder.getService(); // Register callback mService.registerCallback(mCallback); for (Runnable runnable : mExecOnConnect) { runnable.run(); } mExecOnConnect.clear(); } /** * {@inheritDoc} */ @Override public void onServiceDisconnected(ComponentName componentName) { mService = null; } /** * {@inheritDoc} */ @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case ACTIVITY_REQUEST_INPUT_FILE: if (data != null && resultCode == Activity.RESULT_OK) { onSelectedInputUri(data.getData()); } break; case ACTIVITY_REQUEST_OUTPUT_FILE: if (data != null && resultCode == Activity.RESULT_OK) { onSelectedOutputUri(data.getData()); } break; default: super.onActivityResult(requestCode, resultCode, data); break; } } /** * {@inheritDoc} */ @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { case PERMISSIONS_REQUEST_STORAGE: if (PermissionUtils.verifyPermissions(grantResults)) { selectInputUri(); } else { if (PermissionUtils.shouldShowRationales(this, permissions)) { showPermissionsRationaleDialogYesNo(); } else { showPermissionsRationaleDialogConfirm(); } } break; default: super.onRequestPermissionsResult(requestCode, permissions, grantResults); break; } } /** * {@inheritDoc} */ @Override public void onItemMoved(int fromPosition, int toPosition) { // Update index map PatchFileItem fromItem = mItems.get(fromPosition); PatchFileItem toItem = mItems.get(toPosition); mItemsMap.put(fromItem.taskId, toPosition); mItemsMap.put(toItem.taskId, fromPosition); Collections.swap(mItems, fromPosition, toPosition); mAdapter.notifyItemMoved(fromPosition, toPosition); } /** * {@inheritDoc} */ @Override public void onItemDismissed(int position) { final PatchFileItem item = mItems.get(position); mItemsMap.remove(item.taskId); mItems.remove(position); mAdapter.notifyItemRemoved(position); updateAddZipMessage(); updateToolbarIcons(); executeNeedsService(new Runnable() { @Override public void run() { mService.removePatchFileTask(item.taskId); } }); } /** * {@inheritDoc} */ @Override public void onPatchFileItemClicked(PatchFileItem item) { showPatcherOptionsDialog(item.taskId); } /** * Toggle main UI and progress bar visibility depending on {@link #mShowingProgress} */ private void updateLoadingStatus() { if (mShowingProgress) { mRecycler.setVisibility(View.GONE); mFAB.hideMenuButton(true); mAddZipMessage.setVisibility(View.GONE); mProgressBar.setVisibility(View.VISIBLE); } else { mRecycler.setVisibility(View.VISIBLE); mFAB.showMenuButton(true); mAddZipMessage.setVisibility(View.VISIBLE); mProgressBar.setVisibility(View.GONE); } } /** * Called when the patcher data and libmbp have been initialized * * This method is guaranteed to be called only once during between onStart() and onStop() */ private void onPatcherInitialized() { Log.d(TAG, "Patcher has been initialized"); onReady(); } /** * Called when everything has been initialized and we have the necessary permissions */ private void onReady() { // Load patch file items from the service int[] taskIds = mService.getPatchFileTaskIds(); Arrays.sort(taskIds); for (int taskId : taskIds) { PatchFileItem item = new PatchFileItem(); item.taskId = taskId; item.patcherId = mService.getPatcherId(taskId); item.inputUri = mService.getInputUri(taskId); item.outputUri = mService.getOutputUri(taskId); item.displayName = mService.getDisplayName(taskId); item.device = mService.getDevice(taskId); item.romId = mService.getRomId(taskId); item.state = mService.getState(taskId); item.details = mService.getDetails(taskId); item.bytes = mService.getCurrentBytes(taskId); item.maxBytes = mService.getMaximumBytes(taskId); item.files = mService.getCurrentFiles(taskId); item.maxFiles = mService.getMaximumFiles(taskId); item.successful = mService.isSuccessful(taskId); item.errorCode = mService.getErrorCode(taskId); mItems.add(item); mItemsMap.put(taskId, mItems.size() - 1); } mAdapter.notifyDataSetChanged(); // We are now fully initialized. Hide the loading spinner mShowingProgress = false; updateLoadingStatus(); // Hide add zip message if we've already added a zip updateAddZipMessage(); updateToolbarIcons(); updateModifiability(); updateScreenOnState(); // Resume URI metadata query if it was interrupted if (mQueryingMetadata) { queryUriMetadata(); } } private void updateAddZipMessage() { mAddZipMessage.setVisibility(mItems.isEmpty() ? View.VISIBLE : View.GONE); } private void updateToolbarIcons() { if (mCheckItem != null && mCancelItem != null) { boolean checkVisible = false; boolean cancelVisible = false; for (PatchFileItem item : mItems) { if (item.state == PatchFileState.QUEUED) { checkVisible = true; } else if (item.state == PatchFileState.IN_PROGRESS) { checkVisible = false; cancelVisible = true; break; } } mCheckItem.setVisible(checkVisible); mCancelItem.setVisible(cancelVisible); } } private void updateModifiability() { boolean canModify = true; for (PatchFileItem item : mItems) { if (item.state == PatchFileState.PENDING || item.state == PatchFileState.IN_PROGRESS) { canModify = false; break; } } mItemTouchCallback.setLongPressDragEnabled(canModify); mItemTouchCallback.setItemViewSwipeEnabled(canModify); } private void updateScreenOnState() { boolean keepScreenOn = false; for (PatchFileItem item : mItems) { if (item.state == PatchFileState.PENDING || item.state == PatchFileState.IN_PROGRESS) { keepScreenOn = true; break; } } if (keepScreenOn) { getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } else { getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } } private void showPatcherOptionsDialog(int taskId) { String preselectedDeviceId = null; String preselectedRomId = null; if (taskId >= 0) { PatchFileItem item = mItems.get(mItemsMap.get(taskId)); preselectedDeviceId = item.device.getId(); preselectedRomId = item.romId; } PatcherOptionsDialog dialog = PatcherOptionsDialog.newInstanceFromFragment(this, taskId, preselectedDeviceId, preselectedRomId); dialog.show(getFragmentManager(), DIALOG_PATCHER_OPTIONS); } private void startFileSelection(String patcherId) { mSelectedPatcherId = patcherId; if (PermissionUtils.supportsRuntimePermissions()) { requestPermissions(); } else { selectInputUri(); } } private void requestPermissions() { FragmentCompat.requestPermissions(PatchFileFragment.this, PermissionUtils.STORAGE_PERMISSIONS, PERMISSIONS_REQUEST_STORAGE); } private void showPermissionsRationaleDialogYesNo() { GenericYesNoDialog dialog = (GenericYesNoDialog) getFragmentManager() .findFragmentByTag(YES_NO_DIALOG_PERMISSIONS); if (dialog == null) { GenericYesNoDialog.Builder builder = new GenericYesNoDialog.Builder(); builder.message(R.string.patcher_storage_permission_required); builder.positive(R.string.try_again); builder.negative(R.string.cancel); dialog = builder.buildFromFragment(YES_NO_DIALOG_PERMISSIONS, this); dialog.show(getFragmentManager(), YES_NO_DIALOG_PERMISSIONS); } } private void showPermissionsRationaleDialogConfirm() { GenericConfirmDialog dialog = (GenericConfirmDialog) getFragmentManager() .findFragmentByTag(CONFIRM_DIALOG_PERMISSIONS); if (dialog == null) { GenericConfirmDialog.Builder builder = new GenericConfirmDialog.Builder(); builder.message(R.string.patcher_storage_permission_required); builder.buttonText(R.string.ok); dialog = builder.build(); dialog.show(getFragmentManager(), CONFIRM_DIALOG_PERMISSIONS); } } /** * Show activity for selecting an input file * * After the user selects a file, {@link #onSelectedInputUri(Uri)} will be called. If the user * cancels the selection, nothing will happen. * * @see {@link #onSelectedInputUri(Uri)} */ private void selectInputUri() { Intent intent = FileUtils.getFileOpenIntent(getActivity(), "*/*"); startActivityForResult(intent, ACTIVITY_REQUEST_INPUT_FILE); } /** * Show activity for selecting an output file * * After the user confirms the patcher options, this function will be called. Once the user * selects the output filename, {@link #onSelectedOutputUri(Uri)} will be called. If the user * leaves the activity without selecting a file, nothing will happen. * * The specified ROM ID is added to the suggested filename before the file extension. For * example, if the original input filename was "SuperDuperROM-1.0.zip" and the ROM ID is "dual", * then the suggested filename is "SuperDuperROM-1.0_dual.zip". * * @see {@link #onSelectedOutputUri(Uri)} */ private void selectOutputFile() { String baseName; String extension; if (mSelectedPatcherId.equals(PatcherUtils.PATCHER_ID_ODINPATCHER)) { baseName = mSelectedInputFileName.replaceAll("(\\.tar\\.md5(\\.gz|\\.xz)?|\\.zip)$", ""); extension = "zip"; } else { baseName = FilenameUtils.getBaseName(mSelectedInputFileName); extension = FilenameUtils.getExtension(mSelectedInputFileName); } StringBuilder sb = new StringBuilder(); if (baseName != null) { sb.append(baseName); sb.append('_'); } sb.append(mSelectedRomId); if (extension != null) { sb.append('.'); sb.append(extension); } String desiredName = sb.toString(); Intent intent = FileUtils.getFileSaveIntent(getActivity(), "*/*", desiredName); startActivityForResult(intent, ACTIVITY_REQUEST_OUTPUT_FILE); } /** * Query the metadata for the input file * * After the user selects an input file, this function is called to start the task of retrieving * the file's name and size. Once the information has been retrieved, * {@link #onQueriedMetadata(UriMetadata)} is called. * * @see {@link #onQueriedMetadata(UriMetadata)} */ private void queryUriMetadata() { if (mQueryMetadataTask != null) { throw new IllegalStateException("Already querying metadata!"); } mQueryMetadataTask = new GetUriMetadataTask(); mQueryMetadataTask.execute(mSelectedInputUri); // Show progress dialog. Dialog may already exist if a configuration change occurred during // the query (and thus, this function is called again in onReady()). GenericProgressDialog dialog = (GenericProgressDialog) getFragmentManager() .findFragmentByTag(PROGRESS_DIALOG_QUERYING_METADATA); if (dialog == null) { GenericProgressDialog.Builder builder = new GenericProgressDialog.Builder(); builder.message(R.string.please_wait); dialog = builder.build(); dialog.show(getFragmentManager(), PROGRESS_DIALOG_QUERYING_METADATA); } } /** * Cancel task for querying the input URI metadata * * This function is a no-op if there is no such task. * * @see {@link #onStop()} */ private void cancelQueryUriMetadata() { if (mQueryMetadataTask != null) { mQueryMetadataTask.cancel(true); mQueryMetadataTask = null; } } /** * Called after the user tapped the FAB and selected a file * * This function will call {@link #queryUriMetadata()} to start querying various metadata of the * input URI. * * @see {@link #queryUriMetadata()} * * @param uri Input file's URI */ private void onSelectedInputUri(@NonNull Uri uri) { mSelectedInputUri = uri; queryUriMetadata(); } /** * Called after the user selects the output file * * This function will call {@link #addOrEditItem()} to either add the new patcher item or edit * the selected item. * * @see {@link #addOrEditItem()} * * @param uri Output file's URI */ private void onSelectedOutputUri(@NonNull Uri uri) { mSelectedOutputUri = uri; addOrEditItem(); } /** * Called after the input URI's metadata has been retrieved * * This function will open the patcher options dialog. * * @param metadata URI metadata * * @see {@link #queryUriMetadata()} * @see {@link #showPatcherOptionsDialog(int)} */ private void onQueriedMetadata(@NonNull UriMetadata metadata) { GenericProgressDialog dialog = (GenericProgressDialog) getFragmentManager() .findFragmentByTag(PROGRESS_DIALOG_QUERYING_METADATA); if (dialog != null) { dialog.dismiss(); } mSelectedInputFileName = metadata.displayName; mSelectedInputFileSize = metadata.size; // Open patcher options showPatcherOptionsDialog(-1); } /** * Called after the user confirms the patcher options * * This function will call {@link #selectOutputFile()} for choosing an output file * * @see {@link #selectOutputFile()} * * @param id Task ID (-1 if new item) * @param device Target device * @param romId Target ROM ID */ @Override public void onConfirmedOptions(final int id, final Device device, final String romId) { mSelectedTaskId = id; mSelectedDevice = device; mSelectedRomId = romId; selectOutputFile(); } @Override public void onConfirmYesNo(@Nullable String tag, boolean choice) { if (YES_NO_DIALOG_PERMISSIONS.equals(tag)) { if (choice) { requestPermissions(); } } } private void addOrEditItem() { if (mSelectedTaskId >= 0) { // Edit existing task executeNeedsService(new Runnable() { @Override public void run() { int index = mItemsMap.get(mSelectedTaskId); PatchFileItem item = mItems.get(index); item.device = mSelectedDevice; item.romId = mSelectedRomId; mAdapter.notifyItemChanged(index); mService.setDevice(mSelectedTaskId, mSelectedDevice); mService.setRomId(mSelectedTaskId, mSelectedRomId); } }); return; } // Do not allow two patching operations with the same output file. This is not completely // foolproof since two URIs can refer to the same target path, but it's the best we can do. for (PatchFileItem item : mItems) { if (item.outputUri.equals(mSelectedOutputUri)) { SnackbarUtils.createSnackbar(getActivity(), mFAB, R.string.patcher_cannot_add_same_item, Snackbar.LENGTH_LONG).show(); return; } } executeNeedsService(new Runnable() { @Override public void run() { final PatchFileItem pf = new PatchFileItem(); pf.patcherId = mSelectedPatcherId; pf.device = mSelectedDevice; pf.inputUri = mSelectedInputUri; pf.outputUri = mSelectedOutputUri; pf.displayName = mSelectedInputFileName; pf.romId = mSelectedRomId; pf.state = PatchFileState.QUEUED; int taskId = mService.addPatchFileTask(pf.patcherId, pf.inputUri, pf.outputUri, pf.displayName, pf.device, pf.romId); pf.taskId = taskId; mItems.add(pf); mItemsMap.put(taskId, mItems.size() - 1); mAdapter.notifyItemInserted(mItems.size() - 1); updateAddZipMessage(); updateToolbarIcons(); } }); } /** * Execute a {@link Runnable} that requires the service to be connected * * NOTE: If the service is disconnect before this method is called and does not reconnect before * this fragment is destroyed, then the runnable will NOT be executed. * * @param runnable Runnable that requires access to the service */ public void executeNeedsService(final Runnable runnable) { if (mService != null) { runnable.run(); } else { mExecOnConnect.add(runnable); } } private class PatcherEventCallback implements PatcherEventListener { @Override public void onPatcherInitialized() { // Make sure we don't initialize more than once. This event could be sent more than // once if, eg., mService.initializePatcher() is called and the device is rotated // before this event is received. Then mService.initializePatcher() will be called // again leading to a duplicate event. if (!mInitialized) { mInitialized = true; mHandler.post(new Runnable() { @Override public void run() { PatchFileFragment.this.onPatcherInitialized(); } }); } } @Override public void onPatcherUpdateDetails(final int taskId, final String details) { mHandler.post(new Runnable() { @Override public void run() { if (mItemsMap.containsKey(taskId)) { int itemIndex = mItemsMap.get(taskId); PatchFileItem item = mItems.get(itemIndex); item.details = details; mAdapter.notifyItemChanged(itemIndex); } } }); } @Override public void onPatcherUpdateProgress(final int taskId, final long bytes, final long maxBytes) { mHandler.post(new Runnable() { @Override public void run() { if (mItemsMap.containsKey(taskId)) { int itemIndex = mItemsMap.get(taskId); PatchFileItem item = mItems.get(itemIndex); item.bytes = bytes; item.maxBytes = maxBytes; mAdapter.notifyItemChanged(itemIndex); } } }); } @Override public void onPatcherUpdateFilesProgress(final int taskId, final long files, final long maxFiles) { mHandler.post(new Runnable() { @Override public void run() { if (mItemsMap.containsKey(taskId)) { int itemIndex = mItemsMap.get(taskId); PatchFileItem item = mItems.get(itemIndex); item.files = files; item.maxFiles = maxFiles; mAdapter.notifyItemChanged(itemIndex); } } }); } @Override public void onPatcherStarted(final int taskId) { mHandler.post(new Runnable() { @Override public void run() { if (mItemsMap.containsKey(taskId)) { int itemIndex = mItemsMap.get(taskId); PatchFileItem item = mItems.get(itemIndex); item.state = PatchFileState.IN_PROGRESS; updateToolbarIcons(); updateModifiability(); updateScreenOnState(); mAdapter.notifyItemChanged(itemIndex); } } }); } @Override public void onPatcherFinished(final int taskId, final PatchFileState state, final boolean ret, final int errorCode) { mHandler.post(new Runnable() { @Override public void run() { if (mItemsMap.containsKey(taskId)) { int itemIndex = mItemsMap.get(taskId); PatchFileItem item = mItems.get(itemIndex); item.state = state; item.details = getString(R.string.details_done); item.successful = ret; item.errorCode = errorCode; updateToolbarIcons(); updateModifiability(); updateScreenOnState(); mAdapter.notifyItemChanged(itemIndex); //returnResult(ret ? RESULT_PATCHING_SUCCEEDED : RESULT_PATCHING_FAILED, // "See " + LogUtils.getPath("patch-file.log") + " for details", newPath); } } }); } } /** * Task to query the display name, size, and MIME type of a list of openable URIs. */ private class GetUriMetadataTask extends AsyncTask<Uri, Void, UriMetadata[]> { private ContentResolver mCR; @Override protected void onPreExecute() { mCR = getActivity().getContentResolver(); mQueryingMetadata = true; } @Override protected UriMetadata[] doInBackground(Uri... params) { return FileUtils.queryUriMetadata(mCR, params); } @Override protected void onPostExecute(UriMetadata[] metadatas) { mCR = null; if (isAdded()) { mQueryingMetadata = false; onQueriedMetadata(metadatas[0]); } } } }