Android 应用开发-实现将公共存储空间内的文件复制到应用的私用存储空间中

一、前言

几个月前,我用Android Studio给公司销售部门的同事开发了一款手机app,让同事们用自己的手机就能进行商品的扫码盘点操作,帮他们提高了工作效率,他们用了一段时间,反映还不错。不过前几天,销售部门的同事找到我,说近期公司新增了一些商品,用我的这款软件无法正常扫码这些新商品,希望我能解决问题。

  

这个问题的产生原因是,因为我的能力和资源有限,开发的这款手机app只是一个单机版的辅助工具。在开发时,商品信息是以Sqlit3数据库文件的形式保存在raw文件夹下,随代码一并打包在apk里,软件安装后第一次运行时,会将raw文件夹内的数据库文件导入到应用的私有存储空间内,等于是内嵌在软件中的。这个应用提供了一个手工添加商品信息的功能,但只能一个个手工添加,遇到大量新增的商品信息会很麻烦。这次新增了400多个商品信息,因此同事赶紧找我们解决这个问题。

这篇日志记录了解决问题的过程以备忘。

二、实现过程

要解决这个问题,最简单的方法就是将raw文件夹里的数据库文件更新,再重新打包成apk后,让同事重装一下。但使用这个软件的同事有好些个,有些同事觉得这样的话,以后要是更新商品就要重装一次应用,觉得不妥。希望我还是提供批量添加新商品信息的功能。

鉴于此,我决定为这个应用开发一个导入商品信息的功能。该功能要达到的目标是,在商品更新后,由与我对接的同事按我要求的模板将商品信息做成一个xls文件,然后通过通信软件(如钉钉、微信)将这个xls文件发送给其他的同事。其他同事在手机上下载这个xls文件,然后进入本应用的导入商品模块,选择这个xls文件,将文件内的数据导入的应用内数据库文件中。这样的好处是,以后的商品更新操作都可以由同事们自己完成,不用我插手了。我按这个思路在网上搜索了相关的知识,我希望使用第三方的poi库读取xls文件的数据内容(因为之前开发的将db文件中将数据导出为xls文件就是用的poi库),然后将获取到的数据更新到应用的私有空间内。网上这方面的内容还是比较容易找到,将搜索到的几篇文章中的知识点了解清楚后,依瓢画葫芦就完成了这个功能。通过真机调试,实现了从选定的数据功能。

(Android Studio连真机调试,成功将xls文件中的数据添加到数据库中)

但是在我将应用打包成apk发给同事进行测试时,在所有测试的同事手机上使用该功能导入数据时都出现了闪退现象,这让我很疑惑,之后我将apk文件安装到我的手机上,也出现了闪退,我将手机连接电脑,通过Android Studio检查错误信息,看到了如下的错误提示:

(执行导入商品操作时出现的错误提示)

通过对错误提示的分析,发现是执行语句“InputStream input = contentResolver.openInputStream(fileUri);”时出的错,获取到的对象为空。但是,我之后又通过Android Studio连真机执行“run”命令,用调试模式覆盖掉apk安装文件后,再测试又是正常的。反复进行安装apk和Android Studio连真机调试,都是安装apk后执行导入功能就闪退,调试模式下正常,这就很郁闷了。网上又找不要引起这个问题的原因,导致开发受阻。

但问题总要尽快解决,不然会让这款应用的使用感受严重下降。不得已我要另寻他法。

之后我转换一下思路,开发一个替换数据库文件的功能来解决问题。这个功能的要达到的目标是,在商品信息发生变化后,由与我对接的同事将新的商品信息提供给我,我做好db数据库文件,返回给他,由他通过通信软件(如钉钉、微信)将这个db文件发送给其他的同事。其他同事在手机上下载db文件,然后进入本应用的更新数据模块,选择这个db文件,将文件内容读取出来覆盖应用内原来的数据库文件,达到更新商品信息的目的。读取db文件不会用到poi库,有可能避开上面的问题。有了之前的开发经验,只需要对原来的代码做适当的修改就得到了新的功能,比之前的开发快了不少。在完成了新模块的开发后,再次进行真机调试和打包apk测试,均正常实现了从公共存储空间将db文件的内容复制到应用私有空间中,没有出现闪退问题。

 

(将db文件内容覆盖了原数据库)    (更新后可以查询到新增的商品信息)

