From f99eb2c1d9baf9688a82bfdb63466c0e0e58fad6 Mon Sep 17 00:00:00 2001 From: Christian S Date: Thu, 5 May 2016 10:44:56 +0200 Subject: added audio player --- libs/audiowife/.gitignore | 5 + libs/audiowife/build.gradle | 27 + libs/audiowife/src/main/AndroidManifest.xml | 9 + .../main/java/nl/changer/audiowife/AudioWife.java | 699 +++++++++++++++++++++ .../src/main/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 5237 bytes .../src/main/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 14383 bytes .../src/main/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 19388 bytes .../main/res/drawable/gray_border_wo_padding.xml | 15 + libs/audiowife/src/main/res/layout/aw_player.xml | 49 ++ libs/audiowife/src/main/res/values/strings.xml | 5 + libs/audiowife/src/main/res/values/styles.xml | 20 + 11 files changed, 829 insertions(+) create mode 100644 libs/audiowife/.gitignore create mode 100644 libs/audiowife/build.gradle create mode 100644 libs/audiowife/src/main/AndroidManifest.xml create mode 100644 libs/audiowife/src/main/java/nl/changer/audiowife/AudioWife.java create mode 100644 libs/audiowife/src/main/res/drawable-mdpi/ic_launcher.png create mode 100644 libs/audiowife/src/main/res/drawable-xhdpi/ic_launcher.png create mode 100644 libs/audiowife/src/main/res/drawable-xxhdpi/ic_launcher.png create mode 100644 libs/audiowife/src/main/res/drawable/gray_border_wo_padding.xml create mode 100644 libs/audiowife/src/main/res/layout/aw_player.xml create mode 100644 libs/audiowife/src/main/res/values/strings.xml create mode 100644 libs/audiowife/src/main/res/values/styles.xml (limited to 'libs') diff --git a/libs/audiowife/.gitignore b/libs/audiowife/.gitignore new file mode 100644 index 000000000..e99645287 --- /dev/null +++ b/libs/audiowife/.gitignore @@ -0,0 +1,5 @@ +.gradle +.idea +build +*.iml +*.properties \ No newline at end of file diff --git a/libs/audiowife/build.gradle b/libs/audiowife/build.gradle new file mode 100644 index 000000000..63c2e8e78 --- /dev/null +++ b/libs/audiowife/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.3" + + defaultConfig { + minSdkVersion 14 + targetSdkVersion 21 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + + } + } + + // It would be better to fix the issues + lintOptions { + abortOnError false + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) +} diff --git a/libs/audiowife/src/main/AndroidManifest.xml b/libs/audiowife/src/main/AndroidManifest.xml new file mode 100644 index 000000000..dad095276 --- /dev/null +++ b/libs/audiowife/src/main/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/libs/audiowife/src/main/java/nl/changer/audiowife/AudioWife.java b/libs/audiowife/src/main/java/nl/changer/audiowife/AudioWife.java new file mode 100644 index 000000000..ade93475a --- /dev/null +++ b/libs/audiowife/src/main/java/nl/changer/audiowife/AudioWife.java @@ -0,0 +1,699 @@ +/*** + * The MIT License (MIT) + *

