Android搜索本地应用、联系人、本地文件
2024-04-09 19:00:57  阅读数 2183
效果演示:

列表采用一个recyclerview实现,定义一个公共父实体类,定义基础属性,各个item类型的实体类拥有自己的属性,继承公共父实体类。然后adapter采用多itemType,多viewHolder处理。

Adapter代码:
class MutipleAdapter(val context: Context): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    private var itemList = CopyOnWriteArrayList<AdapterItem>()

    fun appendDatas(datas: List<AdapterItem>): MutipleAdapter {
        this.itemList.addAll(datas)
        notifyDataSetChanged()
        return this
    }

    fun appendData(data: AdapterItem): MutipleAdapter {
        this.itemList.add(data)
        return this
    }

    fun clearData() {
        itemList.clear()
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return if (viewType == ITEM_TYPE_FILE) {
            ViewHolderFile(ItemFileBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        } else if (viewType == ITEM_TYPE_CONTACT) {
            ViewHolderContact(ItemContactBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        } else if (viewType == ITEM_TYPE_App) {
            ViewHolderApp(ItemAppBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        } else {
            ViewHolderHeader(ItemHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        }
    }

    override fun getItemCount() = itemList.size

    override fun getItemViewType(position: Int): Int {
        return itemList[position].itemType
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        var itemData = itemList[position]
        if (holder is ViewHolderHeader) {
            holder.bind(itemData)
        } else if (holder is ViewHolderFile) {
            holder.bind(itemData)
        } else if (holder is ViewHolderContact) {
            holder.bind(itemData)
        } else if (holder is ViewHolderApp) {
            holder.bind(itemData)
        }
    }

    inner class ViewHolderHeader(val binding: ItemHeaderBinding) : RecyclerView.ViewHolder(binding.root) {

        fun bind(item: AdapterItem) {
            binding.tvHeaderTitle.text = item.headerTitle
        }
    }

    inner class ViewHolderFile(val binding: ItemFileBinding) : RecyclerView.ViewHolder(binding.root) {

        fun bind(item: AdapterItem) {
            if (item is FileBean) {
                binding.tvFileName.text = item.fileName
                binding.tvFileInfo.text = "" + (item.fileSize/1000) + "KB"

                setFileIcon(binding.ivIcon, item.fileName)
            }
        }
    }

    inner class ViewHolderContact(val binding: ItemContactBinding) : RecyclerView.ViewHolder(binding.root) {

        fun bind(item: AdapterItem) {
            if (item is ContactBean) {
                binding.tvName.text = item.name
                binding.tvNumber.text = item.number
                binding.tvCall.setOnClickListener {
                    callPhone(item.number)
                }
            }
        }
    }

    inner class ViewHolderApp(val binding: ItemAppBinding) : RecyclerView.ViewHolder(binding.root) {

        fun bind(item: AdapterItem) {
            if (item is AppBean) {
                binding.tvAppName.text = item.name
                item.intent?.let {
                    binding.ivIcon.background = context.packageManager.getActivityIcon(it)
                }
            }
        }
    }

    /**
     * 根据文件后缀类型设置对应类型图标
     */
    private fun setFileIcon(imageView: ImageView, fileName: String) {
        if (fileName.endsWith(".jpg") || fileName.endsWith(".mp4")) {
            imageView.background = context.resources.getDrawable(R.drawable.category_file_icon_pic_phone)
        } else {
            var drawableId = 0
            if (fileName.endsWith(".txt") || fileName.endsWith(".pdf")) {
                drawableId = R.drawable.category_file_icon_doc_phone
            } else if (fileName.endsWith(".zip")) {
                drawableId = R.drawable.category_file_icon_zip_phone
            } else if (fileName.endsWith(".mp3")) {
                drawableId = R.drawable.category_file_icon_music_phone
            } else if (fileName.endsWith(".apk")) {
                drawableId = R.drawable.category_file_icon_apk_phone
            } else {
                drawableId = R.drawable.ic_local_file
            }
            imageView.background = context.resources.getDrawable(drawableId)
        }
    }

    /**
     * 拨打电话
     */
    fun callPhone(phoneNumber: String) {
        var intent = Intent(Intent.ACTION_CALL)
        var data = Uri.parse("tel:$phoneNumber")
        intent.data = data
        context.startActivity(intent)
    }

}

各个实体类代码:

open class AdapterItem(var itemType: Int = 0, val headerTitle: String = "")

class AppBean(val pkg: String, val icon: Int, val name: String, val intent: Intent?): AdapterItem() {

    init {
        itemType = ItemType.ITEM_TYPE_App
    }
}

class ContactBean(val name: String, val number: String): AdapterItem() {

    init {
        itemType = ItemType.ITEM_TYPE_CONTACT
    }
}

data class FileBean(val fileName: String, val path: String, val fileSize: Int): AdapterItem() {

    init {
        itemType = ITEM_TYPE_FILE
    }
}

获取本地应用:

object SearchAppProvider {
    fun searchInstallApps(context: Context): List<AppBean>? {
        return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            checkInstallAppsBeforeL(context)
        } else {
            checkInstallAppsAfterL(context)
        }
    }

    private fun checkInstallAppsBeforeL(context: Context): List<AppBean> {
        val apps: MutableList<AppBean> = ArrayList()
        val pm = context.packageManager
        try {
            val packageInfos = pm.getInstalledPackages(0)
            for (i in packageInfos.indices) {
                val pkgInfo = packageInfos[i]
                val AppBean = pkgInfo.applicationInfo
                if (TextUtils.equals(context.packageName, pkgInfo.packageName)) continue
                val intent = getLaunchIntent(pm, pkgInfo.packageName)
                intent!!.flags =
                    Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED or Intent.FLAG_ACTIVITY_CLEAR_TOP
                val app = AppBean(
                    pkgInfo.packageName,
                    AppBean.icon,
                    AppBean.loadLabel(pm).toString(),
                    intent
                )
                apps.add(app)
            }
        } catch (e: Exception) {
            //
        }


        return apps
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private fun checkInstallAppsAfterL(context: Context): List<AppBean>? {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return null
        val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
            ?: return null
        val apps: MutableList<AppBean> = ArrayList()
        try {
            val activityInfos = launcherApps.getActivityList(null, Process.myUserHandle())
            for (activityInfo in activityInfos) {
                val AppBean = activityInfo.applicationInfo
                val intent = Intent(Intent.ACTION_MAIN)
                intent.addCategory(Intent.CATEGORY_LAUNCHER)
                intent.setPackage(AppBean.packageName)
                intent.component = activityInfo.componentName
                intent.flags =
                    Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED or Intent.FLAG_ACTIVITY_CLEAR_TOP
                val app = AppBean(
                    AppBean.packageName,
                    AppBean.icon,
                    activityInfo.label.toString(),
                    intent
                )
                apps.add(app)
            }
        } catch (e: Exception) {
        }
        return apps
    }

    private fun getLaunchIntent(pm: PackageManager, pkg: String): Intent? {
        var intent = pm.getLaunchIntentForPackage(pkg)
        return if (intent != null) {
            intent
        } else {
            intent = Intent(Intent.ACTION_MAIN)
            intent.addCategory(Intent.CATEGORY_LAUNCHER)
            intent.setPackage(pkg)
            val apps = pm.queryIntentActivities(intent, 0)
            if (apps == null || apps.isEmpty()) {
                return null
            }
            val ri = apps.iterator().next() ?: return null
            intent.component = ComponentName(pkg, ri.activityInfo.name)
            intent
        }
    }
}

模糊查询联系人:

object SearchContactProvider {

    @SuppressLint("Range")
    fun readContacts(context: Context) {
        //ContactsContract.CommonDataKinds.Phone 联系人表
        var cursor: Cursor? = context.contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
            null, null, null, null)
        cursor?.let {
            while (it.moveToNext()) {
                //读取通讯录的姓名
                var name = it.getString(it.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
                //读取通讯录的号码
                var number = cursor.getString(it.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
                Log.i("minfo", "$name--$number")
            }
        }
    }

    /**
     * 模糊查询联系人
     */
    @SuppressLint("Range")
    fun searchContact(context: Context, key: String): List<ContactBean> {
        //ContactsContract.CommonDataKinds.Phone 联系人表
        var list = ArrayList<ContactBean>()
        val projection = arrayOf(
            ContactsContract.PhoneLookup.DISPLAY_NAME,
            ContactsContract.CommonDataKinds.Phone.NUMBER
        )
        val selection = StringBuilder()
        selection.append(ContactsContract.Contacts.DISPLAY_NAME)
        selection.append(" LIKE '%$key%' ")
        var cursor: Cursor? = context.contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
            projection, selection.toString(), null, null)
        cursor?.let {
            while (it.moveToNext()) {
                //读取通讯录的姓名
                var name = it.getString(it.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
                //读取通讯录的号码
                var number = cursor.getString(it.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
                Log.i("minfo", "$name--$number")
                list.add(ContactBean(name, number))
            }
            it.close()
        }
        return list
    }
}

模糊查询本地文件:

/**
 * 使用contentResolver查询本地各种文件
 */
object SearchFileProvider {
    private const val MAX_FILE_COUNT = 20

    /**
     * 模糊查询本地文件
     */
    suspend fun searchLocalFile(context: Context, key: String): List<FileBean> {
        var list = ArrayList<FileBean>()
        val volumeName = "external"
        val columns = arrayOf(MediaStore.Files.FileColumns.DATA)
        val selection = MediaStore.Files.FileColumns.DATA + " LIKE '%$key%.mp3' OR " +
                MediaStore.Files.FileColumns.DATA + " LIKE '%$key%.json' OR " +
                MediaStore.Files.FileColumns.DATA + " LIKE '%$key%.log' OR " +
                MediaStore.Files.FileColumns.DATA + " LIKE '%$key%.apk' OR " +
                MediaStore.Files.FileColumns.DATA + " LIKE '%$key%.mp4' OR " +
                MediaStore.Files.FileColumns.DATA + " LIKE '%$key%.pdf' OR " +
                MediaStore.Files.FileColumns.DATA + " LIKE '%$key%.txt' OR " +
                MediaStore.Files.FileColumns.DATA + " LIKE '%$key%.jpg' OR " +
                MediaStore.Files.FileColumns.DATA + " LIKE '%$key%.zip'"
        var cursor: Cursor? = null
        try {
            cursor = context.contentResolver.query(
                MediaStore.Files.getContentUri(volumeName),
                columns,
                selection,
                null,
                null
            )
            if (cursor != null) {
                while (cursor.moveToNext()) {
                    if (list.size < MAX_FILE_COUNT) {
                        val absolutePath = cursor.getString(0)
                        File(absolutePath).apply {
                            if (exists() && !TextUtils.isEmpty(name) && name.contains(".")) {
                                if (!TextUtils.isEmpty(name)) {
                                    var bean = FileBean(name, path, readBytes().size)
                                    list.add(bean)
                                }
                            }
                        }
                    } else {
                        return list
                    }
                }
            }
        } catch (e: java.lang.Exception) {
            e.printStackTrace()
        } finally {
            try {
                if (cursor != null) {
                    cursor.close()
                    cursor = null
                }
            } catch (e: java.lang.Exception) {
            }
        }
        return list
    }
}

异步加载各类数据,再使用async/await同步使用数据结果

    /**
     * 搜索各类数据
     */
    private fun loadData(key: String) {
        adapter.clearData()

        //搜索本地App
        val localAppsDeferred = GlobalScope.async(Dispatchers.IO) {
            SearchAppProvider.searchInstallApps(applicationContext)
        }

        //搜索联系人
        val contactsDeferred = GlobalScope.async(Dispatchers.IO) {
            SearchContactProvider.searchContact(applicationContext, key)
        }

        //搜索本地文件
        val localFilesDeferred = GlobalScope.async(Dispatchers.IO) {
            SearchFileProvider.searchLocalFile(applicationContext, key)
        }

        GlobalScope.launch {
            // 通过 await 获取异步任务的结果
            val localApps = localAppsDeferred.await()
            val contacts = contactsDeferred.await()
            val localFiles = localFilesDeferred.await()

            withContext(Dispatchers.Main) {
                adapter.appendData(AdapterItem(0, "本机应用"))
                adapter.appendDatas(localApps!!.take(10))

                adapter.appendData(AdapterItem(0, "联系人"))
                    .appendDatas(contacts)

                adapter.appendData(AdapterItem(0, "文件管理")).appendDatas(localFiles)  //先添加内容的header,再添加内容
            }
        }
    }

然后,需要能够访问文件,别忘了加上6.0访问权限,获取本地文件、读取联系人访问的权限。并且注意,在targetsdk 29及以下,可以访问所有问题,高于29,则只能够访问到图片,视频,音乐这样的多媒体文件。


        requestPermissions(arrayOf(Manifest.permission.READ_CONTACTS,
            Manifest.permission.CALL_PHONE, Manifest.permission.WRITE_EXTERNAL_STORAGE), this)

Github 代码地址:

https://github.com/running-libo/SearchLocalData