From: David A. Velasco Date: Mon, 25 Feb 2013 11:24:14 +0000 (+0100) Subject: Force preview of images in 'full screen' X-Git-Tag: oc-android-1.4.3~39^2~47 X-Git-Url: http://git.linex4red.de/pub/Android/ownCloud.git/commitdiff_plain/3d272eb75378ae95274ef4c868fdd956bba60d31?ds=inline;hp=--cc Force preview of images in 'full screen' --- 3d272eb75378ae95274ef4c868fdd956bba60d31 diff --git a/AndroidManifest.xml b/AndroidManifest.xml index e1f02506..f0dc61ba 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -86,7 +86,9 @@ - + + diff --git a/res/layout/preview_image_activity.xml b/res/layout/preview_image_activity.xml new file mode 100644 index 00000000..7884f02b --- /dev/null +++ b/res/layout/preview_image_activity.xml @@ -0,0 +1,32 @@ + + + + + + + + + \ No newline at end of file diff --git a/res/layout/preview_image_fragment.xml b/res/layout/preview_image_fragment.xml new file mode 100644 index 00000000..378fc8f2 --- /dev/null +++ b/res/layout/preview_image_fragment.xml @@ -0,0 +1,48 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/com/owncloud/android/ui/activity/FileDisplayActivity.java b/src/com/owncloud/android/ui/activity/FileDisplayActivity.java index c8f23f0f..2031aad2 100644 --- a/src/com/owncloud/android/ui/activity/FileDisplayActivity.java +++ b/src/com/owncloud/android/ui/activity/FileDisplayActivity.java @@ -1053,25 +1053,37 @@ public class FileDisplayActivity extends SherlockFragmentActivity implements @Override public void onFileClick(OCFile file) { - // If we are on a large device -> update fragment - if (mDualPane) { - // buttons in the details view are problematic when trying to reuse an existing fragment; create always a new one solves some of them, BUT no all; downloads are 'dangerous' - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - if (file != null && FilePreviewFragment.canBePreviewed(file)) { - if (file.isDown()) { - transaction.replace(R.id.file_details_container, new FilePreviewFragment(file, AccountUtils.getCurrentOwnCloudAccount(this)), FileDetailFragment.FTAG); - } else { - transaction.replace(R.id.file_details_container, new FileDetailFragment(file, AccountUtils.getCurrentOwnCloudAccount(this)), FileDetailFragment.FTAG); - mWaitingToPreview = file; - requestForDownload(); - } + if (file != null && FilePreviewFragment.canBePreviewed(file)) { + if (file.isDown()) { + // preview it + startPreview(file); + } else { - transaction.replace(R.id.file_details_container, new FileDetailFragment(file, AccountUtils.getCurrentOwnCloudAccount(this)), FileDetailFragment.FTAG); + // automatic download, preview on finish + startDownloadForPreview(file); + } - //transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); + } else { + // details view + startDetails(file); + } + } + + private void startPreview(OCFile file) { + if (mDualPane && + !file.isImage() // this is a trick to get a quick-to-implement 'full screen' preview for images in landscape + ) { + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + transaction.replace(R.id.file_details_container, new FilePreviewFragment(file, AccountUtils.getCurrentOwnCloudAccount(this)), FileDetailFragment.FTAG); transaction.commit(); - } else { // small or medium screen device -> new Activity + } else if (file.isImage()) { + Intent showDetailsIntent = new Intent(this, PreviewImageActivity.class); + showDetailsIntent.putExtra(FileDetailFragment.EXTRA_FILE, file); + showDetailsIntent.putExtra(FileDetailFragment.EXTRA_ACCOUNT, AccountUtils.getCurrentOwnCloudAccount(this)); + startActivity(showDetailsIntent); + + } else { Intent showDetailsIntent = new Intent(this, FileDetailActivity.class); showDetailsIntent.putExtra(FileDetailFragment.EXTRA_FILE, file); showDetailsIntent.putExtra(FileDetailFragment.EXTRA_ACCOUNT, AccountUtils.getCurrentOwnCloudAccount(this)); @@ -1079,7 +1091,37 @@ public class FileDisplayActivity extends SherlockFragmentActivity implements } } + private void startDownloadForPreview(OCFile file) { + if (mDualPane) { + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + transaction.replace(R.id.file_details_container, new FileDetailFragment(file, AccountUtils.getCurrentOwnCloudAccount(this)), FileDetailFragment.FTAG); + transaction.commit(); + mWaitingToPreview = file; + requestForDownload(); + + } else { + Intent showDetailsIntent = new Intent(this, FileDetailActivity.class); + showDetailsIntent.putExtra(FileDetailFragment.EXTRA_FILE, file); + showDetailsIntent.putExtra(FileDetailFragment.EXTRA_ACCOUNT, AccountUtils.getCurrentOwnCloudAccount(this)); + startActivity(showDetailsIntent); + } + } + + private void startDetails(OCFile file) { + if (mDualPane && !file.isImage()) { + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + transaction.replace(R.id.file_details_container, new FileDetailFragment(file, AccountUtils.getCurrentOwnCloudAccount(this)), FileDetailFragment.FTAG); + transaction.commit(); + } else { + Intent showDetailsIntent = new Intent(this, FileDetailActivity.class); + showDetailsIntent.putExtra(FileDetailFragment.EXTRA_FILE, file); + showDetailsIntent.putExtra(FileDetailFragment.EXTRA_ACCOUNT, AccountUtils.getCurrentOwnCloudAccount(this)); + startActivity(showDetailsIntent); + } + } + + /** * {@inheritDoc} */ diff --git a/src/com/owncloud/android/ui/activity/PreviewImageActivity.java b/src/com/owncloud/android/ui/activity/PreviewImageActivity.java new file mode 100644 index 00000000..f1284972 --- /dev/null +++ b/src/com/owncloud/android/ui/activity/PreviewImageActivity.java @@ -0,0 +1,302 @@ +/* ownCloud Android client application + * Copyright (C) 2012-2013 ownCloud Inc. + * + * 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 . + * + */ +package com.owncloud.android.ui.activity; + +import android.accounts.Account; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.IBinder; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.util.Log; + +import com.actionbarsherlock.app.ActionBar; +import com.actionbarsherlock.app.SherlockFragmentActivity; +import com.actionbarsherlock.view.MenuItem; +import com.owncloud.android.datamodel.OCFile; +import com.owncloud.android.files.services.FileDownloader; +import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder; +import com.owncloud.android.files.services.FileUploader; +import com.owncloud.android.files.services.FileUploader.FileUploaderBinder; +import com.owncloud.android.ui.fragment.FileDetailFragment; +import com.owncloud.android.ui.fragment.FileFragment; +import com.owncloud.android.ui.fragment.FilePreviewFragment; + +import com.owncloud.android.AccountUtils; +import com.owncloud.android.R; + +/** + * Used as an utility to preview image files contained in an ownCloud account. + * + * @author David A. Velasco + */ +public class PreviewImageActivity extends SherlockFragmentActivity implements FileFragment.ContainerActivity { + + public static final int DIALOG_SHORT_WAIT = 0; + + public static final String TAG = PreviewImageActivity.class.getSimpleName(); + + public static final String KEY_WAITING_TO_PREVIEW = "WAITING_TO_PREVIEW"; + + private FileDownloaderBinder mDownloaderBinder = null; + private ServiceConnection mDownloadConnection, mUploadConnection = null; + private FileUploaderBinder mUploaderBinder = null; + private boolean mWaitingToPreview; + + private OCFile mFile; + private Account mAccount; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mFile = getIntent().getParcelableExtra(FileDetailFragment.EXTRA_FILE); + mAccount = getIntent().getParcelableExtra(FileDetailFragment.EXTRA_ACCOUNT); + if (mFile == null) { + throw new IllegalStateException("Instanced with a NULL OCFile"); + } + if (mAccount == null) { + throw new IllegalStateException("Instanced with a NULL ownCloud Account"); + } + if (!mFile.isImage()) { + throw new IllegalArgumentException("Non-image file passed as argument"); + } + + setContentView(R.layout.preview_image_activity); + + ActionBar actionBar = getSupportActionBar(); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setDisplayShowTitleEnabled(true); + actionBar.setTitle(mFile.getFileName()); + + if (savedInstanceState == null) { + mWaitingToPreview = false; + createChildFragment(); + } else { + mWaitingToPreview = savedInstanceState.getBoolean(KEY_WAITING_TO_PREVIEW); + } + + mDownloadConnection = new DetailsServiceConnection(); + bindService(new Intent(this, FileDownloader.class), mDownloadConnection, Context.BIND_AUTO_CREATE); + mUploadConnection = new DetailsServiceConnection(); + bindService(new Intent(this, FileUploader.class), mUploadConnection, Context.BIND_AUTO_CREATE); + + } + + /** + * Creates the proper fragment depending upon the state of the handled {@link OCFile} and + * the requested {@link Intent}. + */ + private void createChildFragment() { + Fragment newFragment = null; + if (mFile.isDown()) { + newFragment = new FilePreviewFragment(mFile, mAccount); + + } else { + newFragment = new FileDetailFragment(mFile, mAccount); + mWaitingToPreview = true; + } + + FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); + ft.replace(R.id.fragment, newFragment, FileDetailFragment.FTAG); + ft.commit(); + } + + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(KEY_WAITING_TO_PREVIEW, mWaitingToPreview); + } + + + /** Defines callbacks for service binding, passed to bindService() */ + private class DetailsServiceConnection implements ServiceConnection { + + @Override + public void onServiceConnected(ComponentName component, IBinder service) { + + if (component.equals(new ComponentName(PreviewImageActivity.this, FileDownloader.class))) { + Log.d(TAG, "Download service connected"); + mDownloaderBinder = (FileDownloaderBinder) service; + if (mWaitingToPreview) { + requestForDownload(); + } + + } else if (component.equals(new ComponentName(PreviewImageActivity.this, FileUploader.class))) { + Log.d(TAG, "Upload service connected"); + mUploaderBinder = (FileUploaderBinder) service; + } else { + return; + } + + Fragment fragment = getSupportFragmentManager().findFragmentByTag(FileDetailFragment.FTAG); + FileDetailFragment detailsFragment = (fragment instanceof FileDetailFragment) ? (FileDetailFragment) fragment : null; + if (detailsFragment != null) { + detailsFragment.listenForTransferProgress(); + detailsFragment.updateFileDetails(mWaitingToPreview); // let the fragment gets the mDownloadBinder through getDownloadBinder() (see FileDetailFragment#updateFileDetais()) + } + } + + @Override + public void onServiceDisconnected(ComponentName component) { + if (component.equals(new ComponentName(PreviewImageActivity.this, FileDownloader.class))) { + Log.d(TAG, "Download service disconnected"); + mDownloaderBinder = null; + } else if (component.equals(new ComponentName(PreviewImageActivity.this, FileUploader.class))) { + Log.d(TAG, "Upload service disconnected"); + mUploaderBinder = null; + } + } + }; + + + @Override + public void onDestroy() { + super.onDestroy(); + if (mDownloadConnection != null) { + unbindService(mDownloadConnection); + mDownloadConnection = null; + } + if (mUploadConnection != null) { + unbindService(mUploadConnection); + mUploadConnection = null; + } + } + + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + boolean returnValue = false; + + switch(item.getItemId()){ + case android.R.id.home: + backToDisplayActivity(); + returnValue = true; + break; + default: + returnValue = super.onOptionsItemSelected(item); + } + + return returnValue; + } + + + @Override + protected void onResume() { + super.onResume(); + Fragment fragment = getSupportFragmentManager().findFragmentByTag(FileDetailFragment.FTAG); + if (fragment != null && fragment instanceof FileDetailFragment) { + ((FileDetailFragment) fragment).updateFileDetails(false); + } + } + + + private void backToDisplayActivity() { + /* + Intent intent = new Intent(this, FileDisplayActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.putExtra(FileDetailFragment.EXTRA_FILE, mFile); + intent.putExtra(FileDetailFragment.EXTRA_ACCOUNT, mAccount); + startActivity(intent); + */ + finish(); + } + + + @Override + protected Dialog onCreateDialog(int id) { + Dialog dialog = null; + switch (id) { + case DIALOG_SHORT_WAIT: { + ProgressDialog working_dialog = new ProgressDialog(this); + working_dialog.setMessage(getResources().getString( + R.string.wait_a_moment)); + working_dialog.setIndeterminate(true); + working_dialog.setCancelable(false); + dialog = working_dialog; + break; + } + default: + dialog = null; + } + return dialog; + } + + + /** + * {@inheritDoc} + */ + @Override + public void onFileStateChanged() { + // nothing to do here! + } + + + /** + * {@inheritDoc} + */ + @Override + public FileDownloaderBinder getFileDownloaderBinder() { + return mDownloaderBinder; + } + + + @Override + public FileUploaderBinder getFileUploaderBinder() { + return mUploaderBinder; + } + + + @Override + public void showFragmentWithDetails(OCFile file) { + Intent showDetailsIntent = new Intent(this, FileDetailActivity.class); + showDetailsIntent.putExtra(FileDetailFragment.EXTRA_FILE, file); + showDetailsIntent.putExtra(FileDetailFragment.EXTRA_ACCOUNT, AccountUtils.getCurrentOwnCloudAccount(this)); + startActivity(showDetailsIntent); + } + + + private void requestForDownload() { + if (!mDownloaderBinder.isDownloading(mAccount, mFile)) { + Intent i = new Intent(this, FileDownloader.class); + i.putExtra(FileDownloader.EXTRA_ACCOUNT, mAccount); + i.putExtra(FileDownloader.EXTRA_FILE, mFile); + startService(i); + } + } + + @Override + public void notifySuccessfulDownload(OCFile file, Intent intent, boolean success) { + if (success) { + if (mWaitingToPreview) { + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + transaction.replace(R.id.fragment, new FilePreviewFragment(file, mAccount), FileDetailFragment.FTAG); + transaction.commit(); + mWaitingToPreview = false; + } + } + } + +} diff --git a/src/com/owncloud/android/ui/activity/PreviewVideoActivity.java b/src/com/owncloud/android/ui/activity/PreviewVideoActivity.java new file mode 100644 index 00000000..a3c30f6a --- /dev/null +++ b/src/com/owncloud/android/ui/activity/PreviewVideoActivity.java @@ -0,0 +1,194 @@ +/* ownCloud Android client application + * Copyright (C) 2012-2013 ownCloud Inc. + * + * 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 2 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 . + * + */ + +package com.owncloud.android.ui.activity; + +import android.accounts.Account; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.media.MediaPlayer; +import android.media.MediaPlayer.OnCompletionListener; +import android.media.MediaPlayer.OnErrorListener; +import android.media.MediaPlayer.OnPreparedListener; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.view.MotionEvent; +import android.widget.MediaController; +import android.widget.VideoView; + +import com.owncloud.android.AccountUtils; +import com.owncloud.android.R; +import com.owncloud.android.datamodel.OCFile; +import com.owncloud.android.media.MediaService; + +/** + * Activity implementing a basic video player. + * + * Used as an utility to preview video files contained in an ownCloud account. + * + * Currently, it always plays in landscape mode, full screen. When the playback ends, + * the activity is finished. + * + * @author David A. Velasco + */ +public class PreviewVideoActivity extends Activity implements OnCompletionListener, OnPreparedListener, OnErrorListener { + + /** Key to receive an {@link OCFile} to play as an extra value in an {@link Intent} */ + public static final String EXTRA_FILE = "FILE"; + /** Key to receive the ownCloud {@link Account} where the file to play is saved as an extra value in an {@link Intent} */ + public static final String EXTRA_ACCOUNT = "ACCOUNT"; + + private static final String TAG = null; + + private OCFile mFile; // video file to play + private Account mAccount; // ownCloud account holding mFile + private VideoView mVideoPlayer; // view to play the file; both performs and show the playback + private MediaController mMediaController; // panel control used by the user to control the playback + + /** + * Called when the activity is first created. + * + * Searches for an {@link OCFile} and ownCloud {@link Account} holding it in the starting {@link Intent}. + * + * The {@link Account} is unnecessary if the file is downloaded; else, the {@link Account} is used to + * try to stream the remote file - TODO get the streaming works + * + * {@inheritDoc} + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.video_layout); + + mFile = getIntent().getExtras().getParcelable(EXTRA_FILE); + mAccount = getIntent().getExtras().getParcelable(EXTRA_ACCOUNT); + + mVideoPlayer = (VideoView) findViewById(R.id.videoPlayer); + + // set listeners to get more contol on the playback + mVideoPlayer.setOnPreparedListener(this); + mVideoPlayer.setOnCompletionListener(this); + mVideoPlayer.setOnErrorListener(this); + + // keep the screen on while the playback is performed (prevents screen off by battery save) + mVideoPlayer.setKeepScreenOn(true); + + if (mFile != null) { + if (mFile.isDown()) { + mVideoPlayer.setVideoPath(mFile.getStoragePath()); + + } else if (mAccount != null) { + String url = AccountUtils.constructFullURLForAccount(this, mAccount) + mFile.getRemotePath(); + mVideoPlayer.setVideoURI(Uri.parse(url)); + + } else { + onError(null, MediaService.OC_MEDIA_ERROR, R.string.media_err_no_account); + } + + // create and prepare control panel for the user + mMediaController = new MediaController(this); + mMediaController.setMediaPlayer(mVideoPlayer); + mMediaController.setAnchorView(mVideoPlayer); + mVideoPlayer.setMediaController(mMediaController); + + } else { + onError(null, MediaService.OC_MEDIA_ERROR, R.string.media_err_nothing_to_play); + } + } + + + /** + * Called when the file is ready to be played. + * + * Just starts the playback. + * + * @param mp {@link MediaPlayer} instance performing the playback. + */ + @Override + public void onPrepared(MediaPlayer vp) { + mVideoPlayer.start(); + mMediaController.show(5000); + } + + + /** + * Called when the file is finished playing. + * + * Finishes the activity. + * + * @param mp {@link MediaPlayer} instance performing the playback. + */ + @Override + public void onCompletion(MediaPlayer mp) { + this.finish(); + } + + + /** + * Called when an error in playback occurs. + * + * @param mp {@link MediaPlayer} instance performing the playback. + * @param what Type of error + * @param extra Extra code specific to the error + */ + @Override + public boolean onError(MediaPlayer mp, int what, int extra) { + Log.e(TAG, "Error in video playback, what = " + what + ", extra = " + extra); + + if (mMediaController != null) { + mMediaController.hide(); + } + + if (mVideoPlayer.getWindowToken() != null) { + String message = MediaService.getMessageForMediaError(this, what, extra); + new AlertDialog.Builder(this) + .setMessage(message) + .setPositiveButton(android.R.string.VideoView_error_button, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + PreviewVideoActivity.this.onCompletion(null); + } + }) + .setCancelable(false) + .show(); + } + return true; + } + + + /** + * Screen touches trigger the appearance of the control panel for a limited time. + * + * {@inheritDoc} + */ + @Override + public boolean onTouchEvent (MotionEvent ev){ + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mMediaController.show(MediaService.MEDIA_CONTROL_SHORT_LIFE); + return true; + } else { + return false; + } + } + + +} \ No newline at end of file diff --git a/src/com/owncloud/android/ui/activity/VideoActivity.java b/src/com/owncloud/android/ui/activity/VideoActivity.java deleted file mode 100644 index be0f8621..00000000 --- a/src/com/owncloud/android/ui/activity/VideoActivity.java +++ /dev/null @@ -1,194 +0,0 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2013 ownCloud Inc. - * - * 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 2 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 . - * - */ - -package com.owncloud.android.ui.activity; - -import android.accounts.Account; -import android.app.Activity; -import android.app.AlertDialog; -import android.content.DialogInterface; -import android.content.Intent; -import android.media.MediaPlayer; -import android.media.MediaPlayer.OnCompletionListener; -import android.media.MediaPlayer.OnErrorListener; -import android.media.MediaPlayer.OnPreparedListener; -import android.net.Uri; -import android.os.Bundle; -import android.util.Log; -import android.view.MotionEvent; -import android.widget.MediaController; -import android.widget.VideoView; - -import com.owncloud.android.AccountUtils; -import com.owncloud.android.R; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.media.MediaService; - -/** - * Activity implementing a basic video player. - * - * Used as an utility to preview video files contained in an ownCloud account. - * - * Currently, it always plays in landscape mode, full screen. When the playback ends, - * the activity is finished. - * - * @author David A. Velasco - */ -public class VideoActivity extends Activity implements OnCompletionListener, OnPreparedListener, OnErrorListener { - - /** Key to receive an {@link OCFile} to play as an extra value in an {@link Intent} */ - public static final String EXTRA_FILE = "FILE"; - /** Key to receive the ownCloud {@link Account} where the file to play is saved as an extra value in an {@link Intent} */ - public static final String EXTRA_ACCOUNT = "ACCOUNT"; - - private static final String TAG = null; - - private OCFile mFile; // video file to play - private Account mAccount; // ownCloud account holding mFile - private VideoView mVideoPlayer; // view to play the file; both performs and show the playback - private MediaController mMediaController; // panel control used by the user to control the playback - - /** - * Called when the activity is first created. - * - * Searches for an {@link OCFile} and ownCloud {@link Account} holding it in the starting {@link Intent}. - * - * The {@link Account} is unnecessary if the file is downloaded; else, the {@link Account} is used to - * try to stream the remote file - TODO get the streaming works - * - * {@inheritDoc} - */ - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.video_layout); - - mFile = getIntent().getExtras().getParcelable(EXTRA_FILE); - mAccount = getIntent().getExtras().getParcelable(EXTRA_ACCOUNT); - - mVideoPlayer = (VideoView) findViewById(R.id.videoPlayer); - - // set listeners to get more contol on the playback - mVideoPlayer.setOnPreparedListener(this); - mVideoPlayer.setOnCompletionListener(this); - mVideoPlayer.setOnErrorListener(this); - - // keep the screen on while the playback is performed (prevents screen off by battery save) - mVideoPlayer.setKeepScreenOn(true); - - if (mFile != null) { - if (mFile.isDown()) { - mVideoPlayer.setVideoPath(mFile.getStoragePath()); - - } else if (mAccount != null) { - String url = AccountUtils.constructFullURLForAccount(this, mAccount) + mFile.getRemotePath(); - mVideoPlayer.setVideoURI(Uri.parse(url)); - - } else { - onError(null, MediaService.OC_MEDIA_ERROR, R.string.media_err_no_account); - } - - // create and prepare control panel for the user - mMediaController = new MediaController(this); - mMediaController.setMediaPlayer(mVideoPlayer); - mMediaController.setAnchorView(mVideoPlayer); - mVideoPlayer.setMediaController(mMediaController); - - } else { - onError(null, MediaService.OC_MEDIA_ERROR, R.string.media_err_nothing_to_play); - } - } - - - /** - * Called when the file is ready to be played. - * - * Just starts the playback. - * - * @param mp {@link MediaPlayer} instance performing the playback. - */ - @Override - public void onPrepared(MediaPlayer vp) { - mVideoPlayer.start(); - mMediaController.show(5000); - } - - - /** - * Called when the file is finished playing. - * - * Finishes the activity. - * - * @param mp {@link MediaPlayer} instance performing the playback. - */ - @Override - public void onCompletion(MediaPlayer mp) { - this.finish(); - } - - - /** - * Called when an error in playback occurs. - * - * @param mp {@link MediaPlayer} instance performing the playback. - * @param what Type of error - * @param extra Extra code specific to the error - */ - @Override - public boolean onError(MediaPlayer mp, int what, int extra) { - Log.e(TAG, "Error in video playback, what = " + what + ", extra = " + extra); - - if (mMediaController != null) { - mMediaController.hide(); - } - - if (mVideoPlayer.getWindowToken() != null) { - String message = MediaService.getMessageForMediaError(this, what, extra); - new AlertDialog.Builder(this) - .setMessage(message) - .setPositiveButton(android.R.string.VideoView_error_button, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - VideoActivity.this.onCompletion(null); - } - }) - .setCancelable(false) - .show(); - } - return true; - } - - - /** - * Screen touches trigger the appearance of the control panel for a limited time. - * - * {@inheritDoc} - */ - @Override - public boolean onTouchEvent (MotionEvent ev){ - if (ev.getAction() == MotionEvent.ACTION_DOWN) { - mMediaController.show(MediaService.MEDIA_CONTROL_SHORT_LIFE); - return true; - } else { - return false; - } - } - - -} \ No newline at end of file diff --git a/src/com/owncloud/android/ui/fragment/FileDetailFragment.java b/src/com/owncloud/android/ui/fragment/FileDetailFragment.java index 2416ec2c..da215317 100644 --- a/src/com/owncloud/android/ui/fragment/FileDetailFragment.java +++ b/src/com/owncloud/android/ui/fragment/FileDetailFragment.java @@ -102,7 +102,7 @@ import com.owncloud.android.ui.activity.FileDetailActivity; import com.owncloud.android.ui.activity.FileDisplayActivity; import com.owncloud.android.ui.OnSwipeTouchListener; import com.owncloud.android.ui.activity.TransferServiceGetter; -import com.owncloud.android.ui.activity.VideoActivity; +import com.owncloud.android.ui.activity.PreviewVideoActivity; import com.owncloud.android.ui.dialog.EditNameDialog; import com.owncloud.android.ui.dialog.EditNameDialog.EditNameDialogListener; import com.owncloud.android.utils.OwnCloudVersion; @@ -420,9 +420,9 @@ public class FileDetailFragment extends SherlockFragment implements private void startVideoActivity() { - Intent i = new Intent(getActivity(), VideoActivity.class); - i.putExtra(VideoActivity.EXTRA_FILE, mFile); - i.putExtra(VideoActivity.EXTRA_ACCOUNT, mAccount); + Intent i = new Intent(getActivity(), PreviewVideoActivity.class); + i.putExtra(PreviewVideoActivity.EXTRA_FILE, mFile); + i.putExtra(PreviewVideoActivity.EXTRA_ACCOUNT, mAccount); startActivity(i); } diff --git a/src/com/owncloud/android/ui/fragment/FilePreviewFragment.java b/src/com/owncloud/android/ui/fragment/FilePreviewFragment.java index e95c7d1f..817e8c44 100644 --- a/src/com/owncloud/android/ui/fragment/FilePreviewFragment.java +++ b/src/com/owncloud/android/ui/fragment/FilePreviewFragment.java @@ -111,7 +111,7 @@ import com.owncloud.android.ui.activity.FileDetailActivity; import com.owncloud.android.ui.activity.FileDisplayActivity; import com.owncloud.android.ui.OnSwipeTouchListener; import com.owncloud.android.ui.activity.TransferServiceGetter; -import com.owncloud.android.ui.activity.VideoActivity; +import com.owncloud.android.ui.activity.PreviewVideoActivity; import com.owncloud.android.ui.dialog.EditNameDialog; import com.owncloud.android.ui.dialog.EditNameDialog.EditNameDialogListener; import com.owncloud.android.utils.OwnCloudVersion; diff --git a/src/com/owncloud/android/ui/fragment/PreviewImageFragment.java b/src/com/owncloud/android/ui/fragment/PreviewImageFragment.java new file mode 100644 index 00000000..f083a46a --- /dev/null +++ b/src/com/owncloud/android/ui/fragment/PreviewImageFragment.java @@ -0,0 +1,594 @@ +/* ownCloud Android client application + * Copyright (C) 2012-2013 ownCloud Inc. + * + * 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 2 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 . + * + */ +package com.owncloud.android.ui.fragment; + +import java.io.File; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + + +import android.accounts.Account; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BitmapFactory.Options; +import android.graphics.Point; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.util.Log; +import android.view.Display; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.MimeTypeMap; +import android.widget.ImageView; +import android.widget.Toast; + +import com.actionbarsherlock.app.SherlockFragment; +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuInflater; +import com.actionbarsherlock.view.MenuItem; +import com.owncloud.android.datamodel.FileDataStorageManager; +import com.owncloud.android.datamodel.OCFile; +import com.owncloud.android.network.OwnCloudClientUtils; +import com.owncloud.android.operations.OnRemoteOperationListener; +import com.owncloud.android.operations.RemoteOperation; +import com.owncloud.android.operations.RemoteOperationResult; +import com.owncloud.android.operations.RemoveFileOperation; +import com.owncloud.android.ui.activity.PreviewImageActivity; + +import com.owncloud.android.R; +import eu.alefzero.webdav.WebdavClient; +import eu.alefzero.webdav.WebdavUtils; + + +/** + * This fragment shows a preview of a downloaded image. + * + * Trying to get an instance with NULL {@link OCFile} or ownCloud {@link Account} values will produce an {@link IllegalStateException}. + * + * If the {@link OCFile} passed is not downloaded, an {@link IllegalStateException} is generated on instantiation too. + * + * @author David A. Velasco + */ +public class PreviewImageFragment extends SherlockFragment implements FileFragment, + OnRemoteOperationListener, + ConfirmationDialogFragment.ConfirmationDialogFragmentListener{ + public static final String EXTRA_FILE = "FILE"; + public static final String EXTRA_ACCOUNT = "ACCOUNT"; + + private View mView; + private OCFile mFile; + private Account mAccount; + private FileDataStorageManager mStorageManager; + private ImageView mImageView; + public Bitmap mBitmap = null; + + private Handler mHandler; + private RemoteOperation mLastRemoteOperation; + + private static final String TAG = PreviewImageFragment.class.getSimpleName(); + + + /** + * Creates a fragment to preview an image. + * + * When 'imageFile' or 'ocAccount' are null + * + * @param imageFile An {@link OCFile} to preview as an image in the fragment + * @param ocAccount An ownCloud account; needed to start downloads + */ + public PreviewImageFragment(OCFile fileToDetail, Account ocAccount) { + mFile = fileToDetail; + mAccount = ocAccount; + mStorageManager = null; // we need a context to init this; the container activity is not available yet at this moment + } + + + /** + * Creates an empty fragment for image previews. + * + * MUST BE KEPT: the system uses it when tries to reinstantiate a fragment automatically (for instance, when the device is turned a aside). + * + * DO NOT CALL IT: an {@link OCFile} and {@link Account} must be provided for a successful construction + */ + public PreviewImageFragment() { + mFile = null; + mAccount = null; + mStorageManager = null; + } + + + /** + * {@inheritDoc} + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mHandler = new Handler(); + setHasOptionsMenu(true); + } + + + /** + * {@inheritDoc} + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + mView = inflater.inflate(R.layout.preview_image_fragment, container, false); + mImageView = (ImageView)mView.findViewById(R.id.image); + return mView; + } + + + /** + * {@inheritDoc} + */ + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + if (!(activity instanceof FileFragment.ContainerActivity)) + throw new ClassCastException(activity.toString() + " must implement " + FileFragment.ContainerActivity.class.getSimpleName()); + } + + + /** + * {@inheritDoc} + */ + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + mStorageManager = new FileDataStorageManager(mAccount, getActivity().getApplicationContext().getContentResolver()); + if (savedInstanceState != null) { + mFile = savedInstanceState.getParcelable(PreviewImageFragment.EXTRA_FILE); + mAccount = savedInstanceState.getParcelable(PreviewImageFragment.EXTRA_ACCOUNT); + + } + if (mFile == null) { + throw new IllegalStateException("Instanced with a NULL OCFile"); + } + if (mAccount == null) { + throw new IllegalStateException("Instanced with a NULL ownCloud Account"); + } + if (!mFile.isDown()) { + throw new IllegalStateException("There is no local file to preview"); + } + } + + + /** + * {@inheritDoc} + */ + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putParcelable(PreviewImageFragment.EXTRA_FILE, mFile); + outState.putParcelable(PreviewImageFragment.EXTRA_ACCOUNT, mAccount); + } + + + @Override + public void onStart() { + super.onStart(); + if (mFile != null) { + BitmapLoader bl = new BitmapLoader(mImageView); + bl.execute(new String[]{mFile.getStoragePath()}); + } + } + + + /** + * {@inheritDoc} + */ + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + + inflater.inflate(R.menu.file_actions_menu, menu); + List toHide = new ArrayList(); + + MenuItem item = null; + toHide.add(R.id.action_cancel_download); + toHide.add(R.id.action_cancel_upload); + toHide.add(R.id.action_download_file); + toHide.add(R.id.action_rename_file); // by now + + for (int i : toHide) { + item = menu.findItem(i); + if (item != null) { + item.setVisible(false); + item.setEnabled(false); + } + } + + } + + + /** + * {@inheritDoc} + */ + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_open_file_with: { + openFile(); + return true; + } + case R.id.action_remove_file: { + removeFile(); + return true; + } + case R.id.action_see_details: { + seeDetails(); + return true; + } + + default: + return false; + } + } + + + private void seeDetails() { + ((FileFragment.ContainerActivity)getActivity()).showFragmentWithDetails(mFile); + } + + + @Override + public void onResume() { + super.onResume(); + /* + mDownloadFinishReceiver = new DownloadFinishReceiver(); + IntentFilter filter = new IntentFilter( + FileDownloader.DOWNLOAD_FINISH_MESSAGE); + getActivity().registerReceiver(mDownloadFinishReceiver, filter); + + mUploadFinishReceiver = new UploadFinishReceiver(); + filter = new IntentFilter(FileUploader.UPLOAD_FINISH_MESSAGE); + getActivity().registerReceiver(mUploadFinishReceiver, filter); + */ + + } + + + @Override + public void onPause() { + super.onPause(); + /* + if (mVideoPreview.getVisibility() == View.VISIBLE) { + mSavedPlaybackPosition = mVideoPreview.getCurrentPosition(); + }*/ + /* + getActivity().unregisterReceiver(mDownloadFinishReceiver); + mDownloadFinishReceiver = null; + + getActivity().unregisterReceiver(mUploadFinishReceiver); + mUploadFinishReceiver = null; + */ + } + + + @Override + public void onStop() { + super.onStop(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mBitmap != null) { + mBitmap.recycle(); + } + } + + + /** + * Opens the previewed image with an external application. + * + * TODO - improve this; instead of prioritize the actions available for the MIME type in the server, + * we should get a list of available apps for MIME tpye in the server and join it with the list of + * available apps for the MIME type known from the file extension, to let the user choose + */ + private void openFile() { + String storagePath = mFile.getStoragePath(); + String encodedStoragePath = WebdavUtils.encodePath(storagePath); + try { + Intent i = new Intent(Intent.ACTION_VIEW); + i.setDataAndType(Uri.parse("file://"+ encodedStoragePath), mFile.getMimetype()); + i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + startActivity(i); + + } catch (Throwable t) { + Log.e(TAG, "Fail when trying to open with the mimeType provided from the ownCloud server: " + mFile.getMimetype()); + boolean toastIt = true; + String mimeType = ""; + try { + Intent i = new Intent(Intent.ACTION_VIEW); + mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(storagePath.substring(storagePath.lastIndexOf('.') + 1)); + if (mimeType == null || !mimeType.equals(mFile.getMimetype())) { + if (mimeType != null) { + i.setDataAndType(Uri.parse("file://"+ encodedStoragePath), mimeType); + } else { + // desperate try + i.setDataAndType(Uri.parse("file://"+ encodedStoragePath), "*-/*"); + } + i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + startActivity(i); + toastIt = false; + } + + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, "Trying to find out MIME type of a file without extension: " + storagePath); + + } catch (ActivityNotFoundException e) { + Log.e(TAG, "No activity found to handle: " + storagePath + " with MIME type " + mimeType + " obtained from extension"); + + } catch (Throwable th) { + Log.e(TAG, "Unexpected problem when opening: " + storagePath, th); + + } finally { + if (toastIt) { + Toast.makeText(getActivity(), "There is no application to handle file " + mFile.getFileName(), Toast.LENGTH_SHORT).show(); + } + } + + } + finish(); + } + + + /** + * Starts a the removal of the previewed file. + * + * Shows a confirmation dialog. The action continues in {@link #onConfirmation(String)} , {@link #onNeutral(String)} or {@link #onCancel(String)}, + * depending upon the user selection in the dialog. + */ + private void removeFile() { + ConfirmationDialogFragment confDialog = ConfirmationDialogFragment.newInstance( + R.string.confirmation_remove_alert, + new String[]{mFile.getFileName()}, + R.string.confirmation_remove_remote_and_local, + R.string.confirmation_remove_local, + R.string.common_cancel); + confDialog.setOnConfirmationListener(this); + confDialog.show(getFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION); + } + + + /** + * Performs the removal of the previewed file, both locally and in the server. + */ + @Override + public void onConfirmation(String callerTag) { + if (mStorageManager.getFileById(mFile.getFileId()) != null) { // check that the file is still there; + mLastRemoteOperation = new RemoveFileOperation( mFile, // TODO we need to review the interface with RemoteOperations, and use OCFile IDs instead of OCFile objects as parameters + true, + mStorageManager); + WebdavClient wc = OwnCloudClientUtils.createOwnCloudClient(mAccount, getSherlockActivity().getApplicationContext()); + mLastRemoteOperation.execute(wc, this, mHandler); + + getActivity().showDialog(PreviewImageActivity.DIALOG_SHORT_WAIT); + } + } + + + /** + * Removes the file from local storage + */ + @Override + public void onNeutral(String callerTag) { + // TODO this code should be made in a secondary thread, + if (mFile.isDown()) { // checks it is still there + File f = new File(mFile.getStoragePath()); + f.delete(); + mFile.setStoragePath(null); + mStorageManager.saveFile(mFile); + finish(); + } + } + + /** + * User cancelled the removal action. + */ + @Override + public void onCancel(String callerTag) { + // nothing to do here + } + + + /** + * {@inheritDoc} + */ + public OCFile getFile(){ + return mFile; + } + + /* + /** + * Use this method to signal this Activity that it shall update its view. + * + * @param file : An {@link OCFile} + *-/ + public void updateFileDetails(OCFile file, Account ocAccount) { + mFile = file; + if (ocAccount != null && ( + mStorageManager == null || + (mAccount != null && !mAccount.equals(ocAccount)) + )) { + mStorageManager = new FileDataStorageManager(ocAccount, getActivity().getApplicationContext().getContentResolver()); + } + mAccount = ocAccount; + updateFileDetails(false); + } + */ + + + private class BitmapLoader extends AsyncTask { + + /** + * Weak reference to the target {@link ImageView} where the bitmap will be loaded into. + * + * Using a weak reference will avoid memory leaks if the target ImageView is retired from memory before the load finishes. + */ + private final WeakReference mImageViewRef; + + + /** + * Constructor. + * + * @param imageView Target {@link ImageView} where the bitmap will be loaded into. + */ + public BitmapLoader(ImageView imageView) { + mImageViewRef = new WeakReference(imageView); + } + + + @SuppressWarnings("deprecation") + @SuppressLint({ "NewApi", "NewApi", "NewApi" }) // to avoid Lint errors since Android SDK r20 + @Override + protected Bitmap doInBackground(String... params) { + Bitmap result = null; + if (params.length != 1) return result; + String storagePath = params[0]; + try { + // set desired options that will affect the size of the bitmap + BitmapFactory.Options options = new Options(); + options.inScaled = true; + options.inPurgeable = true; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + options.inPreferQualityOverSpeed = false; + } + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) { + options.inMutable = false; + } + // make a false load of the bitmap - just to be able to read outWidth, outHeight and outMimeType + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(storagePath, options); + + int width = options.outWidth; + int height = options.outHeight; + int scale = 1; + if (width >= 2048 || height >= 2048) { + // try to scale down the image to save memory + scale = (int) Math.ceil((Math.ceil(Math.max(height, width) / 2048.))); + options.inSampleSize = scale; + } + Display display = getActivity().getWindowManager().getDefaultDisplay(); + Point size = new Point(); + int screenwidth; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB_MR2) { + display.getSize(size); + screenwidth = size.x; + } else { + screenwidth = display.getWidth(); + } + + Log.d(TAG, "image width: " + width + ", screen width: " + screenwidth); + + if (width > screenwidth) { + // second try to scale down the image , this time depending upon the screen size; WTF... + scale = (int) Math.ceil((float)width / screenwidth); + options.inSampleSize = scale; + } + + // really load the bitmap + options.inJustDecodeBounds = false; // the next decodeFile call will be real + result = BitmapFactory.decodeFile(storagePath, options); + Log.e(TAG, "loaded width: " + options.outWidth + ", loaded height: " + options.outHeight); + + } catch (OutOfMemoryError e) { + result = null; + Log.e(TAG, "Out of memory occured for file with size " + storagePath); + + } catch (NoSuchFieldError e) { + result = null; + Log.e(TAG, "Error from access to unexisting field despite protection " + storagePath); + + } catch (Throwable t) { + result = null; + Log.e(TAG, "Unexpected error while creating image preview " + storagePath, t); + } + return result; + } + + @Override + protected void onPostExecute(Bitmap result) { + if (result != null && mImageViewRef != null) { + final ImageView imageView = mImageViewRef.get(); + imageView.setImageBitmap(result); + mBitmap = result; + } + } + + } + + /** + * Helper method to test if an {@link OCFile} can be passed to a {@link PreviewImageFragment} to be previewed. + * + * @param file File to test if can be previewed. + * @return 'True' if the file can be handled by the fragment. + */ + public static boolean canBePreviewed(OCFile file) { + return (file != null && file.isImage()); + } + + /** + * {@inheritDoc} + */ + @Override + public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationResult result) { + if (operation.equals(mLastRemoteOperation) && operation instanceof RemoveFileOperation) { + onRemoveFileOperationFinish((RemoveFileOperation)operation, result); + } + } + + private void onRemoveFileOperationFinish(RemoveFileOperation operation, RemoteOperationResult result) { + getActivity().dismissDialog(PreviewImageActivity.DIALOG_SHORT_WAIT); + + if (result.isSuccess()) { + Toast msg = Toast.makeText(getActivity().getApplicationContext(), R.string.remove_success_msg, Toast.LENGTH_LONG); + msg.show(); + finish(); + + } else { + Toast msg = Toast.makeText(getActivity(), R.string.remove_fail_msg, Toast.LENGTH_LONG); + msg.show(); + if (result.isSslRecoverableException()) { + // TODO show the SSL warning dialog + } + } + } + + /** + * Finishes the preview + */ + private void finish() { + Activity container = getActivity(); + container.finish(); + } + + +}