Java tutorial
/* * Copyright (c) 2015. Simas Abramovas * * This file is part of VideoClipper. * * VideoClipper 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. * * VideoClipper 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 VideoClipper. If not, see <http://www.gnu.org/licenses/>. */ package com.simas.vc; import android.animation.ObjectAnimator; import android.app.AlertDialog; import android.database.DataSetObserver; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.support.v4.view.ViewPager; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.support.v4.widget.DrawerLayout; import android.view.View; import android.widget.ListView; import android.widget.Toast; import java.io.File; import java.io.IOException; import java.util.ArrayList; import com.simas.vc.background_tasks.FFmpeg; import com.simas.vc.editor.player.Player; import com.simas.vc.file_chooser.FileChooser; import com.simas.vc.helpers.ObservableList; import com.simas.vc.helpers.Utils; import com.simas.vc.nav_drawer.NavItem; import com.simas.vc.editor.EditorFragment; import com.simas.vc.nav_drawer.NavDrawerFragment; // ToDo prevent copying ONLY non-valid items (other actions still available) // removing invalid or "stuck" items should still be available // ToDo deleted progressing item, should have its parse processes cancelled // not sure if possible but possibly send a cancel flag through jni // ToDo animate toolbar action item icons, i.e. rotate on click (use AnimationDrawable) // ToDo use dimensions in xml instead of hard-coded values /** * Activity that contains all the top-level fragments and manages their transitions. */ public class MainActivity extends AppCompatActivity implements NavDrawerFragment.NavigationDrawerCallbacks, FileChooser.OnFileChosenListener { private static final String STATE_ITEMS = "items_list"; private final String TAG = getClass().getName(); private NavDrawerFragment mNavDrawerFragment; private EditorFragment mEditorFragment; private Toolbar mToolbar; private ViewPager mViewPager; private View mProgressOverlay; private MyPagerAdapter mPagerAdapter; /** * Flag to indicate the need to switch the pager to the newly added item */ private boolean mAddedItemViaToolbar; /** * A list that contains all the added items, shared throughout the app. It's used by * {@link NavDrawerFragment}, {@link com.simas.vc.nav_drawer.NavAdapter}, * {@link MyPagerAdapter} and individual {@link NavItem}s. */ public static ObservableList sItems = new ObservableList(); public static int sPlayerContainerSize; public static int sPreviewSize; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Reset player container and preview sizes when (re-)created sPlayerContainerSize = 0; sPreviewSize = 0; // Always reset observers when (re-)creating the activity sItems.unregisterAllObservers(); // Restore items if available, otherwise make sure the list is empty if (savedInstanceState != null) { ArrayList<NavItem> items = savedInstanceState.getParcelableArrayList(STATE_ITEMS); if (items != null) { sItems = new ObservableList(); sItems.addAll(items); } else { sItems.clear(); } } else { sItems.clear(); } // Init a ProgressBar (to be used by PlayerFragments) mProgressOverlay = getLayoutInflater().inflate(R.layout.progress_bar_overlay, null); setContentView(R.layout.activity_main); /* Toolbar */ mToolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(mToolbar); // addTooltips(); /* Pager */ mViewPager = (ViewPager) findViewById(R.id.view_pager); mPagerAdapter = new MyPagerAdapter(getSupportFragmentManager()); mViewPager.addOnPageChangeListener(new PagerScrollListener()); mViewPager.setAdapter(mPagerAdapter); // This is to avoid a black screen flash when the SurfaceView is setting up another window // https://code.google.com/p/gmaps-api-issues/issues/detail?id=4639#c2 mViewPager.requestTransparentRegion(mViewPager); // Observer final String PAGER_OBSERVER = "pager_observer"; MainActivity.sItems.registerDataSetObserver(new ObservableList.Observer() { @Override public void onRemoved(int position) { Log.e(TAG, "removed: " + position); // Remove fragment and its state from adapter mPagerAdapter.onItemRemoved(position + 1); // Update count and notify mPagerAdapter.setCount(MainActivity.sItems.size()); } @Override public void onModified() { super.onModified(); // Update count and notify mPagerAdapter.setCount(MainActivity.sItems.size()); } }, PAGER_OBSERVER); /* Drawer */ mNavDrawerFragment = (NavDrawerFragment) getSupportFragmentManager() .findFragmentById(R.id.navigation_drawer); mNavDrawerFragment.setUp(R.id.navigation_drawer, (DrawerLayout) findViewById(R.id.drawer_layout)); // Make sure editor item is == to the LV's current selection (e.g. on adapter data deletion) final String EDITOR_AND_DRAWER_ITEM_MATCHER = "match_items_between_editor_and_drawer"; sItems.registerDataSetObserver(new ObservableList.Observer() { private ListView lv = mNavDrawerFragment.getListView(); private Runnable mCheckItems = new Runnable() { @Override public void run() { // Make sure we're not in CAB mode (multiple selections) Object checkedItem; if (lv.getChoiceMode() == ListView.CHOICE_MODE_SINGLE) { try { checkedItem = lv.getItemAtPosition(lv.getCheckedItemPosition()); } catch (IndexOutOfBoundsException ignored) { return; } // Make sure the editor's item is the same as the currently checked one if (getEditorFragment() != null && getEditorFragment().getItem() != checkedItem) { mNavDrawerFragment.selectItem(ListView.INVALID_POSITION); } } } }; @Override public void onModified() { super.onModified(); mCheckItems.run(); } @Override public void onRemoved(int position) { super.onRemoved(position); mCheckItems.run(); } }, EDITOR_AND_DRAWER_ITEM_MATCHER); // ToDo default item test if (sItems.size() == 0) { new Handler().postDelayed(new Runnable() { @Override public void run() { onFileChosen(new File("/sdcard/Movies/Serenity.mp4")); // onFileChosen(new File("/sdcard/Movies/iwatch.mp4")); // onFileChosen(new File("/sdcard/Movies/macbook.mp4")); // onFileChosen(new File("/sdcard/Movies/1.mp4")); // onFileChosen(new File("/sdcard/Movies/2.mp4")); // onFileChosen(new File("/sdcard/Movies/3.mp4")); } }, 1000); // new Handler().postDelayed(new Runnable() { // @Override // public void run() { //// sItems.remove(0); // mViewPager.setCurrentItem(2); // new Handler().postDelayed(new Runnable() { // @Override // public void run() { // sItems.remove(1); // } // }, 2000); // } // }, 2000); } } private class PagerScrollListener extends ViewPager.SimpleOnPageChangeListener { private class IntegerPair { Integer current, previous; } private final String TAG = getClass().getName(); private static final float MIN_SCROLL_OFFSET = 0.01f; private IntegerPair mVisiblePositions = new IntegerPair(); private float mPositionOffset; @Override public void onPageSelected(int position) { super.onPageSelected(position); Log.e(TAG, "got pos: " + position); // Update the EditorFragment and invalidate the PlayerFragment if (position < 1) { // Empty item selected setTitle(Utils.getString(R.string.app_name)); mEditorFragment = null; // Deselect ListView items mNavDrawerFragment.selectItem(ListView.INVALID_POSITION); } else if (position - 1 < sItems.size()) { // Normal item selected setTitle(sItems.get(position - 1).getFile().getName()); Log.e(TAG, "title: " + sItems.get(position - 1).getFile().getName()); mEditorFragment = (EditorFragment) mPagerAdapter.getItem(position); if (getEditorFragment() != null) { getEditorFragment().getPlayerFragment().setInitialized(false); } // Select ListView item mNavDrawerFragment.selectItem(position); } // Make sure the player is paused Player player = Player.getInstance(); if (player.getState() == Player.State.STARTED) { player.pause(); } } @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPxs) { final float previousOffset = mPositionOffset; mPositionOffset = positionOffset; // There can only be 2 pages visible at a time if (mVisiblePositions.current == null || mVisiblePositions.current != position) { mVisiblePositions.previous = mVisiblePositions.current; mVisiblePositions.current = position; } // Re-invoke onPageScrollStateChanged when switched to a valid scroll offset if (Math.abs(mPositionOffset) >= MIN_SCROLL_OFFSET && Math.abs(previousOffset) < MIN_SCROLL_OFFSET) { onPageScrollStateChanged(ViewPager.SCROLL_STATE_DRAGGING); } else if (Math.abs(mPositionOffset) < MIN_SCROLL_OFFSET) { // Hide previews for the visible positions showPreviewsTemporarily(false); } } @Override public void onPageScrollStateChanged(int state) { switch (state) { case ViewPager.SCROLL_STATE_DRAGGING: // Ignore low offsets, to avoid invalid drags if (Math.abs(mPositionOffset) < MIN_SCROLL_OFFSET) { return; } // Pause player Player player = Player.getInstance(); if (player.getState() == Player.State.STARTED) { player.pause(); } // Show previews on the 2 visible editor fragments showPreviewsTemporarily(true); break; } } private void showPreviewsTemporarily(boolean show) { if (mVisiblePositions.current != null && mVisiblePositions.current >= 1) { EditorFragment editor = (EditorFragment) mPagerAdapter.getItem(mVisiblePositions.current); if (editor != null) { editor.getPlayerFragment().showPreviewTemporarily(show); } if (mVisiblePositions.previous != null && mVisiblePositions.previous >= 1) { editor = (EditorFragment) mPagerAdapter.getItem(mVisiblePositions.previous); if (editor != null) { editor.getPlayerFragment().showPreviewTemporarily(show); } } } } } public EditorFragment getEditorFragment() { return mEditorFragment; } public View getProgressOverlay() { return mProgressOverlay; } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelableArrayList(STATE_ITEMS, sItems); } /** * Adds helper tooltips if they haven't yet been closed. Must be called after the toolbar is * set. */ private void addTooltips() { getToolbar().addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { View concat = mToolbar.findViewById(R.id.action_concat); View add = mToolbar.findViewById(R.id.action_add_item); if (concat != null && add != null) { add.setOnClickListener(new View.OnClickListener() { private boolean mRotated; @Override public void onClick(View v) { ObjectAnimator animator = ObjectAnimator.ofFloat(v, "rotation", (mRotated = !mRotated) ? 360 : 0); animator.setDuration(300); animator.start(); } }); new Tooltip(MainActivity.this, concat, Utils.getString(R.string.help_concatenate)); new Tooltip(MainActivity.this, add, Utils.getString(R.string.help_add_item)); mToolbar.removeOnLayoutChangeListener(this); } } }); // Force re-draw getToolbar().requestLayout(); } public boolean isConcatenatable() { if (sItems == null) { // Adapter not available yet return false; } else if (sItems.size() < 2) { // There must be at least 2 videos to concatenate return false; } else { // Loop and look for invalid items for (final NavItem item : sItems) { if (item.getState() == NavItem.State.INPROGRESS) { // If a progressing item is found, wait for it to load and try again item.registerUpdateListener(new NavItem.OnUpdatedListener() { @Override public void onUpdated(NavItem.ItemAttribute attr, Object old, Object newV) { // The reason we also allow invalid state, is because if a 3rd item // becomes invalid, the first two can still be concatenated if (newV == NavItem.State.VALID || newV == NavItem.State.INVALID) { item.unregisterUpdateListener(this); // At this moment, the state is disabled, so only notify if changed if (isConcatenatable()) { Utils.runOnUiThread(new Runnable() { @Override public void run() { getToolbar().getMenu().findItem(R.id.action_concat).setEnabled(true); } }); } } } }); return false; } } } return true; } @Override public void onNavigationDrawerItemSelected(int position) { ListView lv = mNavDrawerFragment.getListView(); if (position == ListView.INVALID_POSITION || position < lv.getHeaderViewsCount() || position >= lv.getCount() - lv.getFooterViewsCount()) { // Skip headers and footers //noinspection UnnecessaryReturnStatement return; } else { // Check the item in the drawer lv.setItemChecked(position, true); Object checkedItem = lv.getItemAtPosition(lv.getCheckedItemPosition()); // Make sure the item is not already selected in the current EditorFragment if (lv.getChoiceMode() == ListView.CHOICE_MODE_SINGLE && getEditorFragment() != null && getEditorFragment().getItem() != checkedItem) { // Select item in pager int itemPosInPager = position - lv.getHeaderViewsCount() + 1; // +1 for empty item mViewPager.setCurrentItem(itemPosInPager); // Close drawer mNavDrawerFragment.setDrawerOpen(false); } } } @Override public boolean onCreateOptionsMenu(Menu menu) { boolean result = super.onCreateOptionsMenu(menu); getMenuInflater().inflate(R.menu.menu_main, menu); final String CONCAT_OBSERVER_TAG = "concatenation_observer"; sItems.registerDataSetObserver(new ObservableList.Observer() { @Override public void onModified() { getToolbar().getMenu().findItem(R.id.action_concat).setEnabled(isConcatenatable()); } @Override public void onRemoved(int position) { super.onRemoved(position); getToolbar().getMenu().findItem(R.id.action_concat).setEnabled(isConcatenatable()); } }, CONCAT_OBSERVER_TAG); return result; } @Override protected void onDestroy() { super.onDestroy(); sItems.unregisterAllObservers(); if (isFinishing()) { Player.getInstance().release(); // ToDo need to release earlier } } @Override public boolean onPrepareOptionsMenu(Menu menu) { if (mNavDrawerFragment != null) { menu.findItem(R.id.action_concat).setEnabled(isConcatenatable()); } return super.onPrepareOptionsMenu(menu); } private static int sNum = 0; // ToDo remove after destination is available @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_concat: // ToDo ask user for a destination String destination = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES) .getPath(); File output = new File(destination + File.separator + "output" + (++sNum) + ".mp4"); if (output.exists()) { //noinspection ResultOfMethodCallIgnored output.delete(); } try { // Concat videos FFmpeg.concat(output, sItems); } catch (IOException e) { Log.e(TAG, "Error!", e); new AlertDialog.Builder(this).setTitle(Utils.getString(R.string.error)) .setMessage("Unrecoverable error! Please try again.").setPositiveButton("OK...", null) .show(); } catch (VCException e) { Log.e(TAG, "Concatenation error:", e); new AlertDialog.Builder(this).setTitle(Utils.getString(R.string.error)).setMessage(e.getMessage()) .setPositiveButton("OK", null).show(); } return true; case R.id.action_add_item: // When adding a new item showFileChooser(true); return true; case R.id.action_settings: return true; } return super.onOptionsItemSelected(item); } public void showFileChooser(boolean fromToolbar) { FileChooser fileChooser = (FileChooser) getSupportFragmentManager().findFragmentByTag(FileChooser.TAG); if (fileChooser == null) { fileChooser = FileChooser.getInstance(); fileChooser.setOnFileChosenListener(this); fileChooser.show(getSupportFragmentManager(), FileChooser.TAG); } mAddedItemViaToolbar = fromToolbar; } @Override public void onFileChosen(File file) { final NavItem item = new NavItem(file); item.registerUpdateListener(new NavItem.OnUpdatedListener() { @Override public void onUpdated(NavItem.ItemAttribute attribute, Object old, Object newValue) { if (newValue == NavItem.State.INVALID) { Utils.runOnUiThread(new Runnable() { @Override public void run() { // If an exception was throw we can fetch the message via the NavItem if (item.getError() != null) { try { new AlertDialog.Builder(MainActivity.this) .setTitle(Utils.getString(R.string.error)).setMessage(item.getError()) .setPositiveButton("OK", null).show(); } catch (Exception ignored) { // If an error occurs while showing a dialog, the context may // be dead, instead show a toast (The truth will be revealed!) Toast.makeText(VC.getAppContext(), item.getError(), Toast.LENGTH_LONG).show(); } } else { // If an error was not registered, we just display a toast warning Toast.makeText(VC.getAppContext(), Utils.getString(R.string.format_parse_failed, item.getFile().getName()), Toast.LENGTH_LONG).show(); } // If an invalid state was reached, remove this item from the drawer MainActivity.sItems.remove(item); } }); } else if (newValue == NavItem.State.VALID) { // Upon reaching the VALID state, remove this listener item.unregisterUpdateListener(this); } } }); MainActivity.sItems.add(item); // Wait until item is added to the pager final boolean addedViaToolbar = mAddedItemViaToolbar; mPagerAdapter.registerDataSetObserver(new DataSetObserver() { @Override public void onChanged() { super.onChanged(); // Only 1 change is needed, remove observer mPagerAdapter.unregisterDataSetObserver(this); // Let the viewPager receive the information about the changes mViewPager.post(new Runnable() { @Override public void run() { // If item added via the ToolBar, and the drawer is closed, // switch pager to the item if (addedViaToolbar && (mNavDrawerFragment .getDrawerState() == NavDrawerFragment.DrawerState.CLOSING || mNavDrawerFragment.getDrawerState() == NavDrawerFragment.DrawerState.CLOSED)) { mViewPager.setCurrentItem(mPagerAdapter.getCount()); } } }); } }); } @Override public void onBackPressed() { // Close drawer on back click if (mNavDrawerFragment.isDrawerOpen()) { mNavDrawerFragment.setDrawerOpen(false); } else { super.onBackPressed(); } } public Toolbar getToolbar() { return mToolbar; } }