Enable sharing media directly from Tusky (#852)
* 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 feedback
This commit is contained in:
parent
ab601c4566
commit
0bca94b94e
18 changed files with 785 additions and 690 deletions
|
@ -15,19 +15,31 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky;
|
package com.keylesspalace.tusky;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.app.DownloadManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.PorterDuff;
|
import android.graphics.PorterDuff;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Environment;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.annotation.Nullable;
|
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.v7.app.AppCompatActivity;
|
import android.support.v7.app.AppCompatActivity;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.evernote.android.job.JobManager;
|
import com.evernote.android.job.JobManager;
|
||||||
import com.evernote.android.job.JobRequest;
|
import com.evernote.android.job.JobRequest;
|
||||||
|
@ -36,6 +48,7 @@ import com.keylesspalace.tusky.db.AccountManager;
|
||||||
import com.keylesspalace.tusky.di.Injectable;
|
import com.keylesspalace.tusky.di.Injectable;
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -50,6 +63,8 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
|
||||||
@Inject
|
@Inject
|
||||||
public AccountManager accountManager;
|
public AccountManager accountManager;
|
||||||
|
|
||||||
|
protected static final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 1;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
@ -177,6 +192,36 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
|
||||||
.scheduleAsync();
|
.scheduleAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void downloadFile(String url) {
|
||||||
|
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 filename = new File(url).getName();
|
||||||
|
|
||||||
|
String toastText = String.format(getResources().getString(R.string.download_image), filename);
|
||||||
|
Toast.makeText(getApplicationContext(), toastText, Toast.LENGTH_SHORT).show();
|
||||||
|
|
||||||
|
DownloadManager downloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
|
||||||
|
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
|
||||||
|
request.allowScanningByMediaScanner();
|
||||||
|
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES,
|
||||||
|
getString(R.string.app_name) + "/" + filename);
|
||||||
|
downloadManager.enqueue(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void showErrorDialog(View anyView, @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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
for (Call call : callList) {
|
for (Call call : callList) {
|
||||||
|
|
|
@ -102,7 +102,6 @@ import com.keylesspalace.tusky.service.SendTootService;
|
||||||
import com.keylesspalace.tusky.util.CountUpDownLatch;
|
import com.keylesspalace.tusky.util.CountUpDownLatch;
|
||||||
import com.keylesspalace.tusky.util.DownsizeImageTask;
|
import com.keylesspalace.tusky.util.DownsizeImageTask;
|
||||||
import com.keylesspalace.tusky.util.ListUtils;
|
import com.keylesspalace.tusky.util.ListUtils;
|
||||||
import com.keylesspalace.tusky.util.MediaUtils;
|
|
||||||
import com.keylesspalace.tusky.util.MentionTokenizer;
|
import com.keylesspalace.tusky.util.MentionTokenizer;
|
||||||
import com.keylesspalace.tusky.util.SaveTootHelper;
|
import com.keylesspalace.tusky.util.SaveTootHelper;
|
||||||
import com.keylesspalace.tusky.util.SpanUtilsKt;
|
import com.keylesspalace.tusky.util.SpanUtilsKt;
|
||||||
|
@ -146,6 +145,12 @@ import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
|
|
||||||
|
import static com.keylesspalace.tusky.util.MediaUtilsKt.MEDIA_SIZE_UNKNOWN;
|
||||||
|
import static com.keylesspalace.tusky.util.MediaUtilsKt.getImageSquarePixels;
|
||||||
|
import static com.keylesspalace.tusky.util.MediaUtilsKt.getImageThumbnail;
|
||||||
|
import static com.keylesspalace.tusky.util.MediaUtilsKt.getMediaSize;
|
||||||
|
import static com.keylesspalace.tusky.util.MediaUtilsKt.getSampledBitmap;
|
||||||
|
import static com.keylesspalace.tusky.util.MediaUtilsKt.getVideoThumbnail;
|
||||||
import static com.uber.autodispose.AutoDispose.autoDisposable;
|
import static com.uber.autodispose.AutoDispose.autoDisposable;
|
||||||
import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from;
|
import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from;
|
||||||
|
|
||||||
|
@ -548,12 +553,12 @@ public final class ComposeActivity
|
||||||
if (!ListUtils.isEmpty(loadedDraftMediaUris)) {
|
if (!ListUtils.isEmpty(loadedDraftMediaUris)) {
|
||||||
for (String uriString : loadedDraftMediaUris) {
|
for (String uriString : loadedDraftMediaUris) {
|
||||||
Uri uri = Uri.parse(uriString);
|
Uri uri = Uri.parse(uriString);
|
||||||
long mediaSize = MediaUtils.getMediaSize(getContentResolver(), uri);
|
long mediaSize = getMediaSize(getContentResolver(), uri);
|
||||||
pickMedia(uri, mediaSize);
|
pickMedia(uri, mediaSize);
|
||||||
}
|
}
|
||||||
} else if (savedMediaQueued != null) {
|
} else if (savedMediaQueued != null) {
|
||||||
for (SavedQueuedMedia item : savedMediaQueued) {
|
for (SavedQueuedMedia item : savedMediaQueued) {
|
||||||
Bitmap preview = MediaUtils.getImageThumbnail(getContentResolver(), item.uri, thumbnailViewSize);
|
Bitmap preview = getImageThumbnail(getContentResolver(), item.uri, thumbnailViewSize);
|
||||||
addMediaToQueue(item.id, item.type, preview, item.uri, item.mediaSize, item.readyStage, item.description);
|
addMediaToQueue(item.id, item.type, preview, item.uri, item.mediaSize, item.readyStage, item.description);
|
||||||
}
|
}
|
||||||
} else if (intent != null && savedInstanceState == null) {
|
} else if (intent != null && savedInstanceState == null) {
|
||||||
|
@ -588,7 +593,7 @@ public final class ComposeActivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (Uri uri : uriList) {
|
for (Uri uri : uriList) {
|
||||||
long mediaSize = MediaUtils.getMediaSize(getContentResolver(), uri);
|
long mediaSize = getMediaSize(getContentResolver(), uri);
|
||||||
pickMedia(uri, mediaSize);
|
pickMedia(uri, mediaSize);
|
||||||
}
|
}
|
||||||
} else if (type.equals("text/plain")) {
|
} else if (type.equals("text/plain")) {
|
||||||
|
@ -874,7 +879,7 @@ public final class ComposeActivity
|
||||||
// Just eat this exception.
|
// Just eat this exception.
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mediaSize = MediaUtils.MEDIA_SIZE_UNKNOWN;
|
mediaSize = MEDIA_SIZE_UNKNOWN;
|
||||||
}
|
}
|
||||||
pickMedia(uri, mediaSize);
|
pickMedia(uri, mediaSize);
|
||||||
|
|
||||||
|
@ -1089,7 +1094,7 @@ public final class ComposeActivity
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (type == QueuedMedia.Type.IMAGE &&
|
if (type == QueuedMedia.Type.IMAGE &&
|
||||||
(mediaSize > STATUS_IMAGE_SIZE_LIMIT || MediaUtils.getImageSquarePixels(getContentResolver(), item.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT)) {
|
(mediaSize > STATUS_IMAGE_SIZE_LIMIT || getImageSquarePixels(getContentResolver(), item.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT)) {
|
||||||
downsizeMedia(item);
|
downsizeMedia(item);
|
||||||
} else {
|
} else {
|
||||||
uploadMedia(item);
|
uploadMedia(item);
|
||||||
|
@ -1132,7 +1137,7 @@ public final class ComposeActivity
|
||||||
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
|
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
|
||||||
|
|
||||||
Single.fromCallable(() ->
|
Single.fromCallable(() ->
|
||||||
MediaUtils.getSampledBitmap(getContentResolver(), item.uri, displayMetrics.widthPixels, displayMetrics.heightPixels))
|
getSampledBitmap(getContentResolver(), item.uri, displayMetrics.widthPixels, displayMetrics.heightPixels))
|
||||||
.subscribeOn(Schedulers.computation())
|
.subscribeOn(Schedulers.computation())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
|
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
|
||||||
|
@ -1275,7 +1280,7 @@ public final class ComposeActivity
|
||||||
|
|
||||||
item.preview.setProgress(0);
|
item.preview.setProgress(0);
|
||||||
|
|
||||||
ProgressRequestBody fileBody = new ProgressRequestBody(stream, MediaUtils.getMediaSize(getContentResolver(), item.uri), MediaType.parse(mimeType),
|
ProgressRequestBody fileBody = new ProgressRequestBody(stream, getMediaSize(getContentResolver(), item.uri), MediaType.parse(mimeType),
|
||||||
new ProgressRequestBody.UploadCallback() { // may reference activity longer than I would like to
|
new ProgressRequestBody.UploadCallback() { // may reference activity longer than I would like to
|
||||||
int lastProgress = -1;
|
int lastProgress = -1;
|
||||||
|
|
||||||
|
@ -1350,17 +1355,17 @@ public final class ComposeActivity
|
||||||
super.onActivityResult(requestCode, resultCode, intent);
|
super.onActivityResult(requestCode, resultCode, intent);
|
||||||
if (resultCode == RESULT_OK && requestCode == MEDIA_PICK_RESULT && intent != null) {
|
if (resultCode == RESULT_OK && requestCode == MEDIA_PICK_RESULT && intent != null) {
|
||||||
Uri uri = intent.getData();
|
Uri uri = intent.getData();
|
||||||
long mediaSize = MediaUtils.getMediaSize(getContentResolver(), uri);
|
long mediaSize = getMediaSize(getContentResolver(), uri);
|
||||||
pickMedia(uri, mediaSize);
|
pickMedia(uri, mediaSize);
|
||||||
} else if (resultCode == RESULT_OK && requestCode == MEDIA_TAKE_PHOTO_RESULT) {
|
} else if (resultCode == RESULT_OK && requestCode == MEDIA_TAKE_PHOTO_RESULT) {
|
||||||
long mediaSize = MediaUtils.getMediaSize(getContentResolver(), photoUploadUri);
|
long mediaSize = getMediaSize(getContentResolver(), photoUploadUri);
|
||||||
pickMedia(photoUploadUri, mediaSize);
|
pickMedia(photoUploadUri, mediaSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void pickMedia(Uri uri, long mediaSize) {
|
private void pickMedia(Uri uri, long mediaSize) {
|
||||||
if (mediaSize == MediaUtils.MEDIA_SIZE_UNKNOWN) {
|
if (mediaSize == MEDIA_SIZE_UNKNOWN) {
|
||||||
displayTransientError(R.string.error_media_upload_opening);
|
displayTransientError(R.string.error_media_upload_opening);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1379,7 +1384,7 @@ public final class ComposeActivity
|
||||||
displayTransientError(R.string.error_media_upload_image_or_video);
|
displayTransientError(R.string.error_media_upload_image_or_video);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Bitmap bitmap = MediaUtils.getVideoThumbnail(this, uri, thumbnailViewSize);
|
Bitmap bitmap = getVideoThumbnail(this, uri, thumbnailViewSize);
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
addMediaToQueue(QueuedMedia.Type.VIDEO, bitmap, uri, mediaSize);
|
addMediaToQueue(QueuedMedia.Type.VIDEO, bitmap, uri, mediaSize);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1388,7 +1393,7 @@ public final class ComposeActivity
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "image": {
|
case "image": {
|
||||||
Bitmap bitmap = MediaUtils.getImageThumbnail(contentResolver, uri, thumbnailViewSize);
|
Bitmap bitmap = getImageThumbnail(contentResolver, uri, thumbnailViewSize);
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
addMediaToQueue(QueuedMedia.Type.IMAGE, bitmap, uri, mediaSize);
|
addMediaToQueue(QueuedMedia.Type.IMAGE, bitmap, uri, mediaSize);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -74,6 +74,7 @@ import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
|
|
||||||
|
import static com.keylesspalace.tusky.util.MediaUtilsKt.deleteStaleCachedMedia;
|
||||||
import static com.uber.autodispose.AutoDispose.autoDisposable;
|
import static com.uber.autodispose.AutoDispose.autoDisposable;
|
||||||
import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from;
|
import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from;
|
||||||
|
|
||||||
|
@ -228,6 +229,8 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Flush old media that was cached for sharing
|
||||||
|
deleteStaleCachedMedia(getApplicationContext().getExternalFilesDir("Tusky"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -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()));
|
|
||||||
}
|
|
||||||
}
|
|
239
app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt
Normal file
239
app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt
Normal file
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
185
app/src/main/java/com/keylesspalace/tusky/ViewVideoActivity.kt
Normal file
185
app/src/main/java/com/keylesspalace/tusky/ViewVideoActivity.kt
Normal file
|
@ -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)))
|
||||||
|
}
|
||||||
|
}
|
|
@ -220,7 +220,9 @@ class AccountMediaFragment : BaseFragment(), Injectable {
|
||||||
}
|
}
|
||||||
Attachment.Type.GIFV, Attachment.Type.VIDEO -> {
|
Attachment.Type.GIFV, Attachment.Type.VIDEO -> {
|
||||||
val intent = Intent(context, ViewVideoActivity::class.java)
|
val intent = Intent(context, ViewVideoActivity::class.java)
|
||||||
intent.putExtra("url", items[currentIndex].attachment.url)
|
intent.putExtra(ViewVideoActivity.URL_EXTRA, items[currentIndex].attachment.url)
|
||||||
|
intent.putExtra(ViewVideoActivity.STATUS_ID_EXTRA, items[currentIndex].statusId)
|
||||||
|
intent.putExtra(ViewVideoActivity.STATUS_URL_EXTRA, items[currentIndex].statusUrl)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
Attachment.Type.UNKNOWN -> {
|
Attachment.Type.UNKNOWN -> {
|
||||||
|
|
|
@ -243,7 +243,9 @@ public abstract class SFragment extends BaseFragment {
|
||||||
case GIFV:
|
case GIFV:
|
||||||
case VIDEO: {
|
case VIDEO: {
|
||||||
Intent intent = new Intent(getContext(), ViewVideoActivity.class);
|
Intent intent = new Intent(getContext(), ViewVideoActivity.class);
|
||||||
intent.putExtra("url", active.getUrl());
|
intent.putExtra(ViewVideoActivity.URL_EXTRA, active.getUrl());
|
||||||
|
intent.putExtra(ViewVideoActivity.STATUS_ID_EXTRA, actionable.getId());
|
||||||
|
intent.putExtra(ViewVideoActivity.STATUS_URL_EXTRA, actionable.getUrl());
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,10 @@ import java.io.FileOutputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
import static com.keylesspalace.tusky.util.MediaUtilsKt.calculateInSampleSize;
|
||||||
|
import static com.keylesspalace.tusky.util.MediaUtilsKt.getImageOrientation;
|
||||||
|
import static com.keylesspalace.tusky.util.MediaUtilsKt.reorientBitmap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reduces the file size of images to fit under a given limit by resizing them, maintaining both
|
* Reduces the file size of images to fit under a given limit by resizing them, maintaining both
|
||||||
* aspect ratio and orientation.
|
* aspect ratio and orientation.
|
||||||
|
@ -65,7 +69,7 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
||||||
BitmapFactory.decodeStream(inputStream, null, options);
|
BitmapFactory.decodeStream(inputStream, null, options);
|
||||||
IOUtils.closeQuietly(inputStream);
|
IOUtils.closeQuietly(inputStream);
|
||||||
// Get EXIF data, for orientation info.
|
// Get EXIF data, for orientation info.
|
||||||
int orientation = MediaUtils.getImageOrientation(uri, contentResolver);
|
int orientation = getImageOrientation(uri, contentResolver);
|
||||||
/* Unfortunately, there isn't a determined worst case compression ratio for image
|
/* Unfortunately, there isn't a determined worst case compression ratio for image
|
||||||
* formats. So, the only way to tell if they're too big is to compress them and
|
* formats. So, the only way to tell if they're too big is to compress them and
|
||||||
* test, and keep trying at smaller sizes. The initial estimate should be good for
|
* test, and keep trying at smaller sizes. The initial estimate should be good for
|
||||||
|
@ -84,7 +88,7 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
options.inSampleSize = MediaUtils.calculateInSampleSize(options, scaledImageSize, scaledImageSize);
|
options.inSampleSize = calculateInSampleSize(options, scaledImageSize, scaledImageSize);
|
||||||
options.inJustDecodeBounds = false;
|
options.inJustDecodeBounds = false;
|
||||||
Bitmap scaledBitmap;
|
Bitmap scaledBitmap;
|
||||||
try {
|
try {
|
||||||
|
@ -97,7 +101,7 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
||||||
if (scaledBitmap == null) {
|
if (scaledBitmap == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Bitmap reorientedBitmap = MediaUtils.reorientBitmap(scaledBitmap, orientation);
|
Bitmap reorientedBitmap = reorientBitmap(scaledBitmap, orientation);
|
||||||
if (reorientedBitmap == null) {
|
if (reorientedBitmap == null) {
|
||||||
scaledBitmap.recycle();
|
scaledBitmap.recycle();
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
245
app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.kt
Normal file
245
app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.kt
Normal file
|
@ -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"
|
||||||
|
}
|
|
@ -113,7 +113,7 @@ class EditProfileViewModel @Inject constructor(
|
||||||
|
|
||||||
Single.fromCallable {
|
Single.fromCallable {
|
||||||
val contentResolver = context.contentResolver
|
val contentResolver = context.contentResolver
|
||||||
val sourceBitmap = MediaUtils.getSampledBitmap(contentResolver, uri, resizeWidth, resizeHeight)
|
val sourceBitmap = getSampledBitmap(contentResolver, uri, resizeWidth, resizeHeight)
|
||||||
|
|
||||||
if (sourceBitmap == null) {
|
if (sourceBitmap == null) {
|
||||||
throw Exception()
|
throw Exception()
|
||||||
|
|
25
app/src/main/res/drawable/ic_menu_share_24dp.xml
Normal file
25
app/src/main/res/drawable/ic_menu_share_24dp.xml
Normal file
|
@ -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>
|
|
@ -9,7 +9,7 @@
|
||||||
<com.keylesspalace.tusky.view.ImageViewPager
|
<com.keylesspalace.tusky.view.ImageViewPager
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:id="@+id/view_pager" />
|
android:id="@+id/viewPager" />
|
||||||
|
|
||||||
<android.support.v7.widget.Toolbar
|
<android.support.v7.widget.Toolbar
|
||||||
android:id="@+id/toolbar"
|
android:id="@+id/toolbar"
|
||||||
|
|
|
@ -9,12 +9,12 @@
|
||||||
android:id="@+id/view_video_container"
|
android:id="@+id/view_video_container"
|
||||||
tools:context=".ViewVideoActivity">
|
tools:context=".ViewVideoActivity">
|
||||||
<VideoView
|
<VideoView
|
||||||
android:id="@+id/video_player"
|
android:id="@+id/videoPlayer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="center" />
|
android:layout_gravity="center" />
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/video_progress"
|
android:id="@+id/videoProgressBar"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
@ -23,6 +23,6 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="?attr/actionBarSize"
|
android:layout_height="?attr/actionBarSize"
|
||||||
android:theme="@style/AppTheme.Account.AppBarLayout"
|
android:theme="@style/AppTheme.Account.AppBarLayout"
|
||||||
app:popupTheme="@style/AppTheme.Account.ToolbarPopupTheme.Dark"
|
app:popupTheme="?attr/toolbar_popup_theme"
|
||||||
android:background="@color/semi_transparent" />
|
android:background="@color/toolbar_view_media" />
|
||||||
</android.support.design.widget.CoordinatorLayout>
|
</android.support.design.widget.CoordinatorLayout>
|
|
@ -6,6 +6,11 @@
|
||||||
android:icon="@drawable/ic_file_download_black_24dp"
|
android:icon="@drawable/ic_file_download_black_24dp"
|
||||||
android:title="@string/dialog_download_image"
|
android:title="@string/dialog_download_image"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="ifRoom" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_share_media"
|
||||||
|
android:icon="@drawable/ic_menu_share_24dp"
|
||||||
|
android:title="@string/action_share"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_open_status"
|
android:id="@+id/action_open_status"
|
||||||
android:title="@string/action_open_toot"
|
android:title="@string/action_open_toot"
|
||||||
|
|
|
@ -114,6 +114,7 @@
|
||||||
|
|
||||||
<string name="send_status_link_to">Share toot URL to…</string>
|
<string name="send_status_link_to">Share toot URL to…</string>
|
||||||
<string name="send_status_content_to">Share toot to…</string>
|
<string name="send_status_content_to">Share toot to…</string>
|
||||||
|
<string name="send_media_to">Share media to…</string>
|
||||||
|
|
||||||
<string name="confirmation_reported">Sent!</string>
|
<string name="confirmation_reported">Sent!</string>
|
||||||
<string name="confirmation_unblocked">User unblocked</string>
|
<string name="confirmation_unblocked">User unblocked</string>
|
||||||
|
|
Loading…
Add table
Reference in a new issue