/* * 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); } }