/*
* Copyright (C) 2016 Jorge Ruesga
*
* 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.
*/
package com.ruesga.android.wallpapers.photophase;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.location.Address;
import android.location.Geocoder;
import android.net.Uri;
import android.nfc.NfcAdapter;
import android.nfc.NfcEvent;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.media.ExifInterface;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.transition.Transition;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.widget.ImageView;
import android.widget.TextView;
import com.ruesga.android.wallpapers.photophase.cast.CastService;
import com.ruesga.android.wallpapers.photophase.preferences.PreferencesProvider;
import com.ruesga.android.wallpapers.photophase.tasks.AsyncPictureLoaderTask;
import com.ruesga.android.wallpapers.photophase.utils.BitmapUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import uk.co.senab.photoview.PhotoViewAttacher;
public class PhotoViewerActivity extends AppCompatActivity {
private static final String TAG = "PhotoViewerActivity";
public static final String EXTRA_PHOTO = "photo";
private File mPhoto;
private PhotoViewAttacher mPhotoViewAttacher;
private ImageView mPhotoView;
private View mDetails;
private MenuItem mDetailsMenu;
private MenuItem mShareMenu;
private MenuItem mCastMenu;
private Toolbar mToolbar;
private AsyncPictureLoaderTask mTask;
private boolean mHasTransition;
private boolean mInDetails;
private final float[] mLocation = new float[2];
boolean mHasLocation = false;
// To avoid passing a bitmap in an extra
private Bitmap mThumbnail;
private boolean mPictureLoaded;
private ICastService mCastService;
private NfcAdapter mNfcAdapter;
private final ServiceConnection mCastConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder binder) {
mCastService = ICastService.Stub.asInterface(binder);
boolean hasDevices = hasNearDevices();
if (!hasDevices) {
try {
mCastService.requestScan();
} catch (RemoteException ex) {
// Ignore
}
}
if (mCastMenu != null && mPictureLoaded) {
mCastMenu.setVisible(hasDevices);
}
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
mCastService = null;
if (mCastMenu != null) {
mCastMenu.setVisible(false);
}
}
};
private final BroadcastReceiver mCastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (mCastMenu != null && mPictureLoaded) {
mCastMenu.setVisible(hasNearDevices());
}
}
};
private final AsyncTask<Float, Void, Bitmap> mMapLoaderTask = new AsyncTask<Float, Void, Bitmap>() {
private static final String OPEN_STREETMAP_URL =
"http://staticmap.openstreetmap.de/staticmap.php/";
private static final String IMAGE_URL = OPEN_STREETMAP_URL +
"?center=%f,%f&zoom=14&size=800x600&maptype=mapnik&markers=%f,%f,red-pushpin";
@Override
@SuppressWarnings("ConstantConditions")
protected void onPreExecute() {
findViewById(R.id.details_map_progress).setVisibility(View.VISIBLE);
}
@Override
protected Bitmap doInBackground(Float... params) {
// Check if we have connectivity
if (!AndroidHelper.isNetworkAvailable(PhotoViewerActivity.this)) {
Log.w(TAG, "No network available. Cannot download map");
return null;
}
try {
// Obtain the map image
String url = String.format(
Locale.US, IMAGE_URL, params[0], params[1], params[0], params[1]);
Log.d(TAG, "Obtain map image from " + url);
Request request = new Request.Builder().url(url).build();
OkHttpClient okClient = new OkHttpClient.Builder().build();
Response response = okClient.newCall(request).execute();
// Extract the bitmap
InputStream is = response.body().byteStream();
try {
return BitmapUtils.decodeBitmap(is);
} finally {
try {
is.close();
} catch (IOException ex) {
// Ignore
}
}
} catch (IOException ex) {
Log.w(TAG, "Failed to download map", ex);
}
return null;
}
@Override
@SuppressWarnings("ConstantConditions")
protected void onPostExecute(Bitmap bitmap) {
if (bitmap == null) {
findViewById(R.id.details_map_progress).setVisibility(View.GONE);
findViewById(R.id.details_map_no_data).setVisibility(View.VISIBLE);
} else {
findViewById(R.id.details_map_progress).setVisibility(View.GONE);
ImageView iv = (ImageView) findViewById(R.id.details_map);
iv.setImageBitmap(bitmap);
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.photo_viewer);
if (savedInstanceState != null) {
mHasTransition = savedInstanceState.getBoolean("has_transition", true);
}
if (getIntent() != null) {
String photo = getIntent().getStringExtra(EXTRA_PHOTO);
if (photo == null) {
finishActivity();
return;
}
mPhoto = new File(photo);
if (!mPhoto.exists()) {
finishActivity();
return;
}
}
initToolbar();
AndroidHelper.setupRecentBar(this);
// Initialize the nfc adapter if available
if (AndroidHelper.isJellyBeanMr1OrGreater()
&& getPackageManager().hasSystemFeature(PackageManager.FEATURE_NFC)) {
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
}
mDetails = findViewById(R.id.photo_details);
mPhotoView = (ImageView) findViewById(R.id.photo);
if (mPhotoView != null) {
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
mThumbnail = BitmapUtils.createUnscaledBitmap(
mPhoto, metrics.widthPixels / 8, metrics.heightPixels / 8);
mPhotoView.setImageBitmap(mThumbnail);
mPhotoViewAttacher = new PhotoViewAttacher(mPhotoView);
mPhotoViewAttacher.setOnViewTapListener(new PhotoViewAttacher.OnViewTapListener() {
@Override
public void onViewTap(View view, float v, float v1) {
if (mToolbar != null) {
boolean hide = mToolbar.getAlpha() > 0.0f;
mToolbar.animate()
.translationY(hide ? mToolbar.getHeight() * -1 : 0)
.alpha(hide ? 0.0f : 1.0f)
.setDuration(350L)
.setInterpolator(new AccelerateInterpolator())
.start();
}
}
});
}
if (AndroidHelper.isLollipopOrGreater() && !mHasTransition) {
addTransitionListener();
}
if (PreferencesProvider.Preferences.Cast.isEnabled(this)) {
try {
Intent i = new Intent(this, CastService.class);
bindService(i, mCastConnection, Context.BIND_AUTO_CREATE);
} catch (SecurityException se) {
Log.w(TAG, "Can't bound to CastService", se);
}
}
IntentFilter filter = new IntentFilter();
filter.addAction(CastService.ACTION_SCAN_FINISHED);
LocalBroadcastManager.getInstance(this).registerReceiver(mCastReceiver, filter);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mPictureLoaded) {
Drawable drawable = mPhotoView.getDrawable();
if (drawable instanceof BitmapDrawable) {
BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
Bitmap bitmap = bitmapDrawable.getBitmap();
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
}
}
}
if (mThumbnail != null) {
mThumbnail.recycle();
mThumbnail = null;
}
if (mCastService != null) {
unbindService(mCastConnection);
}
LocalBroadcastManager.getInstance(this).unregisterReceiver(mCastReceiver);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.photoviewer, menu);
mDetailsMenu = menu.findItem(R.id.mnu_details);
mShareMenu = menu.findItem(R.id.mnu_share);
mCastMenu = menu.findItem(R.id.mnu_cast);
return true;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void addTransitionListener() {
getWindow().getSharedElementEnterTransition().addListener(new Transition.TransitionListener() {
@Override
public void onTransitionStart(Transition transition) {
performAsyncPhotoLoading();
getWindow().getSharedElementEnterTransition().removeListener(this);
}
@Override
public void onTransitionEnd(Transition transition) {
}
@Override
public void onTransitionCancel(Transition transition) {
}
@Override
public void onTransitionPause(Transition transition) {
}
@Override
public void onTransitionResume(Transition transition) {
}
});
}
private void performAsyncPhotoLoading() {
if (mTask == null) {
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
mTask = new AsyncPictureLoaderTask(this, mPhotoView, metrics.widthPixels,
metrics.heightPixels, new AsyncPictureLoaderTask.OnPictureLoaded() {
@Override
public void onPreloadImage() {
try {
Thread.sleep(600L);
} catch (InterruptedException e) {
// Ignore
}
}
@Override
public void onPictureLoaded(Object o, Drawable drawable) {
if (mPhotoView != null) {
mPhotoViewAttacher.update();
}
// Update the details information
updateDetailsInformation();
if (mDetailsMenu != null) {
mDetailsMenu.setVisible(true);
}
if (mShareMenu != null) {
mShareMenu.setVisible(true);
}
if (mCastMenu != null) {
mCastMenu.setVisible(hasNearDevices());
}
mPictureLoaded = true;
// publish the photo via nfc if available
shareViaNfc();
}
});
mTask.execute(new File(mPhoto.getAbsolutePath()));
}
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
// Transition is not supported
if (!AndroidHelper.isLollipopOrGreater() || !mHasTransition) {
performAsyncPhotoLoading();
}
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mTask != null && (mTask.getStatus() == AsyncTask.Status.RUNNING ||
mTask.getStatus() == AsyncTask.Status.PENDING)) {
mTask.cancel(true);
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("has_transition", false);
}
private void initToolbar() {
// Add a toolbar
mToolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(mToolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setTitle(" ");
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem menuItem) {
switch (menuItem.getItemId()) {
case android.R.id.home:
if (mInDetails) {
hideDetails();
} else {
finishActivity();
}
return true;
case R.id.mnu_share:
AndroidHelper.sharePicture(this, Uri.fromFile(mPhoto));
return true;
case R.id.mnu_cast:
try {
mCastService.cast(mPhoto.toString());
} catch (RemoteException e) {
Log.w(TAG, "Got a remote exception while casting " + mPhoto, e);
}
return true;
case R.id.mnu_details:
displayDetails();
return true;
default:
return super.onOptionsItemSelected(menuItem);
}
}
@Override
public void onBackPressed() {
if (mInDetails) {
hideDetails();
} else {
super.onBackPressed();
finishActivity();
}
}
private void finishActivity() {
if (mPhotoViewAttacher != null) {
mPhotoViewAttacher.cleanup();
}
supportFinishAfterTransition();
}
@SuppressWarnings("ConstantConditions")
private void displayDetails() {
final AnimatorListener listener = new AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
mDetails.setAlpha(0f);
mDetails.setVisibility(View.VISIBLE);
mDetailsMenu.setVisible(false);
mShareMenu.setVisible(false);
mCastMenu.setVisible(false);
if (getSupportActionBar() != null) {
getSupportActionBar().setTitle(getString(R.string.mnu_details));
}
}
@Override
public void onAnimationEnd(Animator animation) {
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
};
mDetails.animate().alpha(1).setListener(listener).setDuration(650L).start();
mInDetails = true;
// Request the image of the map
if (mHasLocation) {
ImageView iv = (ImageView) findViewById(R.id.details_map);
if (iv.getDrawable() == null && mMapLoaderTask.getStatus() != AsyncTask.Status.RUNNING) {
mMapLoaderTask.execute(mLocation[0], mLocation[1]);
}
}
}
private void hideDetails() {
final AnimatorListener listener = new AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
mDetails.setAlpha(0f);
mDetails.setVisibility(View.GONE);
mDetailsMenu.setVisible(true);
mShareMenu.setVisible(true);
mCastMenu.setVisible(hasNearDevices());
if (getSupportActionBar() != null) {
getSupportActionBar().setTitle(" ");
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
};
mDetails.animate().alpha(0).setListener(listener).setDuration(250L).start();
mInDetails = false;
}
@SuppressWarnings("ConstantConditions")
private void updateDetailsInformation() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.US);
DateFormat df = DateFormat.getDateInstance(DateFormat.FULL);
DateFormat tf = DateFormat.getTimeInstance(DateFormat.SHORT);
DecimalFormat nf1 = new DecimalFormat("0");
DecimalFormat nf2 = new DecimalFormat("0.#");
String notAvailable = getString(R.string.photoviewer_details_not_available);
String title = mPhoto.getName();
Date datetime = null;
String location = null;
String manufacturer = null;
String model = null;
double exposure = -1d;
double aperture = -1d;
String iso = null;
int flash = -1;
int w = -1;
int h = -1;
int orientation = -1;
ExifInterface exif;
try {
exif = new ExifInterface(mPhoto.getAbsolutePath());
// Title
if (AndroidHelper.isNougatOrGreater()) {
title = exif.getAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION);
if (TextUtils.isEmpty(title)) {
title = exif.getAttribute(ExifInterface.TAG_USER_COMMENT);
if (TextUtils.isEmpty(title)) {
title = mPhoto.getName();
}
}
}
// Date
String date = exif.getAttribute(ExifInterface.TAG_DATETIME);
if (date != null) {
try {
datetime = sdf.parse(date);
} catch (ParseException e) {
// Ignore
}
}
// Location
if (exif.getLatLong(mLocation)) {
mHasLocation = true;
if (Geocoder.isPresent()) {
Geocoder geocoder = new Geocoder(this, AndroidHelper.getLocale(getResources()));
List<Address> addresses = geocoder.getFromLocation(mLocation[0], mLocation[1], 1);
if (!addresses.isEmpty()) {
Address address = addresses.get(0);
int max = address.getMaxAddressLineIndex();
location = "";
for (int i = 0; i <= max; i++) {
if (i > 0) {
location += ", ";
}
location += address.getAddressLine(i);
}
}
}
}
// Camera
manufacturer = exif.getAttribute(ExifInterface.TAG_MAKE);
model = exif.getAttribute(ExifInterface.TAG_MODEL);
try {
exposure = Double.parseDouble(exif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME));
if (exposure > 0) {
exposure = Math.floor(1 / exposure);
}
} catch (NullPointerException | NumberFormatException ex) {
// Ignore
}
try {
if (AndroidHelper.isNougatOrGreater()) {
aperture = Double.parseDouble(exif.getAttribute(ExifInterface.TAG_F_NUMBER));
} else {
//noinspection deprecation
aperture = Double.parseDouble(exif.getAttribute(ExifInterface.TAG_F_NUMBER));
}
} catch (NullPointerException | NumberFormatException ex) {
// Ignore
}
if (AndroidHelper.isNougatOrGreater()) {
iso = exif.getAttribute(ExifInterface.TAG_ISO_SPEED_RATINGS);
} else {
//noinspection deprecation
iso = exif.getAttribute(ExifInterface.TAG_ISO_SPEED_RATINGS);
}
flash = exif.getAttributeInt(ExifInterface.TAG_FLASH, -1);
// Resolution
w = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, -1);
h = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, -1);
// Orientation
if (orientation == ExifInterface.ORIENTATION_NORMAL) {
orientation = 0;
} else if (orientation == ExifInterface.ORIENTATION_ROTATE_90) {
orientation = 90;
} else if (orientation == ExifInterface.ORIENTATION_ROTATE_180) {
orientation = 180;
} else if (orientation == ExifInterface.ORIENTATION_ROTATE_270) {
orientation = 270;
}
} catch (IOException ioEx) {
Log.w(TAG, "Not exif information for "+ mPhoto.getAbsolutePath(), ioEx);
}
// Ensure we can have information about the picture size
if (w <= 0 || h <= 0) {
Rect r = BitmapUtils.getBitmapDimensions(mPhoto);
w = r.width();
h = r.height();
}
TextView tv;
// Date
tv = (TextView) findViewById(R.id.details_datetime);
tv.setText(datetime == null
? notAvailable
: getString(R.string.photoviewer_details_format,
df.format(datetime), tf.format(datetime)));
// Location
if (!mHasLocation) {
findViewById(R.id.details_lat_lon).setVisibility(View.GONE);
findViewById(R.id.details_map_block).setVisibility(View.GONE);
tv = (TextView) findViewById(R.id.details_location);
tv.setText(notAvailable);
} else {
tv = (TextView) findViewById(R.id.details_location);
if (location == null) {
tv.setVisibility(View.GONE);
} else {
tv.setText(location);
}
tv = (TextView) findViewById(R.id.details_lat_lon);
tv.setText(getString(R.string.photoviewer_details_latitude_longitude,
mLocation[0], mLocation[1]));
// Check if map can be open in an external app
if (hasExternalMapApp(mLocation[0], mLocation[1])) {
View v = findViewById(R.id.details_location_block);
v.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
openLocationInExternalApp(mLocation[0], mLocation[1]);
}
});
}
}
// Camera
if (manufacturer == null && model == null ) {
findViewById(R.id.details_camera).setVisibility(View.GONE);
} else {
tv = (TextView) findViewById(R.id.details_manufacturer);
tv.setText(manufacturer == null
? getString(R.string.photoviewer_details_manufacturer, notAvailable)
: getString(R.string.photoviewer_details_manufacturer, manufacturer));
tv = (TextView) findViewById(R.id.details_model);
tv.setText(model == null
? getString(R.string.photoviewer_details_model, notAvailable)
: getString(R.string.photoviewer_details_model, model));
tv = (TextView) findViewById(R.id.details_exposure);
tv.setText(exposure == -1
? getString(R.string.photoviewer_details_exposure, notAvailable)
: getString(R.string.photoviewer_details_exposure, nf1.format(exposure)));
tv = (TextView) findViewById(R.id.details_aperture);
tv.setText(aperture == -1
? getString(R.string.photoviewer_details_aperture, notAvailable)
: getString(R.string.photoviewer_details_aperture, nf2.format(aperture)));
tv = (TextView) findViewById(R.id.details_iso);
tv.setText(iso == null
? getString(R.string.photoviewer_details_iso, notAvailable)
: getString(R.string.photoviewer_details_iso, iso));
tv = (TextView) findViewById(R.id.details_flash);
tv.setText(flash == -1
? getString(R.string.photoviewer_details_flash, notAvailable)
: getString(R.string.photoviewer_details_flash, ((flash & 0x01) == 0x1)
? getString(R.string.photoviewer_details_flash_used)
: getString(R.string.photoviewer_details_flash_not_used)));
}
// Info
tv = (TextView) findViewById(R.id.details_name);
tv.setText(getString(R.string.photoviewer_details_name, title));
tv = (TextView) findViewById(R.id.details_size);
tv.setText(getString(R.string.photoviewer_details_size, mPhoto.length() / 1024));
tv = (TextView) findViewById(R.id.details_resolution);
if (w == -1 || h == -1) {
tv.setVisibility(View.GONE);
} else {
tv.setText(getString(R.string.photoviewer_details_resolution, w, h));
}
tv = (TextView) findViewById(R.id.details_orientation);
if (orientation == -1) {
tv.setVisibility(View.GONE);
} else {
tv.setText(getString(R.string.photoviewer_details_orientation, orientation));
}
tv = (TextView) findViewById(R.id.details_path);
tv.setText(getString(R.string.photoviewer_details_path, mPhoto.getParent()));
}
private boolean hasExternalMapApp(float latitude, float longitude) {
String uri = String.format(Locale.ENGLISH, "geo:%f,%f", latitude, longitude);
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
PackageManager manager = getPackageManager();
return manager.queryIntentActivities(intent, 0).size() > 0;
}
private void openLocationInExternalApp(float latitude, float longitude) {
String uri = String.format(Locale.ENGLISH, "geo:%f,%f", latitude, longitude);
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
startActivity(intent);
}
private boolean hasNearDevices() {
if (mCastService != null) {
try {
return mCastService.hasNearDevices();
} catch (RemoteException ex) {
// Ignore
}
}
return false;
}
@TargetApi(value=Build.VERSION_CODES.JELLY_BEAN_MR1)
private void shareViaNfc() {
if (mNfcAdapter != null) {
mNfcAdapter.setBeamPushUrisCallback(new NfcAdapter.CreateBeamUrisCallback() {
@Override
public Uri[] createBeamUris(NfcEvent nfcEvent) {
return new Uri[]{Uri.fromFile(mPhoto)};
}
}, this);
}
}
}