IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android笔记(四) -> 正文阅读

[移动开发]Android笔记(四)

Android笔记(四)

1. 数据存储

1.1 文件存储

数据存储到文件中:

Context提供的openFileOutput()方法

  • 第一个参数:文件名,不指定路径,默认存储到/data/data/<package name>/files/
  • 第二个参数:文件操作模式,可选值MODE_PRIVATE(默认,覆盖)和MODE_APPEND(追加)
  • 返回值:FileOutputStream对象
fun save(inputText: String) {
    try {
        val output = openFileOutput("data", Context.MODE_PRIVATE)
        val writer = BufferedWriter(OutputStreamWriter(output))
        writer.use {
            it.write(inputText)
        }
    } catch(e: IOException) {
        e.printStackTrace()
    }
}

Kotlin小知识——use函数,保证Lambda表达式中代码全部执行完毕之后自动将外层的流关闭,不用再使用finally语句手动关闭流

从文件中读取数据:

Context提供的openFileInput()方法

  • 参数:文件名
  • 返回值:FileInputStream对象
fun load(): String {
    val content = StringBuilder()
    try {
        val input = openFileInput("data")
        val reader = BufferedReader(InputStreamReader(input))
        reader.use {
            reader.forEachLine() {
                content.append(it)
            }
        }
    } catch(e: IOException) {
        e.printStackTrace()
    }
    return content.toString()
}

Kotlin小知识——forEachLine函数,将读到的每行内容回调到Lambda表达式中

1.2 SharedPreferences存储

键值对的方式存储,支持不同数据类型存储

将数据存储到SharedPreferences:

  1. 获取SharedPreferences对象

    1. Context类的getSharedPreferences()方法
      • 第一个参数:文件名称,默认存储到/data/data/<package name>/shared_prefs/
      • 第二个参数:操作模式,目前只有默认MODE_PRIVATE,其他均废弃
    2. Activity类的getPreferences()方法
      • 参数:操作模式
      • 自动以当前Activity类名为文件名
  2. 调用SharedPreferences对象的edit()方法获取SharedPreferences.Editor对象

  3. 向SharedPreferences.Editor对象添加数据,提供一系列putXxx()方法

  4. 调用Editor对象apply()方法提交添加的数据

button.setOnClickLietener {
    val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
    editor.putString("name", "Tom")
    editor.putInt("age", 20)
    editor.apply()
}

从SharedPreferences读取数据:

SharedPreferences对象提供一系列getXxx()方法

  • 第一个参数:键
  • 第二个参数:默认值

1.3 SQLite数据库存储

创建数据库:

提供SQLiteOpenHelper抽象类帮助创建和升级数据库

  • 构造函数(一般选择参数少的即可)

    • 第一个参数:Context
    • 第二个参数:数据库名
    • 第三个参数:允许在查询数据时返回一个自定义的Cursor,一般传入null即可
    • 第四个参数:版本号
  • 重写onCreate()方法:实现数据库创建

  • 重写onUpgrade()方法:实现数据库升级

  • 当版本号发生改变的时候会自动调用onUpgrade()函数实现数据库升级

  • getReadableDatabase()和geWritableDatabase()

    • 创建或打开一个现有数据库,返回一个可对数据库操作对象SQLiteDataBase对象
    • 不同在于数据不可写入时,前者返回只读方式打开数据库,后者出现异常
  • 数据库文件一般存放在/data/data/<package name>/databases

增删改:

方法参数描述
SQLiteDataBase.insert()表名,null,ContentValues
SQLiteDataBase.update()表名,ContentValues,where,value
SQLiteDataBase.delete()表名,where,value

ContentValues对象:提供一系列put()重载,用于向ContentValues中添加数据,将列名以及待添加值传入即可,例put("name", "The Da Vinci Code")

上述的where为约束条件,其中使用占位符?,value是一个字符串数组,提供值替换占位符,例db.update("Book", contentValue, "name=?", arrayOf("The Da Vinci Code"))

查询数据:

SQLiteDataBase.query()返回一个Cursor对象

参数对应SQL部分
tablefrom table_name
columsselect column1, column2
selectionwhere column = value
selectionArgsvalue
gourpBygroup by column
havinghaving column = value
orderByorder by column1, column2

Cursor常用方法:

  • moveToFirst()
  • moveToNext()
  • getColumnIndex(列名):通过列名返回表中对应的位置索引
  • getXxx(列索引):通过列的索引获取值,通常配合上个方法使用
  • close()