这样做,就绕开了上面读取xls文件出错的问题,但我的同事不会做db数据库文件,还需要我插手,相比前面的方案要略逊一筹。但至少同事提出的批量更新商品信息到手机内嵌的数据库中的需求得到了解决。以后如果找到了前述问题的解决办法,再做处理。有时候遇到些问题并不一定是坏事,在解决问题的过程中,也能学到很多的知识

三、部分代码展示

更新数据功能模块xml文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="@color/gray_245"
    android:orientation="vertical"
    tools:context=".UpdateDbActivity">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/yellow_455"
        app:navigationIcon="@drawable/ic_back_b" >

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:textColor="@color/black"
            android:textSize="24sp"
            android:textStyle="bold"
            android:text="@string/GoodsInfoActivity_update" />

    </androidx.appcompat.widget.Toolbar>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical" >

        <Button
            android:id="@+id/btn_select"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginTop="20dp"
            android:text="@string/select" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:text="选择DB文件,才能更新商品信息数据。"
            android:textSize="16sp"  />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:padding="0dp"
            android:background="@drawable/shape_round_bg_white"
            android:orientation="vertical">
            <TextView
                android:id="@+id/tv_fileInfo"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:paddingStart="10dp"
                android:paddingEnd="10dp"
                android:layout_marginBottom="20dp"
                android:text="@string/fileInfo"
                android:textSize="16sp" />

            <TextView
                android:id="@+id/tv_fileName"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:paddingStart="10dp"
                android:paddingEnd="10dp"
                android:layout_marginBottom="20dp"
                android:text="@string/fileName"
                android:textSize="16sp" />

            <TextView
                android:id="@+id/tv_fileSize"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:paddingStart="10dp"
                android:paddingEnd="10dp"
                android:layout_marginBottom="20dp"
                android:text="@string/fileSize"
                android:textSize="16sp" />

        </LinearLayout>


        <Button
            android:id="@+id/btn_updateDB"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginBottom="20dp"
            android:text="@string/updateDB" />

        <TextView
            android:id="@+id/tv_tips"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginBottom="20dp"
            android:text="" />

    </LinearLayout>

</LinearLayout>

功能模块java文件

import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;

import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.provider.OpenableColumns;
import android.util.Log;
import android.view.View;
import android.webkit.MimeTypeMap;
import android.widget.TextView;
import android.widget.Toast;

import com.bahamutj.easyinventory.database.GoodsDbHelper;

import java.util.Locale;

