content: /*
   * Copyright 2019 Alynx Zhou
   *
   * 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 xyz.alynx.livewallpaper;
  
  import android.app.ActivityManager;
  import android.content.ContentResolver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.SharedPreferences;
  import android.content.pm.ConfigurationInfo;
  import android.content.res.AssetFileDescriptor;
  import android.media.MediaMetadataRetriever;
  import android.opengl.GLSurfaceView;
  import android.os.ParcelFileDescriptor;
  import android.service.wallpaper.WallpaperService;
  import android.support.annotation.NonNull;
  import android.view.SurfaceHolder;
  import android.widget.Toast;
  
  import com.google.android.exoplayer2.C;
  import com.google.android.exoplayer2.ExoPlayerFactory;
  import com.google.android.exoplayer2.Player;
  import com.google.android.exoplayer2.SimpleExoPlayer;
  import com.google.android.exoplayer2.source.ExtractorMediaSource;
  import com.google.android.exoplayer2.source.MediaSource;
  import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
  import com.google.android.exoplayer2.upstream.DataSource;
  import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
  import com.google.android.exoplayer2.util.Util;
  
  import java.io.FileNotFoundException;
  import java.io.IOException;
  import java.util.List;
  
  /**
   *
   * Generally, WallpaperService should not depend other parts of app.
   * Because sometimes (like phone restarted) service starts before Activity and Application.
   * So we cannot get data from them.
   *
   * To solve this, it's better to store WallpaperCard
   * into storage, SharedPreferences is better than JSON because it's easier to get data.
   *
   * So when we cannot get current WallpaperCard from LWApplication, we read SharedPreference,
   * then build a temp WallpaperCard (in fact only type and path matter for service, so we can
   * set thumbnail to null).
   *
   * And when we get a current WallpaperCard, we save it to SharedPreference for further loading.
   *
   */
  public class GLWallpaperService extends WallpaperService {
      @SuppressWarnings("unused")
      private static final String TAG = "GLWallpaperService";
  
      class GLWallpaperEngine extends Engine {
          private static final String TAG = "GLWallpaperEngine";
          private final Context context;
          private GLWallpaperSurfaceView glSurfaceView = null;
          private SimpleExoPlayer exoPlayer = null;
          private MediaSource videoSource = null;
          private DefaultTrackSelector trackSelector = null;
          private WallpaperCard wallpaperCard = null;
          private WallpaperCard oldWallpaperCard = null;
          private GLWallpaperRenderer renderer = null;
          private boolean allowSlide = false;
          private int videoRotation = 0;
          private int videoWidth = 0;
          private int videoHeight = 0;
          private long progress = 0;
  
          private class GLWallpaperSurfaceView extends GLSurfaceView {
              @SuppressWarnings("unused")
              private static final String TAG = "GLWallpaperSurface";
  
              public GLWallpaperSurfaceView(Context context) {
                  super(context);
              }
  
              /**
               * This is a hack. Because Android Live Wallpaper only has a Surface.
               * So we create a GLSurfaceView, and when drawing to its Surface,
               * we replace it with WallpaperEngine's Surface.
               */
              @Override
              public SurfaceHolder getHolder() {
                  return getSurfaceHolder();
              }
  
              void onDestroy() {
                  super.onDetachedFromWindow();
              }
          }
  
          GLWallpaperEngine(@NonNull final Context context) {
              this.context = context;
              setTouchEventsEnabled(false);
          }
  
          // @Override
          // public void onTouchEvent(MotionEvent event) {
          //     super.onTouchEvent(event);
          // }
  
          @Override
          public void onCreate(SurfaceHolder surfaceHolder) {
              super.onCreate(surfaceHolder);
              final SharedPreferences pref = getSharedPreferences(
                  LWApplication.OPTIONS_PREF, MODE_PRIVATE
              );
              allowSlide = pref.getBoolean(LWApplication.SLIDE_WALLPAPER_KEY, false);
          }
  
          @Override
          public void onSurfaceCreated(SurfaceHolder surfaceHolder) {
              super.onSurfaceCreated(surfaceHolder);
              createGLSurfaceView();
              int width = surfaceHolder.getSurfaceFrame().width();
              int height = surfaceHolder.getSurfaceFrame().height();
              renderer.setScreenSize(width, height);
              startPlayer();
          }
  
          @Override
          public void onVisibilityChanged(boolean visible) {
              super.onVisibilityChanged(visible);
              if (renderer != null) {
                  if (visible) {
                      final SharedPreferences pref = getSharedPreferences(
                          LWApplication.OPTIONS_PREF, MODE_PRIVATE
                      );
                      allowSlide = pref.getBoolean(LWApplication.SLIDE_WALLPAPER_KEY, false);
                      glSurfaceView.onResume();
                      startPlayer();
                  } else {
                      stopPlayer();
                      glSurfaceView.onPause();
                      // Prevent useless renderer calculating.
                      allowSlide = false;
                  }
              }
          }
  
          @Override
          public void onOffsetsChanged(
              float xOffset, float yOffset,
              float xOffsetStep, float yOffsetStep,
              int xPixelOffset, int yPixelOffset
          ) {
              super.onOffsetsChanged(
                  xOffset, yOffset, xOffsetStep,
                  yOffsetStep, xPixelOffset, yPixelOffset
              );
              if (allowSlide && !isPreview()) {
                  renderer.setOffset(0.5f - xOffset, 0.5f - yOffset);
              }
          }
  
          @Override
          public void onSurfaceChanged(
              SurfaceHolder surfaceHolder, int format,
              int width, int height
          ) {
              super.onSurfaceChanged(surfaceHolder, format, width, height);
              renderer.setScreenSize(width, height);
          }
  
          @Override
          public void onSurfaceDestroyed(SurfaceHolder holder) {
              super.onSurfaceDestroyed(holder);
              stopPlayer();
              glSurfaceView.onDestroy();
          }
  
          private void createGLSurfaceView() {
              if (glSurfaceView != null) {
                  glSurfaceView.onDestroy();
                  glSurfaceView = null;
              }
              glSurfaceView = new GLWallpaperSurfaceView(context);
              final ActivityManager activityManager = (ActivityManager)getSystemService(
                  Context.ACTIVITY_SERVICE
              );
              if (activityManager == null) {
                  throw new RuntimeException("Cannot get ActivityManager");
              }
              final ConfigurationInfo configInfo = activityManager.getDeviceConfigurationInfo();
              if (configInfo.reqGlEsVersion >= 0x30000) {
                  Utils.debug(TAG, "Support GLESv3");
                  glSurfaceView.setEGLContextClientVersion(3);
                  renderer = new GLES30WallpaperRenderer(context);
              } else if (configInfo.reqGlEsVersion >= 0x20000) {
                  Utils.debug(TAG, "Fallback to GLESv2");
                  glSurfaceView.setEGLContextClientVersion(2);
                  renderer = new GLES20WallpaperRenderer(context);
              } else {
                  Toast.makeText(context, R.string.gles_version, Toast.LENGTH_LONG).show();
                  throw new RuntimeException("Needs GLESv2 or higher");
              }
              glSurfaceView.setPreserveEGLContextOnPause(true);
              glSurfaceView.setRenderer(renderer);
              // On demand render will lead to black screen.
              glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
          }
  
          private boolean checkWallpaperCardValid() {
              if (wallpaperCard == null) {
                  return false;
              }
              if (wallpaperCard.getType() == WallpaperCard.Type.INTERNAL) {
                  return true;
              }
              boolean res = true;
              // Ask persistable permission here because AddCardTask may not have context.
              getContentResolver().takePersistableUriPermission(
                  wallpaperCard.getUri(), Intent.FLAG_GRANT_READ_URI_PERMISSION
              );
              try {
                  final ContentResolver resolver = getContentResolver();
                  final ParcelFileDescriptor pfd = resolver.openFileDescriptor(
                      wallpaperCard.getUri(), "r"
                  );
                  if (pfd == null) {
                      res = false;
                  } else {
                      pfd.close();
                  }
              } catch (FileNotFoundException e) {
                  e.printStackTrace();
                  res = false;
              } catch (IOException e) {
                  e.printStackTrace();
                  res = false;
              }
              return res;
          }
  
          private void loadWallpaperCard() {
              oldWallpaperCard = wallpaperCard;
              if (isPreview()) {
                  wallpaperCard = LWApplication.getPreviewWallpaperCard();
              } else {
                  wallpaperCard = LWApplication.getCurrentWallpaperCard(context);
              }
              if (!checkWallpaperCardValid()) {
                  if (wallpaperCard != null) {
                      // File is removed by user.
                      Toast.makeText(context, R.string.invalid_path, Toast.LENGTH_LONG).show();
                      wallpaperCard.setInvalid();
                  }
                  // Load default wallpaper.
                  final List<WallpaperCard> cards = LWApplication.getCards(context);
                  if (cards.size() > 0 && cards.get(0) != null) {
                      wallpaperCard = cards.get(0);
                  } else {
                      wallpaperCard = null;
                      Toast.makeText(context, R.string.default_failed, Toast.LENGTH_LONG).show();
                      throw new RuntimeException("Failed to fallback to internal wallpaper");
                  }
              }
          }
  
          private void getVideoMetadata() throws IOException {
              final MediaMetadataRetriever mmr = new MediaMetadataRetriever();
              switch (wallpaperCard.getType()) {
              case INTERNAL:
                  final AssetFileDescriptor afd = getAssets().openFd(wallpaperCard.getPath());
                  mmr.setDataSource(
                      afd.getFileDescriptor(),
                      afd.getStartOffset(),
                      afd.getDeclaredLength()
                  );
                  afd.close();
                  break;
              case EXTERNAL:
                  mmr.setDataSource(context, wallpaperCard.getUri());
                  break;
              }
              final String rotation = mmr.extractMetadata(
                  MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION
              );
              final String width = mmr.extractMetadata(
                  MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH
              );
              final String height = mmr.extractMetadata(
                  MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT
              );
              mmr.release();
              videoRotation = Integer.parseInt(rotation);
              videoWidth = Integer.parseInt(width);
              videoHeight = Integer.parseInt(height);
          }
  
          private void startPlayer() {
              if (exoPlayer != null) {
                  stopPlayer();
              }
              Utils.debug(TAG, "Player starting");
              loadWallpaperCard();
              if (wallpaperCard == null) {
                  // gg
                  return;
              }
              try {
                  getVideoMetadata();
              } catch (IOException e) {
                  e.printStackTrace();
                  // gg
                  return;
              }
              trackSelector = new DefaultTrackSelector();
              exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector);
              exoPlayer.setVolume(0.0f);
              // Disable audio decoder.
              final int count = exoPlayer.getRendererCount();
              for (int i = 0; i < count; ++i) {
                  if (exoPlayer.getRendererType(i) == C.TRACK_TYPE_AUDIO) {
                      trackSelector.setParameters(
                          trackSelector.buildUponParameters().setRendererDisabled(i, true)
                      );
                  }
              }
              exoPlayer.setRepeatMode(Player.REPEAT_MODE_ALL);
              final DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(
                  context, Util.getUserAgent(context, "xyz.alynx.livewallpaper")
              );
              // ExoPlayer can load file:///android_asset/ uri correctly.
              videoSource = new ExtractorMediaSource.Factory(
                  dataSourceFactory
              ).createMediaSource(wallpaperCard.getUri());
              // Let we assume video has correct info in metadata, or user should fix it.
              renderer.setVideoSizeAndRotation(videoWidth, videoHeight, videoRotation);
              // This must be set after getting video info.
              renderer.setSourcePlayer(exoPlayer);
              exoPlayer.prepare(videoSource);
              // ExoPlayer's video size changed listener is buggy. Don't use it.
              // It give's width and height after rotation, but did not rotate frames.
              if (oldWallpaperCard != null &&
                  oldWallpaperCard.equals(wallpaperCard)) {
                  exoPlayer.seekTo(progress);
              }
              exoPlayer.setPlayWhenReady(true);
          }
  
          private void stopPlayer() {
              if (exoPlayer != null) {
                  if (exoPlayer.getPlayWhenReady()) {
                      Utils.debug(TAG, "Player stopping");
                      exoPlayer.setPlayWhenReady(false);
                      progress = exoPlayer.getCurrentPosition();
                      exoPlayer.stop();
                  }
                  exoPlayer.release();
                  exoPlayer = null;
              }
              videoSource = null;
              trackSelector = null;
          }
      }
  
      @Override
      public Engine onCreateEngine() {
          return new GLWallpaperEngine(this);
      }
  }
  