SQLiteDataBase.execSQL(SQL语句,值)执行SQL语句,例db.execSQL(insert into Book(name, aythor, price) values(?, ?, ?), arrayOf("The Lost Symbol", "Dan Brown", "19.95"))

1.4 SQLite数据库实践

事务:

  • SQLiteDataBase.beginTransaction():开启事务
  • SQLiteDataBase.setTransactionSuccessful():设置事务执行成功
  • SQLiteDataBase.endTransaction():结束事务

升级数据库:

按需求增加修改表,但又要保留之前数据

class MyDatabaseHelper(val context: Context, name: String, version: Int):
	SQLiteOpenHelper(context, name, null, version) {
	private val createBook = "create table Book ("+
				"id integer primary key autoincrement," +
				"author text," +
				"price real," +
				"pages integer," +
				"name text)"
        
	override fun onCreate(db: SQLiteDatabase) {
		db.execSQL(createBook)
	}
        
	override fun onupgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
	}
}

新需求:增加一张Category表(category:类别)

class MyDatabaseHelper(val context: Context, name: String, version: Int):
	SQLiteOpenHelper(context, name, null, version) {
	private val createBook = "create table Book ("+
				"id integer primary key autoincrement," +
				"author text," +
				"price real," +
				"pages integer," +
				"name text)"
    private val createCategory = "create table Category(" + 
        		"id integer primary key autoincremnet," + 
        		"category_name text," +
        		"category_code integer)"
        
	override fun onCreate(db: SQLiteDatabase) {
        // 新用户会执行
		db.execSQL(createBook)
        db.execSQL(createCategory)
	}
        
	override fun onupgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        if (oldVersion <= 1)
        	// 第一版老用户升级只会创建Category表
        	db.execSQL(createCategory)
	}
}

新需求:Book和Category建立关联,在Book表中添加一个category_id字段

class MyDatabaseHelper(val context: Context, name: String, version: Int):
	SQLiteOpenHelper(context, name, null, version) {
	private val createBook = "create table Book ("+
				"id integer primary key autoincrement," +
				"author text," +
				"price real," +
				"pages integer," +
				"name text," +
        		"category_id integer)"
    private val createCategory = "create table Category(" + 
        		"id integer primary key autoincremnet," + 
        		"category_name text," +
        		"category_code integer)"
        
	override fun onCreate(db: SQLiteDatabase) {
        // 新用户会执行
		db.execSQL(createBook)
        db.execSQL(createCategory)
	}
        
	override fun onupgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        if (oldVersion <= 1)
        	db.execSQL(createCategory)
        if (oldVersion <= 2)
        	// 第一版老用户升级会执行两个if语句,第二版用户升级只会执行第二个if语句
        	db.execSQL("alter table Book add column category_id integer")
	}
}

Google退出了一个专门用于Android平台的数据库框架——Room,后面会进行学习

1.5 使用Kotlin高阶函数简化操作

简化SharedPreferences用法:

fun SharedPreferences.open(bolck: SharedPreferences.Editor.() -> Unit) {
    val editor = edit()
    editor.block()
    editor.apply()
}
// 使用
getSharedPreferences("data", Context.MODE_PRIVATE).open {
    putString("name", "Tom")
    putString("age", 20)
}
//Google提供的KTX扩展库包含上述的简化用法
getSharedPreferences("data", Context.MODE_PRIVATE).edit {
    putString("name", "Tom")
    putString("age", 20)
}

简化ContentVaues用法:

Kotlin小知识——Kotlin中使用A to B会创建一个Pair对象,Kotlin中的mapOf(“Apple” to 1, “Pear” to 2) 可以快速创建一个map

fun cvOf(vararg pairs: Pair<String, Any>): ContentValues {
    val cv = ContentValues()
    for (pair in pairs) {
        val key = pair.first
        val value = pair.second
        when(value) {
            is Int -> cv.put(key, value)	// value自动转换为Int
            is Long -> cv.put(key, value)
            is Short -> cv.put(key, value)
            is Float -> cv.put(key, value)
            is Double -> cv.put(key, value)
            is Boolean -> cv.put(key, value)
            is String -> cv.put(key, value)
            is Byte -> cv.put(key, value)
            is ByteArray -> cv.put(key, value)
            null -> cv.putNull(key)
        }
    }
    return cv
}

