Android搜索本地应用、联系人、本地文件

效果演示:

列表采用一个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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容