public class UpdateDbActivity extends AppCompatActivity implements View.OnClickListener {
    private int state = 0;  // 状态,0-初始状态,1-已选择了文件,2-已完成导入,-1-异常状态
    private final static int DB_CODE = 1;
    private Uri fileUri;
    private TextView tv_fileInfo, tv_fileName,tv_fileSize, tv_tips;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_update_db);

        Toolbar toolbar = findViewById(R.id.toolbar);  // 工具栏
        toolbar.setTitle("");
        setSupportActionBar(toolbar);  // 要导入androidx.appcompat.widget.Toolbar 否则报错
        toolbar.setNavigationOnClickListener(view -> {
            finish(); // 结束当前页面
        });

        tv_fileInfo = findViewById(R.id.tv_fileInfo);
        tv_fileName = findViewById(R.id.tv_fileName);
        tv_fileSize = findViewById(R.id.tv_fileSize);
        tv_tips = findViewById(R.id.tv_tips);
        findViewById(R.id.btn_select).setOnClickListener(this);  // 选择文件按钮设置监听
        findViewById(R.id.btn_updateDB).setOnClickListener(this);  // 导入商品按钮设置监听
    }

    @Override
    public void onClick(View v) {
        int id = v.getId();
        if ( id == R.id.btn_select) {  // 选择DB文件
            this.tv_tips.setText("");
            // 打开Download目录Uri
            Uri uri = Uri.parse("content://com.android.externalstorage.documents/document/primary:Download");
            Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
            intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false); // 是否允许多选
            intent.addCategory(Intent.CATEGORY_OPENABLE);
            // 设置文件类型
            String[] mimetypes = {"application/octet-stream", "application/x-sqlite3","application/vnd.sqlite3", "application/x-trash"};
            intent.putExtra(Intent.EXTRA_MIME_TYPES, mimetypes);  // 设置文件格式
            intent.setType("*/*");
            intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri);
            startActivityForResult(intent, DB_CODE);
        } else if ( id == R.id.btn_updateDB ) {  // 更新数据
            if (this.state == 1) {
                this.tv_tips.setText("更新数据中...");
                // 更新数据库
                GoodsDbHelper dbHelper = new GoodsDbHelper(this);
                int result = dbHelper.copyDatabase(this, this.fileUri);
                if (result ==1) {
                    this.state = 2;  // 设置状态
                    this.tv_tips.setText("数据已完成更新。");
                } else {
                    this.tv_tips.setText("数据更新失败。");
                }
            } else if (this.state == 0) {
                Toast.makeText(this, "未选择文件", Toast.LENGTH_SHORT).show();
                this.tv_tips.setText("");
            } else if (this.state == 2) {
                Toast.makeText(this, "数据已更新,不能重复更新", Toast.LENGTH_SHORT).show();
                this.tv_tips.setText("");
            }
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK && data != null) {
            this.tv_tips.setText("");
            Uri uri = data.getData();  // 获取文件uri
            String uriString = uri.toString();
            // String type = getContentResolver().getType(uri);  // 获取到文件的类型
            // 检查文件的扩展名
            String extension = MimeTypeMap.getFileExtensionFromUrl(uriString);
            if (!extension.equals("db")) {
                Toast.makeText(this, "选择的不是db格式数据库文件", Toast.LENGTH_SHORT).show();
                Toast.makeText(this, "未获取到文件", Toast.LENGTH_SHORT).show();
                this.tv_fileInfo.setText("文件信息:");
                this.tv_fileName.setText("文件名称:");
                this.tv_fileSize.setText("文件大小:");
                this.state = -1;
            } else {
                String fileName;
                // 获取文件路径、文件名称和文件大小
                String fileInfo = uri.getPath();
                fileName = this.getFileName(this, uri);
                long fileSize = this.getFileSize(this, uri) / 1024;
                this.tv_fileInfo.setText(String.format(Locale.CHINESE, "%s%s", "文件信息:\n" , fileInfo));
                this.tv_fileName.setText(String.format(Locale.CHINESE, "%s%s", "文件名称:" , fileName));
                this.tv_fileSize.setText(String.format(Locale.CHINESE, "%s%d%s", "文件大小:", fileSize, "KB"));
                this.state = 1;  // 设置状态
                this.fileUri = uri;
            }
        } else {
            tv_fileInfo.setText("文件信息:");
            this.tv_fileName.setText("文件名称:");
            this.tv_fileSize.setText("文件大小:");
            this.state = 0;
        }
    }

    // 从uri获取文件的名称
    private String getFileName(Context context, Uri uri) {
        Cursor returnCursor = context.getContentResolver().query(uri, null, null, null, null);
        /*
         * Get the column indexes of the data in the Cursor,
         * move to the first row in the Cursor, get the data,
         * and close the Cursor.
         */
        int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
        returnCursor.moveToFirst();
        String name = (returnCursor.getString(nameIndex));
        returnCursor.close();
        return name;
    }

    // 从uri获取文件的大小
    private long getFileSize(Context context, Uri uri) {
        Cursor returnCursor = context.getContentResolver().query(uri, null, null, null, null);
        /*
         * Get the column indexes of the data in the Cursor,
         * move to the first row in the Cursor, get the data,
         * and close the Cursor.
         */
        int sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE);
        returnCursor.moveToFirst();
        long size = (returnCursor.getLong(sizeIndex));
        returnCursor.close();
        return size;
    }

}
GoodsDbHelper.java部分代码
public class GoodsDbHelper extends SQLiteOpenHelper {
    private static final String TAG = "GoodsDbHelper";
    private static final String DB_NAME = "easy_joy.db";
    public static final String TABLE_NAME = "tb_goods"; // 表的名称
    public static final String PACKAGE_NAME = "com.bahamutj.easyinventory";
    // 下面的设置,数据库的位置为(/data/data/com.bahamutj.easyinventory/databases/easy_joy.db)
    public static final String DB_PATH =  "/data"
            + Environment.getDataDirectory().getAbsolutePath() + "/" + PACKAGE_NAME
            + "/databases/";
    public static final String DB_FILE = DB_PATH + DB_NAME;
    private static final int DB_VERSION = 1;
    private SQLiteDatabase mDB = null;  // 数据库的实例
    public static GoodsDbHelper mHelper = null;  // 数据库帮助器的实例
    //private final Context mContext;