val values = cvOf("name" to "Game of Thrones", "author" to "George Martin")
db.insert("Book", null, value)

KTX扩展库也提供了类似的contentValuesOf()方法

相关知识:

Java IO流 - CSDN博客

android studio使用database navigator查看数据库 - CSDN博客

Android中的Cursor到底是什么?如何理解Cursor的方法都在做什么事情?- CSDN博客

Cursor - Android Developers

Kotlin高阶函数的理解与使用 - 简书

2. 运行时权限

  • 普通权限:只需要在AndroidManifest.xml文件中添加权限声明就可以
  • 危险权限:必须用户手动授权(运行时权限处理)

程序运行时申请权限:

以Intent.ACTION_CALL权限为例

先在AndroidManifest.xml声明权限

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        makeCall.setOnClickListener {
            if (ContextCompat.checkSelfPermission(this,
                 Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
                // 未授权,弹窗申请授权
                ActivityCompat.requestPermissions(this,
                 arrayOf(Manifest.permission.CALL_PHONE), 1)
            } else {
                call()
            }
        }
    }
    
    // 用户选择完授权结果后,回调该方法
    override fun onRequestPermissionsResult(requestCode: Int,
          	permissions: Array<String>, grantResults: IntArray) {
        // requestCode:请求码
        // permissions:申请的权限
        // grantResults:授权结果
        super.onRequestPermissionsResult(requestCode,
          	permissions, grantResults)
        when(requestCode) {
            1 -> {
                if (grantResults.isNotEmpty() &&
                  	grantResults[0] == PackageManager.PERMISSION_GRANTED){
                    call()
                } else {
                    Toast.makeText(...)
                }
            }
        }
    }
    
    private fun call() {
        try {
            val intent = Intent(Intent.ACTION_CALL)
            intent.data = Uri.parse("tel:10086")
            startActivity(intent)
        } catch(e: SecurityException) {
            e.printStackTrace()
        }
    }
}

ContextCompat.checkSelfPermission()

  • 第一个参数:Context
  • 第二个参数:具体权限名
  • 返回值和PackageManager.PERMISSION_GRANTED比较,相等则认为已授权

ActivityCompat.requestPermissions()

  • 第一个参数:Activity实例
  • 第二个参数:String数组,存放要申请的权限名
  • 第三个参数:请求码

相关知识:

Android 危险权限列表 - CSDN博客

3. ContentProvider

主要用于在不同应用程序之间实现数据共享功能

3.1 Uri通用资源标志符

Uri代表要操作的数据,Android上可用每种资源都可以用Uri表示,URI包括URL

内容Uri: 给ContentProvider中的数据建立唯一标识符

  • authority部分:用于对不同应用程序做区分,一般采用应用包名的方式进行命名,例com.example.app.provider
  • path部分:对同一应用程序中不同表做区分,通常添加到authority后面
  • 前面还要加上协议部分,标准格式:content://com.example.app.provider/table1
  • 可以在后面加上一个id,例content://com.example.app.provider/table1/1表示调用方期望访问的是com.example.app引用的table1表中id为1的数据
    • *:匹配任意长度的任意字符
    • #:匹配任意长度的数字

Uri.pares(内容URI)可以将内容Uri解析成Uri对象

3.2 ContentResolver基本用法

访问ContentProvider中共享数据一定要借助ContentResolver类(resolver:解析器),通过Context中的getContentResolver()获取实例

ContentResolver提供一系列insert()、update()、delete()、query(),与SQLite的方法只是在参数上有点差别(不接收表名,而是使用Uri参数)

query()方法参数对应SQL部分
urifrom table_name
projectionselect column1, column2
selectionwhere column = value
selectionArgsvalue
sortOrderorder by column1, column2

其他方法与SQLite大致类似

读取联系人:

class MainActivity : AppCompatActivity() {

