mirror of https://github.com/tuskyapp/Tusky.git
Browse Source
* Extract duplicated code into BaseActivity * Migrate MediaUtils to kotlin * Migrate ViewVideoActivity to kotlin * Migrate ViewMediaActivity to kotlin * Initial media sharing functionality * Address code review feedback * Make share icon match * Address code review feedbackpull/871/head^2
18 changed files with 785 additions and 690 deletions
@ -1,287 +0,0 @@ |
|||||||
/* Copyright 2017 Andrew Dawson |
|
||||||
* |
|
||||||
* This file is a part of Tusky. |
|
||||||
* |
|
||||||
* 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. |
|
||||||
* |
|
||||||
* Tusky 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 Tusky; if not, |
|
||||||
* see <http://www.gnu.org/licenses>. */
|
|
||||||
|
|
||||||
package com.keylesspalace.tusky; |
|
||||||
|
|
||||||
import android.Manifest; |
|
||||||
import android.animation.Animator; |
|
||||||
import android.animation.AnimatorListenerAdapter; |
|
||||||
import android.app.DownloadManager; |
|
||||||
import android.content.Context; |
|
||||||
import android.content.Intent; |
|
||||||
import android.content.pm.PackageManager; |
|
||||||
import android.graphics.Color; |
|
||||||
import android.net.Uri; |
|
||||||
import android.os.Build; |
|
||||||
import android.os.Bundle; |
|
||||||
import android.os.Environment; |
|
||||||
import android.support.annotation.NonNull; |
|
||||||
import android.support.annotation.Nullable; |
|
||||||
import android.support.annotation.StringRes; |
|
||||||
import android.support.design.widget.Snackbar; |
|
||||||
import android.support.v4.app.ActivityCompat; |
|
||||||
import android.support.v4.content.ContextCompat; |
|
||||||
import android.support.v4.view.PagerAdapter; |
|
||||||
import android.support.v4.view.ViewPager; |
|
||||||
import android.support.v7.app.ActionBar; |
|
||||||
import android.support.v7.widget.Toolbar; |
|
||||||
import android.view.Menu; |
|
||||||
import android.view.View; |
|
||||||
import android.widget.Toast; |
|
||||||
|
|
||||||
import com.keylesspalace.tusky.entity.Attachment; |
|
||||||
import com.keylesspalace.tusky.fragment.ViewMediaFragment; |
|
||||||
import com.keylesspalace.tusky.pager.AvatarImagePagerAdapter; |
|
||||||
import com.keylesspalace.tusky.pager.ImagePagerAdapter; |
|
||||||
import com.keylesspalace.tusky.view.ImageViewPager; |
|
||||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData; |
|
||||||
|
|
||||||
import java.io.File; |
|
||||||
import java.util.ArrayList; |
|
||||||
import java.util.List; |
|
||||||
|
|
||||||
import kotlin.collections.CollectionsKt; |
|
||||||
import kotlin.jvm.functions.Function0; |
|
||||||
|
|
||||||
public final class ViewMediaActivity extends BaseActivity |
|
||||||
implements ViewMediaFragment.PhotoActionsListener { |
|
||||||
private static final String EXTRA_ATTACHMENTS = "attachments"; |
|
||||||
private static final String EXTRA_ATTACHMENT_INDEX = "index"; |
|
||||||
private static final String EXTRA_AVATAR_URL = "avatar"; |
|
||||||
|
|
||||||
public static Intent newIntent(Context context, List<AttachmentViewData> attachments, int index) { |
|
||||||
final Intent intent = new Intent(context, ViewMediaActivity.class); |
|
||||||
intent.putParcelableArrayListExtra(EXTRA_ATTACHMENTS, new ArrayList<>(attachments)); |
|
||||||
intent.putExtra(EXTRA_ATTACHMENT_INDEX, index); |
|
||||||
return intent; |
|
||||||
} |
|
||||||
|
|
||||||
public static Intent newAvatarIntent(Context context, String url) { |
|
||||||
final Intent intent = new Intent(context, ViewMediaActivity.class); |
|
||||||
intent.putExtra(EXTRA_AVATAR_URL, url); |
|
||||||
return intent; |
|
||||||
} |
|
||||||
|
|
||||||
private static final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 1; |
|
||||||
|
|
||||||
private ImageViewPager viewPager; |
|
||||||
private View anyView; |
|
||||||
private Toolbar toolbar; |
|
||||||
|
|
||||||
private List<AttachmentViewData> attachments; |
|
||||||
|
|
||||||
private boolean isToolbarVisible = true; |
|
||||||
private final List<ToolbarVisibilityListener> toolbarVisibilityListeners = new ArrayList<>(); |
|
||||||
|
|
||||||
public interface ToolbarVisibilityListener { |
|
||||||
void onToolbarVisiblityChanged(boolean isVisible); |
|
||||||
} |
|
||||||
|
|
||||||
public Function0 addToolbarVisibilityListener(ToolbarVisibilityListener listener) { |
|
||||||
this.toolbarVisibilityListeners.add(listener); |
|
||||||
listener.onToolbarVisiblityChanged(isToolbarVisible); |
|
||||||
return () -> toolbarVisibilityListeners.remove(listener); |
|
||||||
} |
|
||||||
|
|
||||||
public boolean isToolbarVisible() { |
|
||||||
return isToolbarVisible; |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) { |
|
||||||
super.onCreate(savedInstanceState); |
|
||||||
setContentView(R.layout.activity_view_media); |
|
||||||
|
|
||||||
supportPostponeEnterTransition(); |
|
||||||
|
|
||||||
// Obtain the views.
|
|
||||||
toolbar = findViewById(R.id.toolbar); |
|
||||||
viewPager = findViewById(R.id.view_pager); |
|
||||||
anyView = toolbar; |
|
||||||
|
|
||||||
// Gather the parameters.
|
|
||||||
Intent intent = getIntent(); |
|
||||||
attachments = intent.getParcelableArrayListExtra(EXTRA_ATTACHMENTS); |
|
||||||
int initialPosition = intent.getIntExtra(EXTRA_ATTACHMENT_INDEX, 0); |
|
||||||
|
|
||||||
final PagerAdapter adapter; |
|
||||||
|
|
||||||
if(attachments != null) { |
|
||||||
List<Attachment> realAttachs = |
|
||||||
CollectionsKt.map(attachments, AttachmentViewData::getAttachment); |
|
||||||
// Setup the view pager.
|
|
||||||
adapter = new ImagePagerAdapter(getSupportFragmentManager(), |
|
||||||
realAttachs, initialPosition); |
|
||||||
|
|
||||||
} else { |
|
||||||
String avatarUrl = intent.getStringExtra(EXTRA_AVATAR_URL); |
|
||||||
|
|
||||||
if(avatarUrl == null) { |
|
||||||
throw new IllegalArgumentException("attachment list or avatar url has to be set"); |
|
||||||
} |
|
||||||
|
|
||||||
adapter = new AvatarImagePagerAdapter(getSupportFragmentManager(), avatarUrl); |
|
||||||
} |
|
||||||
|
|
||||||
viewPager.setAdapter(adapter); |
|
||||||
viewPager.setCurrentItem(initialPosition); |
|
||||||
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { |
|
||||||
@Override |
|
||||||
public void onPageScrolled(int position, float positionOffset, |
|
||||||
int positionOffsetPixels) { |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onPageSelected(int position) { |
|
||||||
CharSequence title = adapter.getPageTitle(position); |
|
||||||
toolbar.setTitle(title); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onPageScrollStateChanged(int state) { |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
// Setup the toolbar.
|
|
||||||
setSupportActionBar(toolbar); |
|
||||||
ActionBar actionBar = getSupportActionBar(); |
|
||||||
if (actionBar != null) { |
|
||||||
actionBar.setDisplayHomeAsUpEnabled(true); |
|
||||||
actionBar.setDisplayShowHomeEnabled(true); |
|
||||||
actionBar.setTitle(adapter.getPageTitle(initialPosition)); |
|
||||||
} |
|
||||||
toolbar.setNavigationOnClickListener(v -> supportFinishAfterTransition()); |
|
||||||
toolbar.setOnMenuItemClickListener(item -> { |
|
||||||
int id = item.getItemId(); |
|
||||||
switch (id) { |
|
||||||
case R.id.action_download: |
|
||||||
downloadImage(); |
|
||||||
break; |
|
||||||
case R.id.action_open_status: |
|
||||||
onOpenStatus(); |
|
||||||
break; |
|
||||||
} |
|
||||||
return true; |
|
||||||
}); |
|
||||||
|
|
||||||
View decorView = getWindow().getDecorView(); |
|
||||||
int uiOptions = View.SYSTEM_UI_FLAG_LOW_PROFILE; |
|
||||||
decorView.setSystemUiVisibility(uiOptions); |
|
||||||
getWindow().setStatusBarColor(Color.BLACK); |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public boolean onCreateOptionsMenu(Menu menu) { |
|
||||||
if(attachments != null) { |
|
||||||
getMenuInflater().inflate(R.menu.view_media_toolbar, menu); |
|
||||||
return true; |
|
||||||
} else { |
|
||||||
return false; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onBringUp() { |
|
||||||
supportStartPostponedEnterTransition(); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onDismiss() { |
|
||||||
supportFinishAfterTransition(); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onPhotoTap() { |
|
||||||
isToolbarVisible = !isToolbarVisible; |
|
||||||
for (ToolbarVisibilityListener listener : toolbarVisibilityListeners) { |
|
||||||
listener.onToolbarVisiblityChanged(isToolbarVisible); |
|
||||||
} |
|
||||||
final int visibility = isToolbarVisible ? View.VISIBLE : View.INVISIBLE; |
|
||||||
int alpha = isToolbarVisible ? 1 : 0; |
|
||||||
|
|
||||||
toolbar.animate().alpha(alpha) |
|
||||||
.setListener(new AnimatorListenerAdapter() { |
|
||||||
@Override |
|
||||||
public void onAnimationEnd(Animator animation) { |
|
||||||
toolbar.setVisibility(visibility); |
|
||||||
animation.removeListener(this); |
|
||||||
} |
|
||||||
}) |
|
||||||
.start(); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], |
|
||||||
@NonNull int[] grantResults) { |
|
||||||
switch (requestCode) { |
|
||||||
case PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE: { |
|
||||||
if (grantResults.length > 0 |
|
||||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) { |
|
||||||
downloadImage(); |
|
||||||
} else { |
|
||||||
doErrorDialog(R.string.error_media_download_permission, R.string.action_retry, |
|
||||||
v -> downloadImage()); |
|
||||||
} |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private void doErrorDialog(@StringRes int descriptionId, @StringRes int actionId, |
|
||||||
View.OnClickListener listener) { |
|
||||||
if (anyView != null) { |
|
||||||
Snackbar bar = Snackbar.make(anyView, getString(descriptionId), |
|
||||||
Snackbar.LENGTH_SHORT); |
|
||||||
bar.setAction(actionId, listener); |
|
||||||
bar.show(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private void downloadImage() { |
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && |
|
||||||
ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) |
|
||||||
!= PackageManager.PERMISSION_GRANTED) { |
|
||||||
ActivityCompat.requestPermissions(this, |
|
||||||
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, |
|
||||||
PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE); |
|
||||||
} else { |
|
||||||
String url = attachments.get(viewPager.getCurrentItem()).getAttachment().getUrl(); |
|
||||||
Uri uri = Uri.parse(url); |
|
||||||
|
|
||||||
String filename = new File(url).getName(); |
|
||||||
|
|
||||||
String toastText = String.format(getResources().getString(R.string.download_image), |
|
||||||
filename); |
|
||||||
Toast.makeText(this.getApplicationContext(), toastText, Toast.LENGTH_SHORT).show(); |
|
||||||
|
|
||||||
DownloadManager downloadManager = |
|
||||||
(DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE); |
|
||||||
|
|
||||||
DownloadManager.Request request = new DownloadManager.Request(uri); |
|
||||||
request.allowScanningByMediaScanner(); |
|
||||||
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES, |
|
||||||
getString(R.string.app_name) + "/" + filename); |
|
||||||
|
|
||||||
downloadManager.enqueue(request); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private void onOpenStatus() { |
|
||||||
final AttachmentViewData attach = attachments.get(viewPager.getCurrentItem()); |
|
||||||
startActivityWithSlideInAnimation(ViewThreadActivity.startIntent(this, attach.getStatusId(), |
|
||||||
attach.getStatusUrl())); |
|
||||||
} |
|
||||||
} |
|
||||||
@ -0,0 +1,239 @@ |
|||||||
|
/* Copyright 2017 Andrew Dawson |
||||||
|
* |
||||||
|
* This file is a part of Tusky. |
||||||
|
* |
||||||
|
* 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. |
||||||
|
* |
||||||
|
* Tusky 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 Tusky; if not, |
||||||
|
* see <http://www.gnu.org/licenses>. */ |
||||||
|
|
||||||
|
package com.keylesspalace.tusky |
||||||
|
|
||||||
|
import android.animation.Animator |
||||||
|
import android.animation.AnimatorListenerAdapter |
||||||
|
import android.content.Context |
||||||
|
import android.content.Intent |
||||||
|
import android.content.pm.PackageManager |
||||||
|
import android.graphics.Bitmap |
||||||
|
import android.graphics.Color |
||||||
|
import android.graphics.drawable.Drawable |
||||||
|
import android.net.Uri |
||||||
|
import android.os.Bundle |
||||||
|
import android.support.v4.content.FileProvider |
||||||
|
import android.support.v4.view.ViewPager |
||||||
|
import android.util.Log |
||||||
|
import android.view.Menu |
||||||
|
import android.view.MenuItem |
||||||
|
import android.view.View |
||||||
|
import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID |
||||||
|
|
||||||
|
import com.keylesspalace.tusky.fragment.ViewMediaFragment |
||||||
|
import com.keylesspalace.tusky.pager.AvatarImagePagerAdapter |
||||||
|
import com.keylesspalace.tusky.pager.ImagePagerAdapter |
||||||
|
import com.keylesspalace.tusky.util.CollectionUtil.map |
||||||
|
import com.keylesspalace.tusky.util.getTemporaryMediaFilename |
||||||
|
import com.keylesspalace.tusky.viewdata.AttachmentViewData |
||||||
|
import com.squareup.picasso.Picasso |
||||||
|
import com.squareup.picasso.Target |
||||||
|
|
||||||
|
import kotlinx.android.synthetic.main.activity_view_media.* |
||||||
|
|
||||||
|
import java.io.File |
||||||
|
import java.io.FileNotFoundException |
||||||
|
import java.io.FileOutputStream |
||||||
|
import java.io.IOException |
||||||
|
import java.util.ArrayList |
||||||
|
|
||||||
|
class ViewMediaActivity : BaseActivity(), ViewMediaFragment.PhotoActionsListener { |
||||||
|
companion object { |
||||||
|
private const val EXTRA_ATTACHMENTS = "attachments" |
||||||
|
private const val EXTRA_ATTACHMENT_INDEX = "index" |
||||||
|
private const val EXTRA_AVATAR_URL = "avatar" |
||||||
|
private const val TAG = "ViewMediaActivity" |
||||||
|
|
||||||
|
@JvmStatic |
||||||
|
fun newIntent(context: Context?, attachments: List<AttachmentViewData>, index: Int): Intent { |
||||||
|
val intent = Intent(context, ViewMediaActivity::class.java) |
||||||
|
intent.putParcelableArrayListExtra(EXTRA_ATTACHMENTS, ArrayList(attachments)) |
||||||
|
intent.putExtra(EXTRA_ATTACHMENT_INDEX, index) |
||||||
|
return intent |
||||||
|
} |
||||||
|
|
||||||
|
fun newAvatarIntent(context: Context, url: String): Intent { |
||||||
|
val intent = Intent(context, ViewMediaActivity::class.java) |
||||||
|
intent.putExtra(EXTRA_AVATAR_URL, url) |
||||||
|
return intent |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private var attachments: ArrayList<AttachmentViewData>? = null |
||||||
|
|
||||||
|
private var toolbarVisible = true |
||||||
|
private val toolbarVisibilityListeners = ArrayList<ToolbarVisibilityListener>() |
||||||
|
|
||||||
|
interface ToolbarVisibilityListener { |
||||||
|
fun onToolbarVisiblityChanged(isVisible: Boolean) |
||||||
|
} |
||||||
|
|
||||||
|
fun addToolbarVisibilityListener(listener: ToolbarVisibilityListener): Function0<Boolean> { |
||||||
|
this.toolbarVisibilityListeners.add(listener) |
||||||
|
listener.onToolbarVisiblityChanged(toolbarVisible) |
||||||
|
return { toolbarVisibilityListeners.remove(listener) } |
||||||
|
} |
||||||
|
|
||||||
|
fun isToolbarVisible(): Boolean { |
||||||
|
return toolbarVisible |
||||||
|
} |
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) { |
||||||
|
super.onCreate(savedInstanceState) |
||||||
|
setContentView(R.layout.activity_view_media) |
||||||
|
|
||||||
|
supportPostponeEnterTransition() |
||||||
|
|
||||||
|
// Gather the parameters. |
||||||
|
attachments = intent.getParcelableArrayListExtra(EXTRA_ATTACHMENTS) |
||||||
|
val initialPosition = intent.getIntExtra(EXTRA_ATTACHMENT_INDEX, 0) |
||||||
|
|
||||||
|
val adapter = if(attachments != null) { |
||||||
|
val realAttachs = map(attachments, AttachmentViewData::attachment) |
||||||
|
// Setup the view pager. |
||||||
|
ImagePagerAdapter(supportFragmentManager, realAttachs, initialPosition) |
||||||
|
|
||||||
|
} else { |
||||||
|
val avatarUrl = intent.getStringExtra(EXTRA_AVATAR_URL) ?: throw IllegalArgumentException("attachment list or avatar url has to be set") |
||||||
|
|
||||||
|
AvatarImagePagerAdapter(supportFragmentManager, avatarUrl) |
||||||
|
} |
||||||
|
|
||||||
|
viewPager.adapter = adapter |
||||||
|
viewPager.currentItem = initialPosition |
||||||
|
viewPager.addOnPageChangeListener(object: ViewPager.OnPageChangeListener { |
||||||
|
override fun onPageSelected(position: Int) { |
||||||
|
toolbar.title = adapter.getPageTitle(position) |
||||||
|
} |
||||||
|
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {} |
||||||
|
override fun onPageScrollStateChanged(state: Int) {} |
||||||
|
}) |
||||||
|
|
||||||
|
// Setup the toolbar. |
||||||
|
setSupportActionBar(toolbar) |
||||||
|
val actionBar = supportActionBar |
||||||
|
if (actionBar != null) { |
||||||
|
actionBar.setDisplayHomeAsUpEnabled(true) |
||||||
|
actionBar.setDisplayShowHomeEnabled(true) |
||||||
|
actionBar.title = adapter.getPageTitle(initialPosition) |
||||||
|
} |
||||||
|
toolbar.setNavigationOnClickListener { _ -> supportFinishAfterTransition() } |
||||||
|
toolbar.setOnMenuItemClickListener { item: MenuItem -> |
||||||
|
when (item.itemId) { |
||||||
|
R.id.action_download -> downloadImage() |
||||||
|
R.id.action_open_status -> onOpenStatus() |
||||||
|
R.id.action_share_media -> shareImage() |
||||||
|
} |
||||||
|
true |
||||||
|
} |
||||||
|
|
||||||
|
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE |
||||||
|
window.statusBarColor = Color.BLACK |
||||||
|
} |
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean { |
||||||
|
if(attachments != null) { |
||||||
|
menuInflater.inflate(R.menu.view_media_toolbar, menu) |
||||||
|
return true |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
override fun onBringUp() { |
||||||
|
supportStartPostponedEnterTransition() |
||||||
|
} |
||||||
|
|
||||||
|
override fun onDismiss() { |
||||||
|
supportFinishAfterTransition() |
||||||
|
} |
||||||
|
|
||||||
|
override fun onPhotoTap() { |
||||||
|
toolbarVisible = !toolbarVisible |
||||||
|
for (listener in toolbarVisibilityListeners) { |
||||||
|
listener.onToolbarVisiblityChanged(toolbarVisible) |
||||||
|
} |
||||||
|
val visibility = if(toolbarVisible){ View.VISIBLE } else { View.INVISIBLE } |
||||||
|
val alpha = if(toolbarVisible){ 1.0f } else { 0.0f } |
||||||
|
|
||||||
|
toolbar.animate().alpha(alpha) |
||||||
|
.setListener(object: AnimatorListenerAdapter() { |
||||||
|
override fun onAnimationEnd(animation: Animator) { |
||||||
|
toolbar.visibility = visibility |
||||||
|
animation.removeListener(this) |
||||||
|
} |
||||||
|
}) |
||||||
|
.start() |
||||||
|
} |
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { |
||||||
|
when (requestCode) { |
||||||
|
PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE -> { |
||||||
|
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { |
||||||
|
downloadImage() |
||||||
|
} else { |
||||||
|
showErrorDialog(toolbar, R.string.error_media_download_permission, R.string.action_retry) { _ -> downloadImage() } |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun downloadImage() { |
||||||
|
downloadFile(attachments!![viewPager.currentItem].attachment.url) |
||||||
|
} |
||||||
|
|
||||||
|
private fun onOpenStatus() { |
||||||
|
val attach = attachments!![viewPager.currentItem] |
||||||
|
startActivityWithSlideInAnimation(ViewThreadActivity.startIntent(this, attach.statusId, attach.statusUrl)) |
||||||
|
} |
||||||
|
|
||||||
|
private fun shareImage() { |
||||||
|
val directory = applicationContext.getExternalFilesDir("Tusky") |
||||||
|
if (directory == null || !(directory.exists())) { |
||||||
|
Log.e(TAG, "Error obtaining directory to save temporary media.") |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
val attachment = attachments!![viewPager.currentItem].attachment |
||||||
|
val context = applicationContext |
||||||
|
val file = File(directory, getTemporaryMediaFilename("png")) |
||||||
|
|
||||||
|
Picasso.with(context).load(Uri.parse(attachment.url)).into(object: Target { |
||||||
|
override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) { |
||||||
|
try { |
||||||
|
val stream = FileOutputStream(file) |
||||||
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) |
||||||
|
stream.close() |
||||||
|
} catch (fnfe: FileNotFoundException) { |
||||||
|
Log.e(TAG, "Error writing temporary media.") |
||||||
|
} catch (ioe: IOException) { |
||||||
|
Log.e(TAG, "Error writing temporary media.") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override fun onBitmapFailed(errorDrawable: Drawable) { |
||||||
|
Log.e(TAG, "Error loading temporary media.") |
||||||
|
} |
||||||
|
|
||||||
|
override fun onPrepareLoad(placeHolderDrawable: Drawable) { } |
||||||
|
}) |
||||||
|
|
||||||
|
val sendIntent = Intent() |
||||||
|
sendIntent.action = Intent.ACTION_SEND |
||||||
|
sendIntent.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(context, "$APPLICATION_ID.fileprovider", file)) |
||||||
|
sendIntent.type = "image/png" |
||||||
|
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_media_to))) |
||||||
|
} |
||||||
|
} |
||||||
@ -1,119 +0,0 @@ |
|||||||
/* Copyright 2017 Andrew Dawson |
|
||||||
* |
|
||||||
* This file is a part of Tusky. |
|
||||||
* |
|
||||||
* 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. |
|
||||||
* |
|
||||||
* Tusky 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 Tusky; if not, |
|
||||||
* see <http://www.gnu.org/licenses>. */
|
|
||||||
|
|
||||||
package com.keylesspalace.tusky; |
|
||||||
|
|
||||||
import android.animation.Animator; |
|
||||||
import android.animation.AnimatorListenerAdapter; |
|
||||||
import android.graphics.Color; |
|
||||||
import android.media.MediaPlayer; |
|
||||||
import android.os.Bundle; |
|
||||||
import android.os.Handler; |
|
||||||
import android.os.Looper; |
|
||||||
import android.support.v7.app.ActionBar; |
|
||||||
import android.support.v7.widget.Toolbar; |
|
||||||
import android.view.MenuItem; |
|
||||||
import android.view.MotionEvent; |
|
||||||
import android.view.View; |
|
||||||
import android.widget.MediaController; |
|
||||||
import android.widget.ProgressBar; |
|
||||||
import android.widget.VideoView; |
|
||||||
|
|
||||||
public class ViewVideoActivity extends BaseActivity { |
|
||||||
|
|
||||||
Handler handler = new Handler(Looper.getMainLooper()); |
|
||||||
Toolbar toolbar; |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onCreate(Bundle savedInstanceState) { |
|
||||||
super.onCreate(savedInstanceState); |
|
||||||
setContentView(R.layout.activity_view_video); |
|
||||||
|
|
||||||
final ProgressBar progressBar = findViewById(R.id.video_progress); |
|
||||||
VideoView videoView = findViewById(R.id.video_player); |
|
||||||
|
|
||||||
toolbar = findViewById(R.id.toolbar); |
|
||||||
setSupportActionBar(toolbar); |
|
||||||
ActionBar bar = getSupportActionBar(); |
|
||||||
if (bar != null) { |
|
||||||
bar.setTitle(null); |
|
||||||
bar.setDisplayHomeAsUpEnabled(true); |
|
||||||
bar.setDisplayShowHomeEnabled(true); |
|
||||||
} |
|
||||||
|
|
||||||
String url = getIntent().getStringExtra("url"); |
|
||||||
|
|
||||||
videoView.setVideoPath(url); |
|
||||||
MediaController controller = new MediaController(this); |
|
||||||
controller.setMediaPlayer(videoView); |
|
||||||
videoView.setMediaController(controller); |
|
||||||
videoView.requestFocus(); |
|
||||||
videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { |
|
||||||
@Override |
|
||||||
public void onPrepared(MediaPlayer mp) { |
|
||||||
progressBar.setVisibility(View.GONE); |
|
||||||
mp.setLooping(true); |
|
||||||
hideToolbarAfterDelay(); |
|
||||||
} |
|
||||||
}); |
|
||||||
videoView.start(); |
|
||||||
|
|
||||||
videoView.setOnTouchListener(new View.OnTouchListener() { |
|
||||||
@Override |
|
||||||
public boolean onTouch(View v, MotionEvent event) { |
|
||||||
if (event.getAction() == MotionEvent.ACTION_DOWN) { |
|
||||||
handler.removeCallbacksAndMessages(null); |
|
||||||
toolbar.animate().cancel(); |
|
||||||
toolbar.setAlpha(1); |
|
||||||
toolbar.setVisibility(View.VISIBLE); |
|
||||||
hideToolbarAfterDelay(); |
|
||||||
} |
|
||||||
return false; |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
getWindow().setStatusBarColor(Color.BLACK); |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public boolean onOptionsItemSelected(MenuItem item) { |
|
||||||
switch (item.getItemId()) { |
|
||||||
case android.R.id.home: { |
|
||||||
onBackPressed(); |
|
||||||
return true; |
|
||||||
} |
|
||||||
} |
|
||||||
return super.onOptionsItemSelected(item); |
|
||||||
} |
|
||||||
|
|
||||||
void hideToolbarAfterDelay() { |
|
||||||
handler.postDelayed(new Runnable() { |
|
||||||
@Override |
|
||||||
public void run() { |
|
||||||
toolbar.animate().alpha(0).setListener(new AnimatorListenerAdapter() { |
|
||||||
@Override |
|
||||||
public void onAnimationEnd(Animator animation) { |
|
||||||
View decorView = getWindow().getDecorView(); |
|
||||||
int uiOptions = View.SYSTEM_UI_FLAG_LOW_PROFILE; |
|
||||||
decorView.setSystemUiVisibility(uiOptions); |
|
||||||
toolbar.setVisibility(View.INVISIBLE); |
|
||||||
animation.removeListener(this); |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
}, 3000); |
|
||||||
} |
|
||||||
} |
|
||||||
@ -0,0 +1,185 @@ |
|||||||
|
/* Copyright 2017 Andrew Dawson |
||||||
|
* |
||||||
|
* This file is a part of Tusky. |
||||||
|
* |
||||||
|
* 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. |
||||||
|
* |
||||||
|
* Tusky 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 Tusky; if not, |
||||||
|
* see <http://www.gnu.org/licenses>. */ |
||||||
|
|
||||||
|
package com.keylesspalace.tusky |
||||||
|
|
||||||
|
import android.animation.Animator |
||||||
|
import android.animation.AnimatorListenerAdapter |
||||||
|
import android.annotation.SuppressLint |
||||||
|
import android.app.DownloadManager |
||||||
|
import android.content.Context |
||||||
|
import android.content.Intent |
||||||
|
import android.content.pm.PackageManager |
||||||
|
import android.graphics.Color |
||||||
|
import android.net.Uri |
||||||
|
import android.os.Bundle |
||||||
|
import android.os.Handler |
||||||
|
import android.os.Looper |
||||||
|
import android.support.v4.content.FileProvider |
||||||
|
import android.util.Log |
||||||
|
import android.view.Menu |
||||||
|
import android.view.MenuItem |
||||||
|
import android.view.MotionEvent |
||||||
|
import android.view.View |
||||||
|
import android.webkit.MimeTypeMap |
||||||
|
import android.widget.MediaController |
||||||
|
|
||||||
|
import kotlinx.android.synthetic.main.activity_view_video.* |
||||||
|
|
||||||
|
import java.io.File |
||||||
|
|
||||||
|
import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID |
||||||
|
import com.keylesspalace.tusky.util.getTemporaryMediaFilename |
||||||
|
import com.keylesspalace.tusky.util.hide |
||||||
|
import com.keylesspalace.tusky.util.show |
||||||
|
|
||||||
|
class ViewVideoActivity: BaseActivity() { |
||||||
|
|
||||||
|
private val handler = Handler(Looper.getMainLooper()) |
||||||
|
private lateinit var url: String |
||||||
|
private lateinit var statusID: String |
||||||
|
private lateinit var statusURL: String |
||||||
|
|
||||||
|
companion object { |
||||||
|
private const val TAG = "ViewVideoActivity" |
||||||
|
const val URL_EXTRA = "url" |
||||||
|
const val STATUS_ID_EXTRA = "statusID" |
||||||
|
const val STATUS_URL_EXTRA = "statusURL" |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility") |
||||||
|
override fun onCreate(savedInstanceState: Bundle?) { |
||||||
|
super.onCreate(savedInstanceState) |
||||||
|
setContentView(R.layout.activity_view_video) |
||||||
|
|
||||||
|
setSupportActionBar(toolbar) |
||||||
|
val bar = supportActionBar |
||||||
|
if (bar != null) { |
||||||
|
bar.title = null |
||||||
|
bar.setDisplayHomeAsUpEnabled(true) |
||||||
|
bar.setDisplayShowHomeEnabled(true) |
||||||
|
} |
||||||
|
toolbar.setOnMenuItemClickListener {item -> |
||||||
|
val id = item.itemId |
||||||
|
when (id) { |
||||||
|
R.id.action_download -> downloadFile(url) |
||||||
|
R.id.action_open_status -> onOpenStatus() |
||||||
|
R.id.action_share_media -> shareVideo() |
||||||
|
} |
||||||
|
true |
||||||
|
} |
||||||
|
|
||||||
|
url = intent.getStringExtra(URL_EXTRA) |
||||||
|
statusID = intent.getStringExtra(STATUS_ID_EXTRA) |
||||||
|
statusURL = intent.getStringExtra(STATUS_URL_EXTRA) |
||||||
|
|
||||||
|
videoPlayer.setVideoPath(url) |
||||||
|
val controller = MediaController(this) |
||||||
|
controller.setMediaPlayer(videoPlayer) |
||||||
|
videoPlayer.setMediaController(controller) |
||||||
|
videoPlayer.requestFocus() |
||||||
|
videoPlayer.setOnPreparedListener { mp -> |
||||||
|
videoProgressBar.hide() |
||||||
|
mp.isLooping = true |
||||||
|
hideToolbarAfterDelay() |
||||||
|
} |
||||||
|
videoPlayer.start() |
||||||
|
|
||||||
|
videoPlayer.setOnTouchListener { _, event -> |
||||||
|
if (event.action == MotionEvent.ACTION_DOWN) { |
||||||
|
handler.removeCallbacksAndMessages(null) |
||||||
|
toolbar.animate().cancel() |
||||||
|
toolbar.alpha = 1.0f |
||||||
|
toolbar.show() |
||||||
|
hideToolbarAfterDelay() |
||||||
|
} |
||||||
|
false |
||||||
|
} |
||||||
|
|
||||||
|
window.statusBarColor = Color.BLACK |
||||||
|
} |
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean { |
||||||
|
when (item.itemId) { |
||||||
|
android.R.id.home -> { |
||||||
|
onBackPressed() |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
return super.onOptionsItemSelected(item) |
||||||
|
} |
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean { |
||||||
|
menuInflater.inflate(R.menu.view_media_toolbar, menu) |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { |
||||||
|
when (requestCode) { |
||||||
|
PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE -> { |
||||||
|
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { |
||||||
|
downloadFile(url) |
||||||
|
} else { |
||||||
|
showErrorDialog(toolbar, R.string.error_media_download_permission, R.string.action_retry) { _ -> downloadFile(url) } |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun hideToolbarAfterDelay() { |
||||||
|
handler.postDelayed({ |
||||||
|
toolbar.animate().alpha(0.0f).setListener(object: AnimatorListenerAdapter() { |
||||||
|
override fun onAnimationEnd(animation: Animator) { |
||||||
|
val decorView = window.decorView |
||||||
|
val uiOptions = View.SYSTEM_UI_FLAG_LOW_PROFILE |
||||||
|
decorView.systemUiVisibility = uiOptions |
||||||
|
toolbar.hide() |
||||||
|
animation.removeListener(this) |
||||||
|
} |
||||||
|
}) |
||||||
|
}, 3000) |
||||||
|
} |
||||||
|
|
||||||
|
private fun onOpenStatus() { |
||||||
|
startActivityWithSlideInAnimation(ViewThreadActivity.startIntent(this, statusID, statusURL)) |
||||||
|
} |
||||||
|
|
||||||
|
private fun shareVideo() { |
||||||
|
val directory = applicationContext.getExternalFilesDir("Tusky") |
||||||
|
if (directory == null || !(directory.exists())) { |
||||||
|
Log.e(TAG, "Error obtaining directory to save temporary media.") |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
val uri = Uri.parse(url) |
||||||
|
val mimeTypeMap = MimeTypeMap.getSingleton() |
||||||
|
val extension = MimeTypeMap.getFileExtensionFromUrl(url) |
||||||
|
val mimeType = mimeTypeMap.getMimeTypeFromExtension(extension) |
||||||
|
val filename = getTemporaryMediaFilename(extension) |
||||||
|
val file = File(directory, filename) |
||||||
|
|
||||||
|
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager |
||||||
|
val request = DownloadManager.Request(uri) |
||||||
|
request.setDestinationUri(Uri.fromFile(file)) |
||||||
|
request.setVisibleInDownloadsUi(false) |
||||||
|
downloadManager.enqueue(request) |
||||||
|
|
||||||
|
val sendIntent = Intent() |
||||||
|
sendIntent.action = Intent.ACTION_SEND |
||||||
|
sendIntent.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(applicationContext, "$APPLICATION_ID.fileprovider", file)) |
||||||
|
sendIntent.type = mimeType |
||||||
|
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_media_to))) |
||||||
|
} |
||||||
|
} |
||||||
@ -1,260 +0,0 @@ |
|||||||
/* Copyright 2017 Andrew Dawson |
|
||||||
* |
|
||||||
* This file is a part of Tusky. |
|
||||||
* |
|
||||||
* 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. |
|
||||||
* |
|
||||||
* Tusky 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 Tusky; if not, |
|
||||||
* see <http://www.gnu.org/licenses>. */
|
|
||||||
|
|
||||||
package com.keylesspalace.tusky.util; |
|
||||||
|
|
||||||
import android.content.ContentResolver; |
|
||||||
import android.content.Context; |
|
||||||
import android.database.Cursor; |
|
||||||
import android.graphics.Bitmap; |
|
||||||
import android.graphics.BitmapFactory; |
|
||||||
import android.graphics.Matrix; |
|
||||||
import android.media.MediaMetadataRetriever; |
|
||||||
import android.media.ThumbnailUtils; |
|
||||||
import android.net.Uri; |
|
||||||
import android.provider.OpenableColumns; |
|
||||||
import android.support.annotation.NonNull; |
|
||||||
import android.support.annotation.Nullable; |
|
||||||
import android.support.annotation.Px; |
|
||||||
import android.support.media.ExifInterface; |
|
||||||
import android.util.Log; |
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream; |
|
||||||
import java.io.FileNotFoundException; |
|
||||||
import java.io.IOException; |
|
||||||
import java.io.InputStream; |
|
||||||
|
|
||||||
/** |
|
||||||
* Class with helper methods for obtaining and resizing media files |
|
||||||
*/ |
|
||||||
public class MediaUtils { |
|
||||||
private static final String TAG = "MediaUtils"; |
|
||||||
public static final int MEDIA_SIZE_UNKNOWN = -1; |
|
||||||
|
|
||||||
/** |
|
||||||
* Copies the entire contents of the given stream into a byte array and returns it. Beware of |
|
||||||
* OutOfMemoryError for streams of unknown size. |
|
||||||
*/ |
|
||||||
@Nullable |
|
||||||
public static byte[] inputStreamGetBytes(InputStream stream) { |
|
||||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream(); |
|
||||||
int read; |
|
||||||
byte[] data = new byte[16384]; |
|
||||||
try { |
|
||||||
while ((read = stream.read(data, 0, data.length)) != -1) { |
|
||||||
buffer.write(data, 0, read); |
|
||||||
} |
|
||||||
buffer.flush(); |
|
||||||
} catch (IOException e) { |
|
||||||
return null; |
|
||||||
} |
|
||||||
return buffer.toByteArray(); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Fetches the size of the media represented by the given URI, assuming it is openable and |
|
||||||
* the ContentResolver is able to resolve it. |
|
||||||
* |
|
||||||
* @return the size of the media in bytes or {@link MediaUtils#MEDIA_SIZE_UNKNOWN} |
|
||||||
*/ |
|
||||||
public static long getMediaSize(@NonNull ContentResolver contentResolver, @Nullable Uri uri) { |
|
||||||
if(uri == null) return MEDIA_SIZE_UNKNOWN; |
|
||||||
long mediaSize; |
|
||||||
Cursor cursor; |
|
||||||
try { |
|
||||||
cursor = contentResolver.query(uri, null, null, null, null); |
|
||||||
} catch (SecurityException e) { |
|
||||||
return MEDIA_SIZE_UNKNOWN; |
|
||||||
} |
|
||||||
if (cursor != null) { |
|
||||||
int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE); |
|
||||||
cursor.moveToFirst(); |
|
||||||
mediaSize = cursor.getLong(sizeIndex); |
|
||||||
cursor.close(); |
|
||||||
} else { |
|
||||||
mediaSize = MEDIA_SIZE_UNKNOWN; |
|
||||||
} |
|
||||||
return mediaSize; |
|
||||||
} |
|
||||||
|
|
||||||
@Nullable |
|
||||||
public static Bitmap getSampledBitmap(ContentResolver contentResolver, Uri uri, @Px int reqWidth, @Px int reqHeight) { |
|
||||||
// First decode with inJustDecodeBounds=true to check dimensions
|
|
||||||
final BitmapFactory.Options options = new BitmapFactory.Options(); |
|
||||||
options.inJustDecodeBounds = true; |
|
||||||
InputStream stream; |
|
||||||
try { |
|
||||||
stream = contentResolver.openInputStream(uri); |
|
||||||
} catch (FileNotFoundException e) { |
|
||||||
Log.w(TAG, e); |
|
||||||
return null; |
|
||||||
} |
|
||||||
|
|
||||||
BitmapFactory.decodeStream(stream, null, options); |
|
||||||
|
|
||||||
IOUtils.closeQuietly(stream); |
|
||||||
|
|
||||||
// Calculate inSampleSize
|
|
||||||
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); |
|
||||||
|
|
||||||
// Decode bitmap with inSampleSize set
|
|
||||||
options.inJustDecodeBounds = false; |
|
||||||
try { |
|
||||||
stream = contentResolver.openInputStream(uri); |
|
||||||
Bitmap bitmap = BitmapFactory.decodeStream(stream, null, options); |
|
||||||
int orientation = getImageOrientation(uri, contentResolver); |
|
||||||
return reorientBitmap(bitmap, orientation); |
|
||||||
} catch (FileNotFoundException e) { |
|
||||||
Log.w(TAG, e); |
|
||||||
return null; |
|
||||||
} catch (OutOfMemoryError e) { |
|
||||||
Log.e(TAG, "OutOfMemoryError while trying to get sampled Bitmap", e); |
|
||||||
return null; |
|
||||||
} finally { |
|
||||||
IOUtils.closeQuietly(stream); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
@Nullable |
|
||||||
public static Bitmap getImageThumbnail(ContentResolver contentResolver, Uri uri, @Px int thumbnailSize) { |
|
||||||
Bitmap source = getSampledBitmap(contentResolver, uri, thumbnailSize, thumbnailSize); |
|
||||||
if(source == null) { |
|
||||||
return null; |
|
||||||
} |
|
||||||
return ThumbnailUtils.extractThumbnail(source, thumbnailSize, thumbnailSize, ThumbnailUtils.OPTIONS_RECYCLE_INPUT); |
|
||||||
} |
|
||||||
|
|
||||||
@Nullable |
|
||||||
public static Bitmap getVideoThumbnail(Context context, Uri uri, @Px int thumbnailSize) { |
|
||||||
MediaMetadataRetriever retriever = new MediaMetadataRetriever(); |
|
||||||
retriever.setDataSource(context, uri); |
|
||||||
Bitmap source = retriever.getFrameAtTime(); |
|
||||||
if (source == null) { |
|
||||||
return null; |
|
||||||
} |
|
||||||
return ThumbnailUtils.extractThumbnail(source, thumbnailSize, thumbnailSize, ThumbnailUtils.OPTIONS_RECYCLE_INPUT); |
|
||||||
} |
|
||||||
|
|
||||||
public static long getImageSquarePixels(ContentResolver contentResolver, Uri uri) throws FileNotFoundException { |
|
||||||
InputStream input = contentResolver.openInputStream(uri); |
|
||||||
|
|
||||||
final BitmapFactory.Options options = new BitmapFactory.Options(); |
|
||||||
options.inJustDecodeBounds = true; |
|
||||||
BitmapFactory.decodeStream(input, null, options); |
|
||||||
|
|
||||||
IOUtils.closeQuietly(input); |
|
||||||
|
|
||||||
return (long) options.outWidth * options.outHeight; |
|
||||||
} |
|
||||||
|
|
||||||
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { |
|
||||||
// Raw height and width of image
|
|
||||||
final int height = options.outHeight; |
|
||||||
final int width = options.outWidth; |
|
||||||
int inSampleSize = 1; |
|
||||||
|
|
||||||
if (height > reqHeight || width > reqWidth) { |
|
||||||
|
|
||||||
final int halfHeight = height / 2; |
|
||||||
final int halfWidth = width / 2; |
|
||||||
|
|
||||||
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
|
|
||||||
// height and width larger than the requested height and width.
|
|
||||||
while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) { |
|
||||||
inSampleSize *= 2; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return inSampleSize; |
|
||||||
} |
|
||||||
|
|
||||||
@Nullable |
|
||||||
public static Bitmap reorientBitmap(Bitmap bitmap, int orientation) { |
|
||||||
Matrix matrix = new Matrix(); |
|
||||||
switch (orientation) { |
|
||||||
default: |
|
||||||
case ExifInterface.ORIENTATION_NORMAL: { |
|
||||||
return bitmap; |
|
||||||
} |
|
||||||
case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: { |
|
||||||
matrix.setScale(-1, 1); |
|
||||||
break; |
|
||||||
} |
|
||||||
case ExifInterface.ORIENTATION_ROTATE_180: { |
|
||||||
matrix.setRotate(180); |
|
||||||
break; |
|
||||||
} |
|
||||||
case ExifInterface.ORIENTATION_FLIP_VERTICAL: { |
|
||||||
matrix.setRotate(180); |
|
||||||
matrix.postScale(-1, 1); |
|
||||||
break; |
|
||||||
} |
|
||||||
case ExifInterface.ORIENTATION_TRANSPOSE: { |
|
||||||
matrix.setRotate(90); |
|
||||||
matrix.postScale(-1, 1); |
|
||||||
break; |
|
||||||
} |
|
||||||
case ExifInterface.ORIENTATION_ROTATE_90: { |
|
||||||
matrix.setRotate(90); |
|
||||||
break; |
|
||||||
} |
|
||||||
case ExifInterface.ORIENTATION_TRANSVERSE: { |
|
||||||
matrix.setRotate(-90); |
|
||||||
matrix.postScale(-1, 1); |
|
||||||
break; |
|
||||||
} |
|
||||||
case ExifInterface.ORIENTATION_ROTATE_270: { |
|
||||||
matrix.setRotate(-90); |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
try { |
|
||||||
Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), |
|
||||||
bitmap.getHeight(), matrix, true); |
|
||||||
if (!bitmap.sameAs(result)) { |
|
||||||
bitmap.recycle(); |
|
||||||
} |
|
||||||
return result; |
|
||||||
} catch (OutOfMemoryError e) { |
|
||||||
return null; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
public static int getImageOrientation(Uri uri, ContentResolver contentResolver) { |
|
||||||
InputStream inputStream; |
|
||||||
try { |
|
||||||
inputStream = contentResolver.openInputStream(uri); |
|
||||||
} catch (FileNotFoundException e) { |
|
||||||
Log.w(TAG, e); |
|
||||||
return ExifInterface.ORIENTATION_UNDEFINED; |
|
||||||
} |
|
||||||
if (inputStream == null) { |
|
||||||
return ExifInterface.ORIENTATION_UNDEFINED; |
|
||||||
} |
|
||||||
ExifInterface exifInterface; |
|
||||||
try { |
|
||||||
exifInterface = new ExifInterface(inputStream); |
|
||||||
} catch (IOException e) { |
|
||||||
Log.w(TAG, e); |
|
||||||
IOUtils.closeQuietly(inputStream); |
|
||||||
return ExifInterface.ORIENTATION_UNDEFINED; |
|
||||||
} |
|
||||||
int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, |
|
||||||
ExifInterface.ORIENTATION_NORMAL); |
|
||||||
IOUtils.closeQuietly(inputStream); |
|
||||||
return orientation; |
|
||||||
} |
|
||||||
} |
|
||||||
@ -0,0 +1,245 @@ |
|||||||
|
/* Copyright 2017 Andrew Dawson |
||||||
|
* |
||||||
|
* This file is a part of Tusky. |
||||||
|
* |
||||||
|
* 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. |
||||||
|
* |
||||||
|
* Tusky 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 Tusky; if not, |
||||||
|
* see <http://www.gnu.org/licenses>. */ |
||||||
|
|
||||||
|
package com.keylesspalace.tusky.util |
||||||
|
|
||||||
|
import android.content.ContentResolver |
||||||
|
import android.content.Context |
||||||
|
import android.database.Cursor |
||||||
|
import android.graphics.Bitmap |
||||||
|
import android.graphics.BitmapFactory |
||||||
|
import android.graphics.Matrix |
||||||
|
import android.media.MediaMetadataRetriever |
||||||
|
import android.media.ThumbnailUtils |
||||||
|
import android.net.Uri |
||||||
|
import android.provider.OpenableColumns |
||||||
|
import android.support.annotation.Px |
||||||
|
import android.support.media.ExifInterface |
||||||
|
import android.util.Log |
||||||
|
import java.io.* |
||||||
|
|
||||||
|
import java.text.SimpleDateFormat |
||||||
|
import java.util.Calendar |
||||||
|
import java.util.Date |
||||||
|
import java.util.Locale |
||||||
|
|
||||||
|
/** |
||||||
|
* Helper methods for obtaining and resizing media files |
||||||
|
*/ |
||||||
|
private const val TAG = "MediaUtils" |
||||||
|
private const val MEDIA_TEMP_PREFIX = "Tusky_Share_Media" |
||||||
|
const val MEDIA_SIZE_UNKNOWN = -1L |
||||||
|
|
||||||
|
/** |
||||||
|
* Fetches the size of the media represented by the given URI, assuming it is openable and |
||||||
|
* the ContentResolver is able to resolve it. |
||||||
|
* |
||||||
|
* @return the size of the media in bytes or {@link MediaUtils#MEDIA_SIZE_UNKNOWN} |
||||||
|
*/ |
||||||
|
fun getMediaSize(contentResolver: ContentResolver, uri: Uri?): Long { |
||||||
|
if(uri == null) { |
||||||
|
return MEDIA_SIZE_UNKNOWN |
||||||
|
} |
||||||
|
|
||||||
|
var mediaSize = MEDIA_SIZE_UNKNOWN |
||||||
|
val cursor: Cursor? |
||||||
|
try { |
||||||
|
cursor = contentResolver.query(uri, null, null, null, null) |
||||||
|
} catch (e: SecurityException) { |
||||||
|
return MEDIA_SIZE_UNKNOWN |
||||||
|
} |
||||||
|
if (cursor != null) { |
||||||
|
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) |
||||||
|
cursor.moveToFirst() |
||||||
|
mediaSize = cursor.getLong(sizeIndex) |
||||||
|
cursor.close() |
||||||
|
} |
||||||
|
return mediaSize |
||||||
|
} |
||||||
|
|
||||||
|
fun getSampledBitmap(contentResolver: ContentResolver, uri: Uri, @Px reqWidth: Int, @Px reqHeight: Int): Bitmap? { |
||||||
|
// First decode with inJustDecodeBounds=true to check dimensions |
||||||
|
val options = BitmapFactory.Options() |
||||||
|
options.inJustDecodeBounds = true |
||||||
|
var stream: InputStream? |
||||||
|
try { |
||||||
|
stream = contentResolver.openInputStream(uri) |
||||||
|
} catch (e: FileNotFoundException) { |
||||||
|
Log.w(TAG, e) |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
BitmapFactory.decodeStream(stream, null, options) |
||||||
|
|
||||||
|
IOUtils.closeQuietly(stream) |
||||||
|
|
||||||
|
// Calculate inSampleSize |
||||||
|
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight) |
||||||
|
|
||||||
|
// Decode bitmap with inSampleSize set |
||||||
|
options.inJustDecodeBounds = false |
||||||
|
return try { |
||||||
|
stream = contentResolver.openInputStream(uri) |
||||||
|
val bitmap = BitmapFactory.decodeStream(stream, null, options) |
||||||
|
val orientation = getImageOrientation(uri, contentResolver) |
||||||
|
reorientBitmap(bitmap, orientation) |
||||||
|
} catch (e: FileNotFoundException) { |
||||||
|
Log.w(TAG, e) |
||||||
|
null |
||||||
|
} catch (e: OutOfMemoryError) { |
||||||
|
Log.e(TAG, "OutOfMemoryError while trying to get sampled Bitmap", e) |
||||||
|
null |
||||||
|
} finally { |
||||||
|
IOUtils.closeQuietly(stream) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun getImageThumbnail(contentResolver: ContentResolver, uri: Uri, @Px thumbnailSize: Int): Bitmap? { |
||||||
|
val source = getSampledBitmap(contentResolver, uri, thumbnailSize, thumbnailSize) ?: return null |
||||||
|
return ThumbnailUtils.extractThumbnail(source, thumbnailSize, thumbnailSize, ThumbnailUtils.OPTIONS_RECYCLE_INPUT) |
||||||
|
} |
||||||
|
|
||||||
|
fun getVideoThumbnail(context: Context, uri: Uri, @Px thumbnailSize: Int): Bitmap? { |
||||||
|
val retriever = MediaMetadataRetriever() |
||||||
|
retriever.setDataSource(context, uri) |
||||||
|
val source = retriever.frameAtTime ?: return null |
||||||
|
return ThumbnailUtils.extractThumbnail(source, thumbnailSize, thumbnailSize, ThumbnailUtils.OPTIONS_RECYCLE_INPUT) |
||||||
|
} |
||||||
|
|
||||||
|
@Throws(FileNotFoundException::class) |
||||||
|
fun getImageSquarePixels(contentResolver: ContentResolver, uri: Uri): Long { |
||||||
|
val input = contentResolver.openInputStream(uri) |
||||||
|
|
||||||
|
val options = BitmapFactory.Options() |
||||||
|
options.inJustDecodeBounds = true |
||||||
|
BitmapFactory.decodeStream(input, null, options) |
||||||
|
|
||||||
|
IOUtils.closeQuietly(input) |
||||||
|
|
||||||
|
return (options.outWidth * options.outHeight).toLong() |
||||||
|
} |
||||||
|
|
||||||
|
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int { |
||||||
|
// Raw height and width of image |
||||||
|
val height = options.outHeight |
||||||
|
val width = options.outWidth |
||||||
|
var inSampleSize = 1 |
||||||
|
|
||||||
|
if (height > reqHeight || width > reqWidth) { |
||||||
|
|
||||||
|
val halfHeight = height / 2 |
||||||
|
val halfWidth = width / 2 |
||||||
|
|
||||||
|
// Calculate the largest inSampleSize value that is a power of 2 and keeps both |
||||||
|
// height and width larger than the requested height and width. |
||||||
|
while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) { |
||||||
|
inSampleSize *= 2 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return inSampleSize |
||||||
|
} |
||||||
|
|
||||||
|
fun reorientBitmap(bitmap: Bitmap?, orientation: Int): Bitmap? { |
||||||
|
val matrix = Matrix() |
||||||
|
when (orientation) { |
||||||
|
ExifInterface.ORIENTATION_NORMAL -> return bitmap |
||||||
|
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.setScale(-1.0f, 1.0f) |
||||||
|
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.setRotate(180.0f) |
||||||
|
ExifInterface.ORIENTATION_FLIP_VERTICAL -> { |
||||||
|
matrix.setRotate(180.0f) |
||||||
|
matrix.postScale(-1.0f, 1.0f) |
||||||
|
} |
||||||
|
ExifInterface.ORIENTATION_TRANSPOSE -> { |
||||||
|
matrix.setRotate(90.0f) |
||||||
|
matrix.postScale(-1.0f, 1.0f) |
||||||
|
} |
||||||
|
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.setRotate(90.0f) |
||||||
|
ExifInterface.ORIENTATION_TRANSVERSE -> { |
||||||
|
matrix.setRotate(-90.0f) |
||||||
|
matrix.postScale(-1.0f, 1.0f) |
||||||
|
} |
||||||
|
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.setRotate(-90.0f) |
||||||
|
else -> return bitmap |
||||||
|
} |
||||||
|
|
||||||
|
if (bitmap == null) { |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
return try { |
||||||
|
val result = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, |
||||||
|
bitmap.height, matrix, true) |
||||||
|
if (!bitmap.sameAs(result)) { |
||||||
|
bitmap.recycle() |
||||||
|
} |
||||||
|
result |
||||||
|
} catch (e: OutOfMemoryError) { |
||||||
|
null |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun getImageOrientation(uri: Uri, contentResolver: ContentResolver): Int { |
||||||
|
val inputStream: InputStream? |
||||||
|
try { |
||||||
|
inputStream = contentResolver.openInputStream(uri) |
||||||
|
} catch (e: FileNotFoundException) { |
||||||
|
Log.w(TAG, e) |
||||||
|
return ExifInterface.ORIENTATION_UNDEFINED |
||||||
|
} |
||||||
|
if (inputStream == null) { |
||||||
|
return ExifInterface.ORIENTATION_UNDEFINED |
||||||
|
} |
||||||
|
val exifInterface: ExifInterface |
||||||
|
try { |
||||||
|
exifInterface = ExifInterface(inputStream) |
||||||
|
} catch (e: IOException) { |
||||||
|
Log.w(TAG, e) |
||||||
|
IOUtils.closeQuietly(inputStream) |
||||||
|
return ExifInterface.ORIENTATION_UNDEFINED |
||||||
|
} |
||||||
|
val orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) |
||||||
|
IOUtils.closeQuietly(inputStream) |
||||||
|
return orientation |
||||||
|
} |
||||||
|
|
||||||
|
fun deleteStaleCachedMedia(mediaDirectory: File?) { |
||||||
|
if (mediaDirectory == null || !mediaDirectory.exists()) { |
||||||
|
// Nothing to do |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
val twentyfourHoursAgo = Calendar.getInstance() |
||||||
|
twentyfourHoursAgo.add(Calendar.HOUR, -24) |
||||||
|
val unixTime = twentyfourHoursAgo.timeInMillis |
||||||
|
|
||||||
|
val files = mediaDirectory.listFiles{ file -> unixTime > file.lastModified() && file.name.contains(MEDIA_TEMP_PREFIX) } |
||||||
|
if (files == null || files.isEmpty()) { |
||||||
|
// Nothing to do |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
for (file in files) { |
||||||
|
try { |
||||||
|
file.delete() |
||||||
|
} catch (se: SecurityException) { |
||||||
|
Log.e(TAG, "Error removing stale cached media") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun getTemporaryMediaFilename(extension: String): String { |
||||||
|
return "${MEDIA_TEMP_PREFIX}_${SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())}.$extension" |
||||||
|
} |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
<!-- |
||||||
|
Copyright (C) 2014 The Android Open Source Project |
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
you may not use this file except in compliance with the License. |
||||||
|
You may obtain a copy of the License at |
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software |
||||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
See the License for the specific language governing permissions and |
||||||
|
limitations under the License. |
||||||
|
--> |
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
android:width="24dp" |
||||||
|
android:height="24dp" |
||||||
|
android:viewportWidth="24.0" |
||||||
|
android:viewportHeight="24.0" |
||||||
|
> |
||||||
|
<path |
||||||
|
android:pathData="M18,16.1c-0.8,0 -1.5,0.3 -2,0.8l-7.1,-4.2C9,12.5 9,12.2 9,12s0,-0.5 -0.1,-0.7L16,7.2C16.5,7.7 17.200001,8 18,8c1.7,0 3,-1.3 3,-3s-1.3,-3 -3,-3s-3,1.3 -3,3c0,0.2 0,0.5 0.1,0.7L8,9.8C7.5,9.3 6.8,9 6,9c-1.7,0 -2.9,1.2 -2.9,2.9s1.3,3 3,3c0.8,0 1.5,-0.3 2,-0.8l7.1,4.2c-0.1,0.3 -0.1,0.5 -0.1,0.7c0,1.6 1.3,2.9 2.9,2.9s2.9,-1.3 2.9,-2.9S19.6,16.1 18,16.1z" |
||||||
|
android:fillColor="#FFF"/> |
||||||
|
</vector> |
||||||
Loading…
Reference in new issue