    public GoodsDbHelper(Context context) {
        super(context, DB_NAME, null, DB_VERSION);
        //mContext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        // 在这里可以创建数据库表格,如果不需要可留空
    }
    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
        // 在这里可以处理数据库升级的逻辑,如果不需要可留空
    }

    // 利用单例模式获取数据库帮助器的唯一实例,防止重复获取
    public static GoodsDbHelper getInstance(Context context) {
        if (mHelper == null) {
            mHelper = new GoodsDbHelper(context);
        }
        return mHelper;
    }

    // 省略......

    // 复制用户选择的数据库文件到应用的数据库路径中
    public int copyDatabase(Context mContext, Uri fileUri){
        int result = 0;
        ContentResolver contentResolver = mContext.getContentResolver();
        try {
            InputStream inputStream = contentResolver.openInputStream(fileUri);
            OutputStream outputStream = new FileOutputStream(mContext.getDatabasePath(DB_NAME));
            byte[] buffer = new byte[2048];  // 设置缓存大小
            int length;
            while ((length = inputStream.read(buffer)) > 0) {
                outputStream.write(buffer, 0, length);
            }
            outputStream.flush();
            outputStream.close();
            inputStream.close();
            result = 1;
        } catch (IOException e) {e.printStackTrace(); }
        return result;
    }

}    

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/629936.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

洗衣洗鞋店做小程序有什么优势?

互联网洗衣洗鞋小程序闪亮登场&#xff0c;想知道这款小程序有何魅力吗&#xff1f; 如今&#xff0c;众多商家纷纷推出预约上门洗鞋服务&#xff0c;&#x1f481;‍♀️并倾力打造洗鞋小程序&#xff0c;旨在拓展线上销售渠道。&#x1f31f;那么&#xff0c;这款洗鞋小程序究…

libsndfile读取wav文件基本属性

本文的目的是提供一种方法读取wav文件的基本属性&#xff1a;音频帧数&#xff0c;格式、通道数和采样率信息。 代码如下所示&#xff1a; #include <iostream> #include <QDebug> #include "sndfile.h"using namespace std;int main() {// 初始化 ALS…

Gradio

文章目录 关于 Gradio安装InterfaceChatInterface TextBlocksSentence BuilderDiff Texts MediaSepia FilterVideo IdentityIterative OutputGenerate Tone TabularFilter RecordsVideo IdentityIterative OutputGenerate Tone TabularFilter RecordsTranspose MatrixTax Calcu…

C++ 日志库 log4cpp 编译、压测及其范例代码 [全流程手工实践]

文章目录 一、 log4cpp官网二、下载三、编译1.目录结构如下2.configure 编译3.cmake 编译 四、测试五、压测源码及结果1.运行环境信息2.压测源码3.压测结果 文章内容&#xff1a;包含了对其linux上的完整使用流程&#xff0c;下载、编译、安装、测试用例尝试、以及一份自己写好…

20232906 2023-2024-2 《网络与系统攻防技术》第十次作业

20232906 2023-2024-2 《网络与系统攻防技术》第十次作业 1.实验内容 一、SEED SQL注入攻击与防御实验 我们已经创建了一个Web应用程序&#xff0c;并将其托管在http://www.seedlabsqlinjection.com/&#xff08;仅在SEED Ubuntu中可访问&#xff09;。该Web应用程序是一个简…

Qt多文档程序的一种实现

注&#xff1a;文中所列代码质量不高&#xff0c;但不影响演示我的思路 实现思路说明 实现DemoApplication 相当于MFC中CWinAppEx的派生类&#xff0c;暂时没加什么功能。 DemoApplication.h #pragma once#include <QtWidgets/QApplication>//相当于MFC中CWinAppEx的派生…

【Python报错】Python安装模块时报错Fatal error in launcher

【Python报错】Python安装模块时报错Fatal error in launcher 最近需要用到python下载一个小工具&#xff0c;自信敲下回车键本想看到黑乎乎的终端上会出现快速跳跃的命令代码&#xff0c;没想到&#xff0c;报错了...... Fatal error in launcher: Unable to create process …

外卖系统拦截器实现(Interceptor)

SpringMVC的拦截器主要是用于拦截控制器方法的执行&#xff1b; 概念&#xff1a;是一种动态拦截方法调用的机制&#xff0c;类似于过滤器。在Spring中动态拦截控制器中方法的执行。 作用&#xff1a;在指定的控制器中调用前后执行预先设定的代码&#xff0c;完成功能增强。 应…

day08|字符串题目part01