    private val contactsList = ArrayList<String>()
    private lateinit var adapter: ArrayAdapter<String>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, contactsList)
        listview.adapter = adapter
        if (ContextCompat.checkSelfPermission(this,
            android.Manifest.permission.READ_CONTACTS) !=
                PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this,
            arrayOf(android.Manifest.permission.READ_CONTACTS), 1)
        } else {
            readContacts()
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when (requestCode) {
            1 -> {
                if (grantResults.isNotEmpty() &&
                        grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    readContacts()
                } else {
                    Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

    @SuppressLint("Range")
    private fun readContacts() {
        contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
        null, null, null, null) ?. apply {
            while (moveToNext()) {
                val name = getString(getColumnIndex(
                    ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME
                ))
                val tel = getString(getColumnIndex(
                    ContactsContract.CommonDataKinds.Phone.NUMBER
                ))
                contactsList.add("$name\n$tel")
            }
            adapter.notifyDataSetChanged()
            close()
        }
    }
}

3.3 创建自己的ContentProvider

新建一个类继承ContentProvider

class MyProvider : ContentProvider() {
    // 重写六个方法
    override fun onCreate(): Boolean {
        // 初始化ContentProvider时调用,通常在其中完成对数据库的创建和升级等操作
        // 返回 true:ContentProvider初始化成功 false:初始化失败
        return false
    }

    override fun query(
        p0: Uri,
        p1: Array<out String>?,
        p2: String?,
        p3: Array<out String>?,
        p4: String?
    ): Cursor? {
        // 从ContentProvider查询数据
        TODO("Not yet implemented")
    }

    override fun getType(p0: Uri): String? {
        // 根据传入的内容URI返回响应的MIME类型
        TODO("Not yet implemented")
    }

    override fun insert(p0: Uri, p1: ContentValues?): Uri? {
        // 向ContentProvider添加数据
        TODO("Not yet implemented")
    }

    override fun delete(p0: Uri, p1: String?, p2: Array<out String>?)
    :Int{
        // 从ContentProvider删除数据
        TODO("Not yet implemented")
    }

    override fun update(p0: Uri, p1: ContentValues?, p2: String?, p3: Array<out String>?): Int {
        // 更新ContentProvider数据
        TODO("Not yet implemented")
    }
}

使用UriMatcher类实现匹配内容URI功能,提供了addURI()方法,参数分别为authority、path、自定义代码,调用UriMatcher.match(uri)方法,返回值是能够匹配这个Uri对象所对应的自定义代码,利用这个代码可以判断调用方期望访问哪张表的数据

class MyProvider : ContentProvider() {
    
    private val table1Dir = 0
    private val table1Item = 1
    private val table2Dir = 2
    private val table2Item = 3
    
    private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
    
    init {
        uriMatcher.addURI("com.example.app.provider", "table1", table1Dir)
        uriMatcher.addURI("com.example.app.provider", "table2", table2Dir)
        uriMatcher.addURI("com.example.app.provider", "table1/#", table1Item)
        uriMatcher.addURI("com.example.app.provider", "table2/#", table2Item)
    }
	// 以query()为例,其他方法使用UriMatcher类的方法差不多
    override fun query(
        p0: Uri,
        p1: Array<out String>?,
        p2: String?,
        p3: Array<out String>?,
        p4: String?
    ): Cursor? {
        when (uriMatcher.match(p0)) {
            table1Dir -> {
                // 查询table1表中所有数据
            }
            table2Dir -> {
                // 查询table2表中所有数据
            }
            table1Item -> {
                // 查询table1表中单条数据
            }
            table2Item -> {
                // 查询table2表中单条数据
            }
        }
        ...
    }
    ...
   
}

一个内容URI所对应的MIME类型主要由3部分组成,有以下规定:

  • 必须以vnd开头
  • 内容URI以路径结尾,则后接android.cursor.dir/;以id结尾,则后接android.cursor.item/
  • 最后接上vnd.<authority>.<path>
  • content://com.example.app.provider/table1对应的MIME类型写成vnd.android.cursor.dir/vnd.com.example.app.provider.table1
override fun getType(p0: Uri) = when(uriMatcher.match(p0)) {
        table1Dir -> "vnd.android.cursor.dir/vnd.com.example.app.provider.table1"
        table2Dir -> "vnd.android.cursor.dir/vnd.com.example.app.provider.table2"
        table1Item -> "vnd.android.cursor.item/vnd.com.example.app.provider.table1"
        table2Item -> "vnd.android.cursor.item/vnd.com.example.app.provider.table2"
        else -> null
}

完整的ContentProvider创建完成,其他应用可以使用ContentResolver访问我们程序中的数据

ContentProvider也是四大组件也需要向Activity一样在AndroidManifest.xml文件中注册

3.3 自定义ContentProvider实践

// 使用快捷方式创建ContentProvider
// 该程序有一个book表和category表
class DatabaseProvider : ContentProvider() {
    private val bookDir = 0
    private val bookItem = 1
    private val categoryDir = 2
    private val categoryItem = 3
    private val authority = "com.example.databasetest.provider"
    private val dbhelper: MyDatabaseHelper? = null
    
    // 使用Kotlin懒加载
    private val uriMatcher by lazy {
        val matcher = UriMatcher(UriMatcher.NO_MATCH)
        matcher.addURI(authority, "book", bookDir)
        matcher.addURI(authority, "book/#", bookItem)
        matcher.addURI(authority, "category", categoryDir)
        matcher.addURI(authority, "category/#", categoryItem)
        matcher
    }
    
    override fun onCreate() = context?.let {
        dbHelper = MyDatabaseHelper(it, "BookStore.db", 2)
        true
    }?: false

    override fun query(
        p0: Uri,
        p1: Array<out String>?,
        p2: String?,
        p3: Array<out String>?,
        p4: String?
    ) = dpHelper?.let {
        val db = it.readableDatabase
        val cursor = when (uriMatcher.match(p0)) {
            bookDir -> db.query("Book", p1, p2, p3, null, null, p4)
            bookItem -> {
                // 调用Uri的getPathSegments()方法,将内容URI权限后面的部分以/分割,第0位置为path,第1位置为id
                val bookId = p0.pathSegments[1]
                db.query("Book", p1, "id=?", arrayOf(bookId),
                         null, null, p4)
            }
            categoryDir -> db.query("Category", p1, p2, p3,
                                    null, null, p4)
            categoryItem -> {
                val categoryId = p0.pathSegments[1]
                db.query("Category", p1, "id=?", arrayOf(bookId),
                         null, null, p4)
            }
            else -> null
        }
        cursor
    }

    override fun getType(p0: Uri) = when(uriMatcher.match(p0)) {
        bookDir -> 		
        "vnd.android.cursor.dir/vnd.com.example.app.provider.book"
        categoryDir -> 
        "vnd.android.cursor.dir/vnd.com.example.app.provider.category"
        bookItem -> 
        	"vnd.android.cursor.item/vnd.com.example.app.provider.book"
        categoryItem -> 
        	"vnd.android.cursor.item/vnd.com.example.app.provider.category"
        else -> null
	}

    override fun insert(p0: Uri, p1: ContentValues?) = dpHelper?.let {
        val db = it.writableDatabase
        val uriReturn = when (uriMatcher.match(p0)) {
            bookDir, bookItem -> {
                val newBookId = dn.insert("Book", null, p1)
                Uri.parse("content://$authority/book/$newBookId")
            }
            categoryDir, categoryItem -> {
                val newCategoryId = dn.insert("Category", null, p1)
                Uri.parse("content://$authority/category/$newCategoryId")
            }
            else -> null
        }
        uriReturn
    }

    override fun delete(p0: Uri, p1: String?, p2: Array<out String>?)
    = dpHelper?.let {
        val db = it.writableDatabase
        val deleteRows = when (uriMatcher.match(p0)) {
            bookDir -> db.update("Book", p1, p2)
            bookItem -> {
                val bookId = p0.pathSegments[1]
                db.update("Book", "id=?", arrayOf(bookId),)
            }
            categoryDir -> db.update("Category", p1, p2)
            categoryItem -> {
                val categoryId = p0.pathSegments[1]
                db.update("Category","id=?", arrayOf(bookId),)
            }
            else -> 0
        }
        deleteRows
    } ?: 0

    override fun update(p0: Uri, p1: ContentValues?, p2: String?, p3: Array<out String>?) = dpHelper?.let {
        val db = it.writableDatabase
        val updateRows = when (uriMatcher.match(p0)) {
            bookDir -> db.update("Book", p1, p2, p3)
            bookItem -> {
                val bookId = p0.pathSegments[1]
                db.update("Book", p1, "id=?", arrayOf(bookId),)
            }
            categoryDir -> db.update("Category", p1, p2, p3)
            categoryItem -> {
                val categoryId = p0.pathSegments[1]
                db.update("Category", p1, "id=?", arrayOf(bookId),)
            }
            else -> 0
        }
        updateRows
    } ?: 0
}

相关知识:

Android中的Uri详解 - CSDN博客

ContentProvider详解 - 简书

内容提供程序基础知识 - Android 开发者

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-02-07 13:50:15  更:2022-02-07 13:51:00 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 14:20:58-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码