add player
* fix aod stream parsing
This commit is contained in:
		@ -37,14 +37,17 @@ dependencies {
 | 
			
		||||
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
 | 
			
		||||
    implementation 'androidx.core:core-ktx:1.3.2'
 | 
			
		||||
    implementation 'androidx.appcompat:appcompat:1.2.0'
 | 
			
		||||
    implementation 'com.google.android.material:material:1.2.1'
 | 
			
		||||
    implementation 'com.google.code.gson:gson:2.8.6'
 | 
			
		||||
 | 
			
		||||
    implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
 | 
			
		||||
    implementation 'androidx.navigation:navigation-fragment:2.3.0'
 | 
			
		||||
    implementation 'androidx.navigation:navigation-ui:2.3.0'
 | 
			
		||||
    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
 | 
			
		||||
    implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0'
 | 
			
		||||
    implementation 'androidx.navigation:navigation-ui-ktx:2.3.0'
 | 
			
		||||
    implementation 'androidx.security:security-crypto:1.1.0-alpha02'
 | 
			
		||||
 | 
			
		||||
    implementation 'com.google.android.material:material:1.2.1'
 | 
			
		||||
    implementation 'com.google.code.gson:gson:2.8.6'
 | 
			
		||||
 | 
			
		||||
    implementation 'org.jsoup:jsoup:1.13.1'
 | 
			
		||||
    implementation 'com.github.bumptech.glide:glide:4.11.0'
 | 
			
		||||
@ -52,6 +55,11 @@ dependencies {
 | 
			
		||||
    implementation 'androidx.recyclerview:recyclerview:1.1.0'
 | 
			
		||||
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
 | 
			
		||||
 | 
			
		||||
    implementation 'com.google.android.exoplayer:exoplayer-core:2.12.0'
 | 
			
		||||
    implementation 'com.google.android.exoplayer:exoplayer-hls:2.12.0'
 | 
			
		||||
    implementation 'com.google.android.exoplayer:exoplayer-dash:2.12.0'
 | 
			
		||||
    implementation 'com.google.android.exoplayer:exoplayer-ui:2.12.0'
 | 
			
		||||
 | 
			
		||||
    testImplementation 'junit:junit:4.12'
 | 
			
		||||
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
 | 
			
		||||
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,7 @@
 | 
			
		||||
        android:roundIcon="@mipmap/ic_launcher_round"
 | 
			
		||||
        android:supportsRtl="true"
 | 
			
		||||
        android:theme="@style/AppTheme">
 | 
			
		||||
        <activity android:name=".PlayerActivity"></activity>
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".MainActivity"
 | 
			
		||||
            android:label="@string/app_name">
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,8 @@
 | 
			
		||||
package org.mosad.teapod
 | 
			
		||||
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.util.Log
 | 
			
		||||
import android.view.MenuItem
 | 
			
		||||
import com.google.android.material.bottomnavigation.BottomNavigationView
 | 
			
		||||
import androidx.appcompat.app.AppCompatActivity
 | 
			
		||||
@ -8,6 +10,7 @@ import androidx.fragment.app.Fragment
 | 
			
		||||
import androidx.fragment.app.commit
 | 
			
		||||
import kotlinx.android.synthetic.main.activity_main.*
 | 
			
		||||
import org.mosad.teapod.parser.AoDParser
 | 
			
		||||
import org.mosad.teapod.preferences.EncryptedPreferences
 | 
			
		||||
import org.mosad.teapod.ui.MediaFragment
 | 
			
		||||
import org.mosad.teapod.ui.account.AccountFragment
 | 
			
		||||
import org.mosad.teapod.ui.home.HomeFragment
 | 
			
		||||
@ -69,7 +72,14 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun load() {
 | 
			
		||||
        // TODO
 | 
			
		||||
        EncryptedPreferences.readCredentials(this)
 | 
			
		||||
 | 
			
		||||
        if (EncryptedPreferences.password.isEmpty()) {
 | 
			
		||||
            Log.i(javaClass.name, "please login!")
 | 
			
		||||
 | 
			
		||||
            // TODO show login dialog
 | 
			
		||||
            //EncryptedPreferences.saveCredentials("", "", this)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun showDetailFragment(media: GUIMedia) {
 | 
			
		||||
@ -85,4 +95,11 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
 | 
			
		||||
        println("visible !!!: " + mediaFragment.isVisible)
 | 
			
		||||
        println(supportFragmentManager.backStackEntryCount)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun startPlayer(streamUrl: String) {
 | 
			
		||||
        val intent = Intent(this, PlayerActivity::class.java).apply {
 | 
			
		||||
            putExtra("streamUrl", streamUrl)
 | 
			
		||||
        }
 | 
			
		||||
        startActivity(intent)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										107
									
								
								app/src/main/java/org/mosad/teapod/PlayerActivity.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								app/src/main/java/org/mosad/teapod/PlayerActivity.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,107 @@
 | 
			
		||||
package org.mosad.teapod
 | 
			
		||||
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.view.View
 | 
			
		||||
import androidx.appcompat.app.AppCompatActivity
 | 
			
		||||
import com.google.android.exoplayer2.ExoPlayer
 | 
			
		||||
import com.google.android.exoplayer2.MediaItem
 | 
			
		||||
import com.google.android.exoplayer2.Player
 | 
			
		||||
import com.google.android.exoplayer2.SimpleExoPlayer
 | 
			
		||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource
 | 
			
		||||
import com.google.android.exoplayer2.upstream.DataSource
 | 
			
		||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
 | 
			
		||||
import com.google.android.exoplayer2.util.Util
 | 
			
		||||
import kotlinx.android.synthetic.main.activity_player.*
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PlayerActivity : AppCompatActivity() {
 | 
			
		||||
 | 
			
		||||
    private lateinit var player: SimpleExoPlayer
 | 
			
		||||
    private lateinit var dataSourceFactory: DataSource.Factory
 | 
			
		||||
 | 
			
		||||
    private var streamUrl = ""
 | 
			
		||||
 | 
			
		||||
    private var playWhenReady = true
 | 
			
		||||
    private var currentWindow = 0
 | 
			
		||||
    private var playbackPosition: Long = 0
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
        setContentView(R.layout.activity_player)
 | 
			
		||||
 | 
			
		||||
        streamUrl = intent.getStringExtra("streamUrl").toString()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    override fun onStart() {
 | 
			
		||||
        super.onStart()
 | 
			
		||||
        if (Util.SDK_INT > 23) {
 | 
			
		||||
            initPlayer()
 | 
			
		||||
            if (video_view != null) video_view.onResume()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onResume() {
 | 
			
		||||
        super.onResume()
 | 
			
		||||
        if (Util.SDK_INT <= 23) {
 | 
			
		||||
            initPlayer()
 | 
			
		||||
            if (video_view != null) video_view.onResume()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onPause() {
 | 
			
		||||
        super.onPause()
 | 
			
		||||
        if (Util.SDK_INT <= 23) {
 | 
			
		||||
            if (video_view != null) video_view.onPause()
 | 
			
		||||
            releasePlayer()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onStop() {
 | 
			
		||||
        super.onStop()
 | 
			
		||||
        if (Util.SDK_INT > 23) {
 | 
			
		||||
            if (video_view != null) video_view.onPause()
 | 
			
		||||
            releasePlayer()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun initPlayer() {
 | 
			
		||||
        if (streamUrl.isEmpty()) {
 | 
			
		||||
            println("no streamUrl set")
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        player = SimpleExoPlayer.Builder(this).build()
 | 
			
		||||
        dataSourceFactory = DefaultDataSourceFactory(this, Util.getUserAgent(this, "Teapod"))
 | 
			
		||||
 | 
			
		||||
        val mediaSource = HlsMediaSource.Factory(dataSourceFactory)
 | 
			
		||||
            .createMediaSource(MediaItem.fromUri(Uri.parse(streamUrl)))
 | 
			
		||||
 | 
			
		||||
        player.setMediaSource(mediaSource)
 | 
			
		||||
        player.prepare()
 | 
			
		||||
        player.play()
 | 
			
		||||
 | 
			
		||||
        player.addListener(object : Player.EventListener {
 | 
			
		||||
            override fun onPlaybackStateChanged(state: Int) {
 | 
			
		||||
                super.onPlaybackStateChanged(state)
 | 
			
		||||
 | 
			
		||||
                loading.visibility = when (state) {
 | 
			
		||||
                    ExoPlayer.STATE_READY ->  View.GONE
 | 
			
		||||
                    ExoPlayer.STATE_BUFFERING -> View.VISIBLE
 | 
			
		||||
                    else -> View.GONE
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        video_view.player = player
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun releasePlayer(){
 | 
			
		||||
        playbackPosition = player.currentPosition
 | 
			
		||||
        currentWindow = player.currentWindowIndex
 | 
			
		||||
        playWhenReady = player.playWhenReady
 | 
			
		||||
        player.release()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -1,57 +0,0 @@
 | 
			
		||||
package org.mosad.teapod.dummy
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList
 | 
			
		||||
import java.util.HashMap
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Helper class for providing sample content for user interfaces created by
 | 
			
		||||
 * Android template wizards.
 | 
			
		||||
 *
 | 
			
		||||
 * TODO: Replace all uses of this class before publishing your app.
 | 
			
		||||
 */
 | 
			
		||||
object DummyContent {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * An array of sample (dummy) items.
 | 
			
		||||
     */
 | 
			
		||||
    val ITEMS: MutableList<DummyItem> = ArrayList()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A map of sample (dummy) items, by ID.
 | 
			
		||||
     */
 | 
			
		||||
    val ITEM_MAP: MutableMap<String, DummyItem> = HashMap()
 | 
			
		||||
 | 
			
		||||
    private val COUNT = 25
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        // Add some sample items.
 | 
			
		||||
        for (i in 1..COUNT) {
 | 
			
		||||
            addItem(createDummyItem(i))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun addItem(item: DummyItem) {
 | 
			
		||||
        ITEMS.add(item)
 | 
			
		||||
        ITEM_MAP.put(item.id, item)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun createDummyItem(position: Int): DummyItem {
 | 
			
		||||
        return DummyItem(position.toString(), "Item " + position, makeDetails(position))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun makeDetails(position: Int): String {
 | 
			
		||||
        val builder = StringBuilder()
 | 
			
		||||
        builder.append("Details about Item: ").append(position)
 | 
			
		||||
        for (i in 0..position - 1) {
 | 
			
		||||
            builder.append("\nMore details information here.")
 | 
			
		||||
        }
 | 
			
		||||
        return builder.toString()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A dummy item representing a piece of content.
 | 
			
		||||
     */
 | 
			
		||||
    data class DummyItem(val id: String, val content: String, val details: String) {
 | 
			
		||||
        override fun toString(): String = content
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -4,16 +4,14 @@ import com.google.gson.JsonParser
 | 
			
		||||
import kotlinx.coroutines.*
 | 
			
		||||
import org.jsoup.Connection
 | 
			
		||||
import org.jsoup.Jsoup
 | 
			
		||||
import org.mosad.teapod.preferences.EncryptedPreferences
 | 
			
		||||
import org.mosad.teapod.util.GUIMedia
 | 
			
		||||
 | 
			
		||||
class AoDParser {
 | 
			
		||||
 | 
			
		||||
    private val baseURL = "https://www.anime-on-demand.de"
 | 
			
		||||
    private val loginPath = "/users/sign_in"
 | 
			
		||||
 | 
			
		||||
    // TODO
 | 
			
		||||
    private val login = ""
 | 
			
		||||
    private val pwd = ""
 | 
			
		||||
    private val libraryPath = "/animes"
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        private var sessionCookies = mutableMapOf<String, String>()
 | 
			
		||||
@ -39,8 +37,8 @@ class AoDParser {
 | 
			
		||||
            println("cookies: $cookies")
 | 
			
		||||
 | 
			
		||||
            val data = mapOf(
 | 
			
		||||
                Pair("user[login]", login),
 | 
			
		||||
                Pair("user[password]", pwd),
 | 
			
		||||
                Pair("user[login]", EncryptedPreferences.login),
 | 
			
		||||
                Pair("user[password]", EncryptedPreferences.password),
 | 
			
		||||
                Pair("user[remember_me]", "1"),
 | 
			
		||||
                Pair("commit", "Einloggen"),
 | 
			
		||||
                Pair("authenticity_token", authenticityToken)
 | 
			
		||||
@ -67,7 +65,7 @@ class AoDParser {
 | 
			
		||||
        if (sessionCookies.isEmpty()) login()
 | 
			
		||||
 | 
			
		||||
        withContext(Dispatchers.Default) {
 | 
			
		||||
            val resAnimes = Jsoup.connect("$baseURL/animes")
 | 
			
		||||
            val resAnimes = Jsoup.connect(baseURL + libraryPath)
 | 
			
		||||
                .cookies(sessionCookies)
 | 
			
		||||
                .get()
 | 
			
		||||
 | 
			
		||||
@ -147,7 +145,7 @@ class AoDParser {
 | 
			
		||||
                .get("sources").asJsonArray
 | 
			
		||||
 | 
			
		||||
            return@withContext sources.toList().map {
 | 
			
		||||
                it.asJsonObject.get("file").toString()
 | 
			
		||||
                it.asJsonObject.get("file").asString
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,66 @@
 | 
			
		||||
package org.mosad.teapod.preferences
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.SharedPreferences
 | 
			
		||||
import android.security.keystore.KeyGenParameterSpec
 | 
			
		||||
import android.security.keystore.KeyProperties
 | 
			
		||||
import android.util.Log
 | 
			
		||||
import androidx.security.crypto.EncryptedSharedPreferences
 | 
			
		||||
import androidx.security.crypto.MasterKey
 | 
			
		||||
import org.mosad.teapod.R
 | 
			
		||||
 | 
			
		||||
object EncryptedPreferences {
 | 
			
		||||
 | 
			
		||||
    var login = ""
 | 
			
		||||
        internal set
 | 
			
		||||
    var password = ""
 | 
			
		||||
        internal set
 | 
			
		||||
 | 
			
		||||
    fun saveCredentials(login: String, password: String, context: Context) {
 | 
			
		||||
        this.login = login
 | 
			
		||||
        this.password = password
 | 
			
		||||
 | 
			
		||||
        with(getEncryptedPreferences(context)?.edit()) {
 | 
			
		||||
            this?.putString(context.getString(R.string.save_key_user_login), login)
 | 
			
		||||
            this?.putString(context.getString(R.string.save_key_user_password), password)
 | 
			
		||||
            this?.apply()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun readCredentials(context: Context) {
 | 
			
		||||
        with(getEncryptedPreferences(context)) {
 | 
			
		||||
            login = this?.getString(context.getString(R.string.save_key_user_login), "").toString()
 | 
			
		||||
            password = this?.getString(context.getString(R.string.save_key_user_password), "").toString()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * create a encrypted shared preference
 | 
			
		||||
     */
 | 
			
		||||
    private fun getEncryptedPreferences(context: Context): SharedPreferences? {
 | 
			
		||||
        return try {
 | 
			
		||||
            val spec = KeyGenParameterSpec.Builder(
 | 
			
		||||
                MasterKey.DEFAULT_MASTER_KEY_ALIAS,
 | 
			
		||||
                KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
 | 
			
		||||
                .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
 | 
			
		||||
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
 | 
			
		||||
                .setKeySize(MasterKey.DEFAULT_AES_GCM_MASTER_KEY_SIZE)
 | 
			
		||||
                .build()
 | 
			
		||||
 | 
			
		||||
            val masterKey = MasterKey.Builder(context)
 | 
			
		||||
                .setKeyGenParameterSpec(spec)
 | 
			
		||||
                .build()
 | 
			
		||||
 | 
			
		||||
            EncryptedSharedPreferences.create(
 | 
			
		||||
                context,
 | 
			
		||||
                context.getString(R.string.encrypted_preference_file_key),
 | 
			
		||||
                masterKey,
 | 
			
		||||
                EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
 | 
			
		||||
                EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
 | 
			
		||||
            )
 | 
			
		||||
        } catch (ex: Exception) {
 | 
			
		||||
            Log.e(javaClass.name, "Could not create encrypted shared preference.", ex)
 | 
			
		||||
            null
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -7,8 +7,11 @@ import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import com.bumptech.glide.Glide
 | 
			
		||||
import kotlinx.android.synthetic.main.fragment_media.*
 | 
			
		||||
import org.mosad.teapod.MainActivity
 | 
			
		||||
import org.mosad.teapod.R
 | 
			
		||||
import org.mosad.teapod.util.GUIMedia
 | 
			
		||||
import java.net.URL
 | 
			
		||||
import java.net.URLEncoder
 | 
			
		||||
 | 
			
		||||
class MediaFragment(val media: GUIMedia, val streams: List<String>) : Fragment() {
 | 
			
		||||
 | 
			
		||||
@ -36,7 +39,8 @@ class MediaFragment(val media: GUIMedia, val streams: List<String>) : Fragment()
 | 
			
		||||
    private fun onClickButtonPlay() {
 | 
			
		||||
        println("play ${streams.first()}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        val mainActivity = activity as MainActivity
 | 
			
		||||
        mainActivity.startPlayer(streams.first())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										23
									
								
								app/src/main/res/layout/activity_player.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								app/src/main/res/layout/activity_player.xml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    xmlns:app="http://schemas.android.com/apk/res-auto"
 | 
			
		||||
    xmlns:tools="http://schemas.android.com/tools"
 | 
			
		||||
    android:layout_width="match_parent"
 | 
			
		||||
    android:layout_height="match_parent"
 | 
			
		||||
    android:background="#000000"
 | 
			
		||||
    tools:context=".PlayerActivity">
 | 
			
		||||
 | 
			
		||||
    <com.google.android.exoplayer2.ui.PlayerView
 | 
			
		||||
        android:id="@+id/video_view"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="match_parent"
 | 
			
		||||
        android:layout_gravity="center" />
 | 
			
		||||
    <!--    app:controller_layout_id="@layout/player_custom_control"/>-->
 | 
			
		||||
 | 
			
		||||
    <ProgressBar
 | 
			
		||||
        android:id="@+id/loading"
 | 
			
		||||
        android:layout_width="70dp"
 | 
			
		||||
        android:layout_height="70dp"
 | 
			
		||||
        android:layout_gravity="center" />
 | 
			
		||||
 | 
			
		||||
</FrameLayout>
 | 
			
		||||
@ -4,4 +4,9 @@
 | 
			
		||||
    <string name="title_library">Library</string>
 | 
			
		||||
    <string name="title_search">Search</string>
 | 
			
		||||
    <string name="title_account">Account</string>
 | 
			
		||||
 | 
			
		||||
    <!-- save keys -->
 | 
			
		||||
    <string name="encrypted_preference_file_key" translatable="false">org.mosad.teapod.encrypted_preferences</string>
 | 
			
		||||
    <string name="save_key_user_login" translatable="false">org.mosad.teapod.user_login</string>
 | 
			
		||||
    <string name="save_key_user_password" translatable="false">org.mosad.teapod.user_password</string>
 | 
			
		||||
</resources>
 | 
			
		||||
		Reference in New Issue
	
	Block a user