相关题目&#xff1a; ● 344.反转字符串 ● 541. 反转字符串II ● 卡码网&#xff1a;54.替换数字 ● 151.翻转字符串里的单词 ● 卡码网&#xff1a;55.右旋转字符串 344.反转字符串—双指针的应用 力扣链接 思路&#xff1a;创建两个指针分别指向头部和尾部&#xff0c;首…

【JavaEE进阶】 Bean的作用域与生命周期

文章目录 &#x1f343;Bean的作用域&#x1f6a9;作用域的使用&#x1f6a9;观察Bean的作用域&#x1f388;单例作用域&#x1f388;多例作用域&#x1f388;请求作用域&#x1f388;会话作⽤域&#x1f388;Application作⽤域 &#x1f384;Bean的⽣命周期⭕总结 &#x1f34…

Linux 第三十三章

&#x1f436;博主主页&#xff1a;ᰔᩚ. 一怀明月ꦿ ❤️‍&#x1f525;专栏系列&#xff1a;线性代数&#xff0c;C初学者入门训练&#xff0c;题解C&#xff0c;C的使用文章&#xff0c;「初学」C&#xff0c;linux &#x1f525;座右铭&#xff1a;“不要等到什么都没有了…

rocketmq的存储和检索

messageId是rocketmq自动生成的。

JSP+SQL学生成绩管理系统

Java版本&#xff1a;1.8 数据库&#xff1a;MySQL 框架&#xff1a;Spring Spring MVC MyBatis 服务器&#xff1a;Tomcat 前端解析框架&#xff1a;Thymeleaf 开发工具&#xff1a;Idea 2017 版本管理工具&#xff1a;Maven 版本控制工具&#xff1a;GitHub 经过对系统的需…

国际化日期(inti)

我们可以使用国际化API自动的格式化数字或者日期&#xff0c;并且格式化日期或数字的时候是按照各个国家的习惯来进行格式化的&#xff0c;非常的简单&#xff1b; const now new Date(); labelDate.textContent new Intl.DateTimeFormat(zh-CN).format(now);比如说这是按照…

link.click()时浏览器报错The file at ‘data:image/png;base64,iVBORw

代码如下&#xff1a; const dataURL canvas.toDataURL({format: "png",width: 400,height: 400, });const link document.createElement("a"); link.download new Date().getTime();link.href dataURL; document.body.appendChild(link); link.click…

【考研数学】准备开强化,更「张宇」还是「武忠祥」?

数一125学长前来回答&#xff0c;选择哪位老师的课程&#xff0c;这通常取决于你的个人偏好和学习风格&#xff01; 张宇老师和武忠祥老师都是非常有经验的数学老师&#xff0c;他们的教学方法各有特点。 张宇老师的教学风格通常被认为是通俗易懂&#xff0c;善于将复杂的概念…

x264 帧类型代价计算原理:slicetype_mb_cost 函数分析

slicetype_mb_cost 函数 函数功能 计算每个宏块 MB 的代价 cost。函数参数分析 x264_t *h:全局编码结构体x264_mb_analysis_t *a:宏块分析结构体x264_frame_t **frames:系列帧数据结构体int p0:帧序号之一,一般指向靠前帧int p1:帧序号之一,一般指向靠后帧int b:帧标志…

《系统架构设计师教程(第2版)》第4章-信息安全技术基础知识-02-信息加密技术

文章目录 1. 信息加密技术1.1 数据加密1.2 对称密钥加密算法1&#xff09;数据加密标准&#xff08;DES)2&#xff09;三重DES&#xff08;Triple-DES&#xff09;3&#xff09;国际数据加密算法&#xff08;IDEA&#xff09;4&#xff09;高级加密标准&#xff08;AES&#xf…

【C -> Cpp】由C迈向Cpp (6):静态、友元和内部类

标题&#xff1a;【C -&#xff1e; Cpp】由C迈向Cpp &#xff08;6&#xff09;&#xff1a;静态、友元和内部类 水墨不写bug &#xff08;图片来源于网络&#xff09; 目录 &#xff08;一&#xff09;静态成员 &#xff08;二&#xff09;友元 &#xff08;三&#xff09…

Zynq UltraScale+ RFSoC 配置存储器器件

Zynq UltraScale RFSoC 配置存储器器件 下表所示闪存器件支持通过 Vivado 软件对 Zynq UltraScale RFSoC 器件执行擦除、空白检查、编程和验证等配置操 作。 本附录中的表格所列赛灵思系列非易失性存储器将不断保持更新 &#xff0c; 并支持通过 Vivado 软件对其中所列…