+ * Copyright (c) 2014 Jaydeep + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package nl.changer.audiowife; + +import android.content.Context; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.MediaPlayer.OnCompletionListener; +import android.net.Uri; +import android.os.Handler; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.SeekBar; +import android.widget.TextView; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; + +/*** + * A simple audio player wrapper for Android + ***/ +public class AudioWife { + + private static final String TAG = AudioWife.class.getSimpleName(); + /**** + * Playback progress update time in milliseconds + ****/ + private static final int AUDIO_PROGRESS_UPDATE_TIME = 100; + // TODO: externalize the error messages. + private static final String ERROR_PLAYVIEW_NULL = "Play view cannot be null"; + private static final String ERROR_PLAYTIME_CURRENT_NEGATIVE = "Current playback time cannot be negative"; + private static final String ERROR_PLAYTIME_TOTAL_NEGATIVE = "Total playback time cannot be negative"; + /*** + * Keep a single copy of this in memory unless required to create a new instance explicitly. + ****/ + private static AudioWife mAudioWife; + /*** + * Audio URI + ****/ + private static Uri mUri; + private Handler mProgressUpdateHandler; + private MediaPlayer mMediaPlayer; + private SeekBar mSeekBar; + @Deprecated + /*** + * Set both current playack time and total runtime + * of the audio in the UI. + */ + private TextView mPlaybackTime; + private View mPlayButton; + private View mPauseButton; + /*** + * Indicates the current run-time of the audio being played + */ + private TextView mRunTime; + /*** + * Indicates the total duration of the audio being played. + */ + private TextView mTotalTime; + /*** + * Set if AudioWife is using the default UI provided with the library. + **/ + private boolean mHasDefaultUi; + /**** + * Array to hold custom completion listeners + ****/ + private ArrayList mCompletionListeners = new ArrayList(); + private ArrayList mPlayListeners = new ArrayList(); + private ArrayList mPauseListeners = new ArrayList(); + private Runnable mUpdateProgress = new Runnable() { + + public void run() { + + if (mSeekBar == null) { + return; + } + + if (mProgressUpdateHandler != null && mMediaPlayer.isPlaying()) { + mSeekBar.setProgress((int) mMediaPlayer.getCurrentPosition()); + int currentTime = mMediaPlayer.getCurrentPosition(); + updatePlaytime(currentTime); + updateRuntime(currentTime); + // repeat the process + mProgressUpdateHandler.postDelayed(this, AUDIO_PROGRESS_UPDATE_TIME); + } else { + // DO NOT update UI if the player is paused + } + } + }; + private OnCompletionListener mOnCompletion = new OnCompletionListener() { + + @Override + public void onCompletion(MediaPlayer mp) { + // set UI when audio finished playing + int currentPlayTime = 0; + mSeekBar.setProgress((int) currentPlayTime); + updatePlaytime(currentPlayTime); + updateRuntime(currentPlayTime); + setPlayable(); + // ensure that our completion listener fires first. + // This will provide the developer to over-ride our + // completion listener functionality + + fireCustomCompletionListeners(mp); + } + }; + private View playerUi; + + public static AudioWife getInstance() { + + if (mAudioWife == null) { + mAudioWife = new AudioWife(); + } + + return mAudioWife; + } + + /*** + * Starts playing audio file associated. Before playing the audio, visibility of appropriate UI + * controls is made visible. Calling this method has no effect if the audio is already being + * played. + ****/ + public void play() { + + // if play button itself is null, the whole purpose of AudioWife is + // defeated. + if (mPlayButton == null) { + throw new IllegalStateException(ERROR_PLAYVIEW_NULL); + } + + if (mUri == null) { + throw new IllegalStateException("Uri cannot be null. Call init() before calling this method"); + } + + if (mMediaPlayer == null) { + throw new IllegalStateException("Call init() before calling this method"); + } + + if (mMediaPlayer.isPlaying()) { + return; + } + + mProgressUpdateHandler.postDelayed(mUpdateProgress, AUDIO_PROGRESS_UPDATE_TIME); + + // enable visibility of all UI controls. + setViewsVisibility(); + + mMediaPlayer.start(); + + setPausable(); + } + + /** + * Ensure the views are visible before playing the audio. + */ + private void setViewsVisibility() { + + if (mSeekBar != null) { + mSeekBar.setVisibility(View.VISIBLE); + } + + if (mPlaybackTime != null) { + mPlaybackTime.setVisibility(View.VISIBLE); + } + + if (mRunTime != null) { + mRunTime.setVisibility(View.VISIBLE); + } + + if (mTotalTime != null) { + mTotalTime.setVisibility(View.VISIBLE); + } + + if (mPlayButton != null) { + mPlayButton.setVisibility(View.VISIBLE); + } + + if (mPauseButton != null) { + mPauseButton.setVisibility(View.VISIBLE); + } + } + + /*** + * Pause the audio being played. Calling this method has no effect if the audio is already + * paused + */ + public void pause() { + + if (mMediaPlayer == null) { + return; + } + + if (mMediaPlayer.isPlaying()) { + mMediaPlayer.pause(); + setPlayable(); + } + } + + @Deprecated + private void updatePlaytime(int currentTime) { + + if (mPlaybackTime == null) { + return; + } + + if (currentTime < 0) { + throw new IllegalArgumentException(ERROR_PLAYTIME_CURRENT_NEGATIVE); + } + + StringBuilder playbackStr = new StringBuilder(); + + // set the current time + // its ok to show 00:00 in the UI + playbackStr.append(String.format("%02d:%02d", TimeUnit.MILLISECONDS.toMinutes((long) currentTime), TimeUnit.MILLISECONDS.toSeconds((long) currentTime) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes((long) currentTime)))); + + playbackStr.append("/"); + + // show total duration. + long totalDuration = 0; + + if (mMediaPlayer != null) { + try { + totalDuration = mMediaPlayer.getDuration(); + } catch (IllegalStateException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + // set total time as the audio is being played + if (totalDuration != 0) { + playbackStr.append(String.format("%02d:%02d", TimeUnit.MILLISECONDS.toMinutes((long) totalDuration), TimeUnit.MILLISECONDS.toSeconds((long) totalDuration) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes((long) totalDuration)))); + } else { + Log.w(TAG, "Something strage this audio track duration in zero"); + } + + mPlaybackTime.setText(playbackStr); + + // DebugLog.i(currentTime + " / " + totalDuration); + } + + private void updateRuntime(int currentTime) { + + if (mRunTime == null) { + // this view can be null if the user + // does not want to use it. Don't throw + // an exception. + return; + } + + if (currentTime < 0) { + throw new IllegalArgumentException(ERROR_PLAYTIME_CURRENT_NEGATIVE); + } + + StringBuilder playbackStr = new StringBuilder(); + + // set the current time + // its ok to show 00:00 in the UI + playbackStr.append(String.format("%02d:%02d", TimeUnit.MILLISECONDS.toMinutes((long) currentTime), TimeUnit.MILLISECONDS.toSeconds((long) currentTime) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes((long) currentTime)))); + + mRunTime.setText(playbackStr); + + // DebugLog.i(currentTime + " / " + totalDuration); + } + + private void setTotalTime() { + + if (mTotalTime == null) { + // this view can be null if the user + // does not want to use it. Don't throw + // an exception. + return; + } + + StringBuilder playbackStr = new StringBuilder(); + long totalDuration = 0; + + // by this point the media player is brought to ready state + // by the call to init(). + if (mMediaPlayer != null) { + try { + totalDuration = mMediaPlayer.getDuration(); + } catch (IllegalStateException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + if (totalDuration < 0) { + throw new IllegalArgumentException(ERROR_PLAYTIME_TOTAL_NEGATIVE); + } + + // set total time as the audio is being played + if (totalDuration != 0) { + playbackStr.append(String.format("%02d:%02d", TimeUnit.MILLISECONDS.toMinutes((long) totalDuration), TimeUnit.MILLISECONDS.toSeconds((long) totalDuration) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes((long) totalDuration)))); + } + + mTotalTime.setText(playbackStr); + } + + /*** + * Changes audiowife state to enable play functionality. + */ + private void setPlayable() { + if (mPlayButton != null) { + mPlayButton.setVisibility(View.VISIBLE); + } + + if (mPauseButton != null) { + mPauseButton.setVisibility(View.GONE); + } + } + + /**** + * Changes audio wife to enable pause functionality. + */ + private void setPausable() { + if (mPlayButton != null) { + mPlayButton.setVisibility(View.GONE); + } + + if (mPauseButton != null) { + mPauseButton.setVisibility(View.VISIBLE); + } + } + + /*** + * Initialize the audio player. This method should be the first one to be called before starting + * to play audio using {@link nl.changer.audiowife.AudioWife} + * + * @param ctx {@link android.app.Activity} Context + * @param uri Uri of the audio to be played. + ****/ + public AudioWife init(Context ctx, Uri uri) { + + if (uri == null) { + throw new IllegalArgumentException("Uri cannot be null"); + } + + if (mAudioWife == null) { + mAudioWife = new AudioWife(); + } + + mUri = uri; + + mProgressUpdateHandler = new Handler(); + + initPlayer(ctx); + + return this; + } + + /*** + * Sets the audio play functionality on click event of this view. You can set {@link android.widget.Button} or + * an {@link android.widget.ImageView} as audio play control + * + * @see nl.changer.audiowife.AudioWife#addOnPauseClickListener(android.view.View.OnClickListener) + ****/ + public AudioWife setPlayView(View play) { + + if (play == null) { + throw new NullPointerException("PlayView cannot be null"); + } + + if (mHasDefaultUi) { + Log.w(TAG, "Already using default UI. Setting play view will have no effect"); + return this; + } + + mPlayButton = play; + + initOnPlayClick(); + return this; + } + + private void initOnPlayClick() { + if (mPlayButton == null) { + throw new NullPointerException(ERROR_PLAYVIEW_NULL); + } + + // add default click listener to the top + // so that it is the one that gets fired first + mPlayListeners.add(0, new View.OnClickListener() { + + @Override + public void onClick(View v) { + play(); + } + }); + + // Fire all the attached listeners + // when the play button is clicked + mPlayButton.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + for (View.OnClickListener listener : mPlayListeners) { + listener.onClick(v); + } + } + }); + } + + /*** + * Sets the audio pause functionality on click event of the view passed in as a parameter. You + * can set {@link android.widget.Button} or an {@link android.widget.ImageView} as audio pause control. Audio pause + * functionality will be unavailable if this method is not called. + * + * @see nl.changer.audiowife.AudioWife#addOnPauseClickListener(android.view.View.OnClickListener) + ****/ + public AudioWife setPauseView(View pause) { + + if (pause == null) { + throw new NullPointerException("PauseView cannot be null"); + } + + if (mHasDefaultUi) { + Log.w(TAG, "Already using default UI. Setting pause view will have no effect"); + return this; + } + + mPauseButton = pause; + + initOnPauseClick(); + return this; + } + + private void initOnPauseClick() { + if (mPauseButton == null) { + throw new NullPointerException("Pause view cannot be null"); + } + + // add default click listener to the top + // so that it is the one that gets fired first + mPauseListeners.add(0, new View.OnClickListener() { + + @Override + public void onClick(View v) { + pause(); + } + }); + + // Fire all the attached listeners + // when the pause button is clicked + mPauseButton.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + for (View.OnClickListener listener : mPauseListeners) { + listener.onClick(v); + } + } + }); + } + + /*** + * @deprecated Use {@link nl.changer.audiowife.AudioWife#setRuntimeView(android.widget.TextView)} and + * {@link nl.changer.audiowife.AudioWife#setTotalTimeView(android.widget.TextView)} instead.
+ * Sets current and total playback time. Use this if you have a playback time + * counter in the UI. + ****/ + public AudioWife setPlaytime(TextView playTime) { + + if (mHasDefaultUi) { + Log.w(TAG, "Already using default UI. Setting play time will have no effect"); + return this; + } + + mPlaybackTime = playTime; + + // initialize the playtime to 0 + updatePlaytime(0); + return this; + } + + /*** + * Sets current playback time view. Use this if you have a playback time counter in the UI. + * + * @see nl.changer.audiowife.AudioWife#setTotalTimeView(android.widget.TextView) + ****/ + public AudioWife setRuntimeView(TextView currentTime) { + + if (mHasDefaultUi) { + Log.w(TAG, "Already using default UI. Setting play time will have no effect"); + return this; + } + + mRunTime = currentTime; + + // initialize the playtime to 0 + updateRuntime(0); + return this; + } + + /*** + * Sets the total playback time view. Use this if you have a playback time counter in the UI. + * + * @see nl.changer.audiowife.AudioWife#setRuntimeView(android.widget.TextView) + ****/ + public AudioWife setTotalTimeView(TextView totalTime) { + + if (mHasDefaultUi) { + Log.w(TAG, "Already using default UI. Setting play time will have no effect"); + return this; + } + + mTotalTime = totalTime; + + setTotalTime(); + return this; + } + + public AudioWife setSeekBar(SeekBar seekbar) { + + if (mHasDefaultUi) { + Log.w(TAG, "Already using default UI. Setting seek bar will have no effect"); + return this; + } + + mSeekBar = seekbar; + initMediaSeekBar(); + return this; + } + + /**** + * Add custom playback completion listener. Adding multiple listeners will queue up all the + * listeners and fire them on media playback completes. + */ + public AudioWife addOnCompletionListener(OnCompletionListener listener) { + + // add default click listener to the top + // so that it is the one that gets fired first + mCompletionListeners.add(0, listener); + + return this; + } + + /**** + * Add custom play view click listener. Calling this method multiple times will queue up all the + * listeners and fire them all together when the event occurs. + ***/ + public AudioWife addOnPlayClickListener(View.OnClickListener listener) { + + mPlayListeners.add(listener); + + return this; + } + + /*** + * Add custom pause view click listener. Calling this method multiple times will queue up all + * the listeners and fire them all together when the event occurs. + ***/ + public AudioWife addOnPauseClickListener(View.OnClickListener listener) { + + mPauseListeners.add(listener); + + return this; + } + + /**** + * Initialize and prepare the audio player + ****/ + private void initPlayer(Context ctx) { + + mMediaPlayer = new MediaPlayer(); + mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + + try { + mMediaPlayer.setDataSource(ctx, mUri); + } catch (Exception ignored) { + } + + try { + mMediaPlayer.prepare(); + } catch (Exception ignored) { + } + + mMediaPlayer.setOnCompletionListener(mOnCompletion); + } + + private void initMediaSeekBar() { + + if (mSeekBar == null) { + return; + } + + // update seekbar + long finalTime = mMediaPlayer.getDuration(); + mSeekBar.setMax((int) finalTime); + + mSeekBar.setProgress(0); + + mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + mMediaPlayer.seekTo(seekBar.getProgress()); + + // if the audio is paused and seekbar is moved, + // update the play time in the UI. + updateRuntime(seekBar.getProgress()); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + + } + }); + } + + private void fireCustomCompletionListeners(MediaPlayer mp) { + for (OnCompletionListener listener : mCompletionListeners) { + listener.onCompletion(mp); + } + } + + public void cleanPlayerUi() { + ((ViewGroup) playerUi.getParent()).removeView(playerUi); + } + + public View getPlayerUi() { + return playerUi; + } + + public AudioWife useDefaultUi(ViewGroup playerContainer, LayoutInflater inflater) { + if (playerContainer == null) { + throw new NullPointerException("Player container cannot be null"); + } + + if (inflater == null) { + throw new IllegalArgumentException("Inflater cannot be null"); + } + + playerUi = inflater.inflate(R.layout.aw_player, playerContainer, false); // IMPORTANT, sonst geht meine Lösung nicht xD + + // init play view + View playView = playerUi.findViewById(R.id.play); + setPlayView(playView); + + // init pause view + View pauseView = playerUi.findViewById(R.id.pause); + setPauseView(pauseView); + + // init seekbar + SeekBar seekBar = (SeekBar) playerUi.findViewById(R.id.media_seekbar); + setSeekBar(seekBar); + + // init playback time view + TextView playbackTime = (TextView) playerUi.findViewById(R.id.playback_time); + setPlaytime(playbackTime); + + // this has to be set after all the views + // have finished initializing. + mHasDefaultUi = true; + return this; + } + public void release() { + + if (mMediaPlayer != null) { + mMediaPlayer.stop(); + mMediaPlayer.reset(); + mMediaPlayer.release(); + mMediaPlayer = null; + mProgressUpdateHandler = null; + } + } +} diff --git a/libs/audiowife/src/main/res/drawable-mdpi/ic_launcher.png b/libs/audiowife/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 000000000..359047dfa Binary files /dev/null and b/libs/audiowife/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/libs/audiowife/src/main/res/drawable-xhdpi/ic_launcher.png b/libs/audiowife/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 000000000..71c6d760f Binary files /dev/null and b/libs/audiowife/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/libs/audiowife/src/main/res/drawable-xxhdpi/ic_launcher.png b/libs/audiowife/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..4df189464 Binary files /dev/null and b/libs/audiowife/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/libs/audiowife/src/main/res/drawable/gray_border_wo_padding.xml b/libs/audiowife/src/main/res/drawable/gray_border_wo_padding.xml new file mode 100644 index 000000000..409ed7ccd --- /dev/null +++ b/libs/audiowife/src/main/res/drawable/gray_border_wo_padding.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/libs/audiowife/src/main/res/layout/aw_player.xml b/libs/audiowife/src/main/res/layout/aw_player.xml new file mode 100644 index 000000000..6d2904e31 --- /dev/null +++ b/libs/audiowife/src/main/res/layout/aw_player.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/audiowife/src/main/res/values/strings.xml b/libs/audiowife/src/main/res/values/strings.xml new file mode 100644 index 000000000..66966868f --- /dev/null +++ b/libs/audiowife/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + LibAudioWife + + diff --git a/libs/audiowife/src/main/res/values/styles.xml b/libs/audiowife/src/main/res/values/styles.xml new file mode 100644 index 000000000..6ce89c7ba --- /dev/null +++ b/libs/audiowife/src/main/res/values/styles.xml @@ -0,0 +1,20 @@ + + + + + + + + + -- cgit v1.2.3