当前位置:   article > 正文

Android基础教程——从入门到精通(上)_安卓教程

安卓教程
  • 本文是对B站教程 动脑学院 Android教程 学习过程中所做的笔记。
  • 文章分为上下两部分,此文是上部分,下部分链接为:Android基础教程——从入门到精通(下)
  • 源视频教程并没有录制全,本文还补充了 Service 和 网络通信 的内容
  • 文章介绍详细,示例代码丰富,相信跟着本教程可以打下很好的Android基础。

文章目录

一、开发环境搭建

二、简单控件

1. 文本显示

2. 按钮

3. 常用布局

(1)线性布局LinearLayout

(3)相对布局RelativeLayout

(3)网格布局GridLayout

4. 图像显示

(1)图像的缩放问题:

(2)图像按钮ImageButton:

三、Activity

1. Activity的启动和结束

2. Activity的生命周期

3. Activity的启动模式

(1)静态设置

(2)动态设置

4. Activity之间传递信息

(1)显式Intent和隐式Intent

(2)向下一个Activity发送消息:

(3)向上一个Activity返回消息:

5. Activity获取一些附加信息

(1)获取资源信息:

(2)获取元数据信息:

四、数据存储

1. 共享参数SharedPreferences

(1)使用:

(2)应用实例:记住密码功能

2. 数据库SQLite

(1)SQLiteDatabase

(2)SQLiteOpenHelper

(3)代码举例:

(4)优化记住密码:

3. 存储卡

(1)私有空间和公有空间

(2)在存储卡上读写文件

(3)在存储卡上读写 图片文件

4. 应用组件Application

5. 实战:购物车

五、内容共享

1. 在应用之间共享数据

2. 使用内容组件获取通讯信息

(1)运行时动态申请权限

(2)使用ContentResolver读写联系人

(3)使用ContentObserver监听短信

3. 在应用之间共享文件

(1)使用相册图片发送彩信

(2)借助FileProvider发送彩信

(3)借助FileProvider安装应用


一、开发环境搭建

  1. 安装android studio
  2. 安装 sdk(当前使用最新版33)

image-20230105224008976

  1. 手动下载gradle

    (更新:弄完之后有时候没用,可以再试试挂梯子,换网络之类的)

    如果第一次启动AndroidStudio没有报错则无需设置,这里是因为我启动完之后下载gradle报错:

  2. mac环境下安装Gradle及配置_gradle mac_慕城南风的博客-CSDN博客https://blog.csdn.net/lovedingd/article/details/123337180

  3. androidStudio配置:可能是网络问题连接不到,所以手动下载。

  4. 日志打印

二、简单控件 

项目说明:

 gradle修改

 名称修改

1. 文本显示

设置文本内容有两种方式:

  • 在 XML 文件中通过属性 android:text 设置文本
  • 在 Java 代码中调用文本视图对象的 setText 方法设置文本

引用字符串资源:

  • 在XML文件中引用(@string/xxx)
  • 在Java代码中引用(R.string.xxx)

其余设置文本字体大小,颜色等都是可以通过关键词+代码提示很容易就能知道怎么写,这里就不赘述。

2. 按钮

Button继承于TextView,因此它们拥有的属性都是共通的。

除此之外,Button最重要的是点击事件。

  • 点击监听器:通过setOnClickListener方法设置。按钮被按住少于500毫秒时,会触发点击事件。

  • 长按监听器:通过setOnLongClickListener方法设置。按钮被按住超过500毫秒时,会触发长按事件。

  1. Button btn = findViewById(R.id.btnMain);
  2. btn.setOnClickListener(new View.OnClickListener() {
  3. @Override
  4. public void onClick(View view) {
  5. Intent intent = new Intent();
  6. intent.setClass(MainActivity.this, MainActivity2.class);
  7. startActivity(intent);
  8. }
  9. });

 

#31-ImageView 图片

 

3. 常用布局

(1)线性布局LinearLayout

特点:要不水平排列,要不竖直排列,通过orintation进行设置(horiztal为水平,vertical为竖直)

权重属性:通过layout_weight来设置,在线性布局的直接下级进行设置,表示该下级布局占据的宽高比例。

  • layout_width填0dp时,layout_weight表示水平方向的宽度比例。
  • layout_height填0dp时,layout_weight表示垂直方向的高度比例。

(2)相对布局RelativeLayout

相对布局中的视图位置由两个因素所影响:

  • 与该视图平级的其他视图
  • 上级视图(也就是它归属的RelativeLayout)

相对位置的一些取值:

 

image-20230107003412969

(3)网格布局GridLayout

顾名思义该布局适用于表格类型的布局。

 (4) 滚动视图 ScrollView

4. 图像显示

图片一般放在res/drawable目录下,设置图像显示一般有两种方法:

  • 在XML文件中,通过属性android:src设置图片资源,属性值格式形如 @drawable/不含扩展名的图片名称。
  • 在Java代码中,调用setImageResource方法设置图片资源,方法参数格式形如 R.drawable.不含扩展名的图片名称。

(1)图像的缩放问题:

ImageView本身默认图片居中显示,若要改变图片的显示方式,可通过scaleType属性设定,该属性的取值说明如下:

image-20230107004159846

(2)图像按钮ImageButton

ImageButton是显示图片的图像按钮,但它继承自ImageView,而非继承Button。

ImageButton和Button之间的区别有:

  • Button既可显示文本也可显示图片,ImageButton只能显示图片不能显示文本。
  • ImageButton上的图像可按比例缩放,而Button通过背景设置的图像会拉伸变形。
  • Button只能靠背景显示一张图片,而ImageButton可分别在前景和背景显示图片,从而实现两张图片叠加的效果。

三、Activity

Activity是安卓开发四大组件之一,非常重要。

1. Activity的启动和结束

Activity的启动这里指的是跳转,从一个页面跳转到一个新的页面,就相当于启动了一个新的页面。

示例:

  1. bt.setOnClickListener(new View.OnClickListener(){
  2. @Override
  3. public void onClick(View v) {
  4. Intent intent = new Intent();
  5. intent.setClass(MainActivity.this, MainActivity2.class);
  6. startActivity(intent);
  7. }
  8. });

结束Activity:调用 finish()

2. Activity的生命周期

onCreate:此时将页面布局加载到内存中,初始化页面。

onStart:将页面展示在屏幕。

onResume:此时页面能够和用户进行交互。

onPause:页面进入暂停状态,无法和用户进行交互。

onStop:页面不在屏幕显示。

onDestory:回收Activity占用的资源,彻底销毁该Activity。

onRestart:onStop状态可以转为onRestart状态。

onNewIntent:重用已存在的活动实例。如果一个Activity已经启动了,并且存在与当前栈,而当前栈的启动模式为SingleTask,SingleInstance,SingleTop(此时在任务栈顶端),那么再次启动该Activity的话,并不会重新进行onCreate,而是会执行onNewIntent方法。

image-20230108223537951

 

 

3. Activity的启动模式

Android允许在创建Activity时设置启动模式,通过启动模式控制Activity的出入栈行为。

(1)静态设置

设置方式:打开AndroidManifest.xml文件,给activity添加属性android:launchMode。如以下表示该activity使用standard标准模式,默认也是标准模式。

<activity android:name=".JumpFirstActivity" android:launchMode="standard" />

launchMode的取值有:

image-20230109161545002

image-20230109161736927

image-20230109161750015

image-20230109161845199

image-20230109161930718

(2)动态设置

通过 Intent 动态设置 Activity启动模式:

intent.setFlags();

 

4. Activity之间传递信息

Intent能够让Android各组件之间进行沟通。

Intent可以完成3部分工作:

  • 表明本次通信从哪里来,往哪里走,要怎么走。
  • 发送方可以携带消息给接收方,接收方可以从收到的Intent解析数据。
  • 发送方如果想要知道接收方的处理结果,接收方也可以通过Intent返回结果。

Intent的一些组成元素:

image-20230109163305363

(1)显式Intent和隐式Intent

1. 显式Intent

创建方式:

  • 在Intent的构造函数中指定:

    Intent intent = new Intent(this, NextActivity.class);
    
  • 调用setClass指定:

    1. Intent intent = new Intent();
    2. intent.setClass(this, NextActivity.class);
  • 调用setComponent指定:

    1. Intent intent = new Intent();
    2. ComponentName component = new ComponentName(this, NextActivity.class);
    3. intent.setComponent(component);

2. 隐式Intent:

没有明确指定所要跳转的页面,而是通过一些动作字符串来让系统自动匹配。

通常是App不想向外暴露Activity的名称,只给出一些定义好的字符串。这些字符串可以自己定义,也有系统定义的。

常见的系统动作如下:

image-20230109224228884

下面以调用系统拨号页面举例:

  1. String phone = "12345";
  2. Intent intent = new Intent();
  3. //这里表示设置意图动作为准备拨号
  4. intent.setAction(Intent.ACTION_DIAL);
  5. intent.setData(Uri.parse("tel:" + phone));
  6. startActivity(intent);

如果想要跳转到自己定义的activity:

步骤一:在AndroidManifest.xml找到该activity,添加action和category标签,同时设置exported为true,表示允许被其他activity调用。

image-20230109230403488

步骤二:调用过程和上面一样:

  1. Intent intent = new Intent();
  2. intent.setAction("android.intent.action.activity2");
  3. intent.addCategory(Intent.CATEGORY_DEFAULT);
  4. startActivity(intent);

(2)向下一个Activity发送消息:

Intent重载了很多putExtra方法用于传递各种类型的信息,包括整数类型,字符串等。但是显然通过调用putExtra方法会很不好管理,因为数据都是零碎传递。所以Android引入了Bundle,其内部是一个Map,使用起来也和Map一样。

image-20230109165406124

示例:

  1. Intent intent = new Intent(this, NextActivity.class);
  2. //通过bundle包装数据
  3. Bundle bundle = new Bundle();
  4. bundle.putString("stringKey", "stringValue");
  5. intent.putExtras(bundle);
  6. startActivity(intent);

然后下一个Activity就可以通过intent获取到所想要的数据了: 

  1. Bundle bundle = getIntent().getExtras();
  2. String stringValue = bundle.getString("stringKey");

(3)向上一个Activity返回消息:

上一个页面跳转到下一个页面,同时携带数据:

  1. private ActivityResultLauncher<Intent> register;
  2. @Override
  3. protected void onCreate(@Nullable Bundle savedInstanceState) {
  4. super.onCreate(savedInstanceState);
  5. setContentView(R.layout.activity_main2);
  6. findViewById(R.id.bt).setOnClickListener(this);
  7. //回调函数,返回到这个页面时所执行的程序
  8. register = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback<ActivityResult>() {
  9. //回调函数
  10. @Override
  11. public void onActivityResult(ActivityResult result) {
  12. if (result != null) {
  13. Intent intent = result.getData();
  14. if (intent != null && result.getResultCode() == Activity.RESULT_OK) {
  15. //获取到返回的数据
  16. Bundle bundle = intent.getExtras();
  17. //...
  18. }
  19. }
  20. }
  21. });
  22. }
  23. @Override
  24. public void onClick(View v) {
  25. Intent intent = new Intent(this, MainActivity3.class);
  26. //跳转下一页面
  27. register.launch(intent);
  28. }

 

下一个页面接受到数据,处理之后返回结果给上一个页面:

  1. Bundle bundle = getIntent().getExtras();
  2. //...页面进行处理
  3. //返回数据给上一个页面
  4. Bundle bundle = new Bundle();
  5. bundle.putString("stringKey", "stringValue");
  6. intent.putExtras(bundle);
  7. setResult(Activity.RESULT_OK, intent);
  8. finish();

  

5. Activity获取一些附加信息

(1)获取资源信息:

  1. //获取strings.xml中的字符串资源
  2. String text = getString(R.string.text);
  3. //获取color.xml中的颜色资源
  4. int black = getColor(R.color.black);

(2)获取元数据信息:

  1. try {
  2. //获取包管理器
  3. PackageManager pm = getPackageManager();
  4. //获取当前的Activity信息
  5. ActivityInfo activityInfo = pm.getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
  6. Bundle bundle = activityInfo.metaData;
  7. String text2 = bundle.getString("text2");
  8. } catch (PackageManager.NameNotFoundException e) {
  9. e.printStackTrace();
  10. }

6.快捷菜单

 

 

中级控件 图形Drawable

 单选按钮RadioButton

 编辑框EditText

 提醒对话框AlertDialog

 

 日期对话框DatePickerDialog

时间对话框TimePickerDialog 

四、数据存储

1. 共享参数SharedPreferences

(1)使用:

sharedPreferences是安卓的一个轻量级存储工具,采用的方式是key-value,以xml文件形式存在,文件路径为/data/data/应用包名/shared_prefs/文件名.xml。


适合场景:

  1. 简单且孤立的数据
  2. 文本数据,二进制数据则不合适
  3. 需要持久化的数据,也就是重启APP后数据仍然存在且有效。

实际开发中,sharedPreferences经常用来存储的数据有:APP的个性化配置信息,用户使用APP的行为信息等。

sharedPreferences对数据的存储和读取类似Map,提供put和set方法。

获取数据可以通过SharedPreferences对象获取:

  1. //第一个参数表示文件名,第二个参数表示私有模式
  2. SharedPreferences shared = getSharedPreferences("fileName", MODE_PRIVATE);
  3. String name = shared.getString("name");

而存储数据则还需要借助Editor类:

  1. SharedPreferences.Editor editor = shared.edit();
  2. editor.putString("name", "oymn");
  3. editor.putInt("age", 20);
  4. editor.commit();

(2)应用实例:记住密码功能

  1. 声明一个共享参数对象,并在onCreate中调用getSharedPreferences方法获取共享参数的实例。
  2. 登录成功时,如果用户勾选了“记住密码”,就使用共享参数保存手机号码与密码。

所以在登录页面的onCreat方法中添加获取共享参数的代码:

  1. // 从share_login.xml获取共享参数对象
  2. mShared = getSharedPreferences("share_login", MODE_PRIVATE);
  3. // 获取共享参数保存的手机号码
  4. String phone = mShared.getString("phone", "");
  5. // 获取共享参数保存的密码
  6. String password = mShared.getString("password", "");
  7. et_phone.setText(phone); // 往手机号码编辑框填写上次保存的手机号
  8. et_password.setText(password); // 往密码编辑框填写上次保存的密码

接着在登录成功方法中添加保存功能:

  1. // 如果勾选了“记住密码”,就把手机号码和密码都保存到共享参数中
  2. if (isRemember) {
  3. SharedPreferences.Editor editor = mShared.edit(); // 获得编辑器的对象
  4. editor.putString("phone", et_phone.getText().toString()); // 添加名叫phone的手机号码
  5. editor.putString("password", et_password.getText().toString()); // 添加名叫password的密码
  6. editor.commit(); // 提交编辑器中的修改
  7. }

2. 数据库SQLite

SQLite是安卓的一种小巧的嵌入式数据库,基本使用和思路和Mysql无异。

(1)SQLiteDatabase

java代码层面借助SQLiteDatabase来对SQLite进行操作。

  1. //创建数据库text.db
  2. SQLiteDatabase db = openOrCreateDatabase(getFileDir() + "/test.db", Context.MODE_PRIVATE, null);

image-20230112213636282

(2)SQLiteOpenHelper

由于SQLiteDatabase存在局限性,一不小心就会重复打开数据库,处理数据库的升级也不方便;因此Android提供了数据库帮助器SQLiteOpenHelper,帮助开发者合理使用SQLite。

SQLiteOpenHelper的具体使用步骤如下:

  • 步骤一,新建一个继承自SQLiteOpenHelper的数据库操作类,按提示重写onCreate和onUpgrade两个方法。其中,onCreate方法只在第一次打开数据库时执行,在此可以创建表结构;而onUpgrade方法在数据库版本升高时执行,在此可以根据新旧版本号变更表结构。
  • 步骤二,为保证数据库安全使用,需要封装几个必要方法,包括获取单例对象、打开数据库连接、关闭数据库连接,说明如下:
    • 获取单例对象:确保在App运行过程中数据库只会打开一次,避免重复打开引起错误。
    • 打开数据库连接:SQLite有锁机制,即读锁和写锁的处理;故而数据库连接也分两种,读连接可调用getReadableDatabase方法获得,写连接可调用getWritableDatabase获得。
    • 关闭数据库连接:数据库操作完毕,调用数据库实例的close方法关闭连接。
  • 步骤三, 提供对表记录增加、删除、修改、查询的操作方法。能被SQLite直接使用的数据结构是ContentValues类,它类似于映射Map,也提供了put和get方法存取键值对。
    • 区别之处在于:ContentValues的键只能是字符串,不能是其他类型。ContentValues主要用于增加记录和更新记录,对应数据库的insert和update方法。
    • 记录的查询操作用到了游标类Cursor,调用query和rawQuery方法返回的都是Cursor对象,若要获取全部的查询结果,则需根据游标的指示一条一条遍历结果集合。Cursor的常用方法可分为3类,说明如下:

image-20230112214321301

(3)代码举例:

  1. public class UserDBHelper extends SQLiteOpenHelper {
  2. private static final String DB_NAME = "user.db"; //数据库名称
  3. private static final int DB_VERSION = 1; //数据库的版本号
  4. private static UserDBHelper helper = null; //单例
  5. private SQLiteDatabase sdb = null; //数据库实例
  6. public static final String TABLE_NAME = "user_info"; //表名
  7. public UserDBHelper(Context context) {
  8. super(context, DB_NAME, null, DB_VERSION);
  9. }
  10. public UserDBHelper(Context context, int version) {
  11. super(context, DB_NAME, null, version);
  12. }
  13. //通过单例模式获取 UserDBHelper 的唯一实例
  14. public static synchronized UserDBHelper getInstance(Context context, int version) {
  15. if (version > 0 && helper == null) {
  16. helper = new UserDBHelper(context, version);
  17. } else if (helper == null) {
  18. helper = new UserDBHelper(context);
  19. }
  20. return helper;
  21. }
  22. //打开读连接
  23. public SQLiteDatabase openReadLink() {
  24. if (sdb == null || !sdb.isOpen()) {
  25. sdb = helper.getReadableDatabase();
  26. }
  27. return sdb;
  28. }
  29. //打开写连接
  30. public SQLiteDatabase openWriteLink() {
  31. if (sdb == null || !sdb.isOpen()) {
  32. sdb = helper.getWritableDatabase();
  33. }
  34. return sdb;
  35. }
  36. //关闭数据库连接
  37. public void closeLink() {
  38. if (sdb != null && sdb.isOpen()) {
  39. sdb.close();
  40. sdb = null;
  41. }
  42. }
  43. //创建数据库,执行建表语句
  44. @Override
  45. public void onCreate(SQLiteDatabase db) {
  46. //先删除已存在表
  47. String drop_sql = "drop table if exists " + TABLE_NAME + ";";
  48. db.execSQL(drop_sql);
  49. //创建表
  50. String create_sql = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " ("
  51. + "_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"
  52. + "name VARCHAR NOT NULL," + "age INTEGER NOT NULL,"
  53. + "height INTEGER NOT NULL," + "weight FLOAT NOT NULL,"
  54. + "married INTEGER NOT NULL," + "update_time VARCHAR NOT NULL"
  55. //演示数据库升级时要先把下面这行注释
  56. + ",phone VARCHAR" + ",password VARCHAR"
  57. + ");";
  58. db.execSQL(create_sql);
  59. }
  60. //修改表结构
  61. @Override
  62. public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
  63. if (newVersion > 1) {
  64. //Android的ALTER命令不支持一次添加多列,只能分多次添加
  65. String alter_sql = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN phone VARCHAR;";
  66. db.execSQL(alter_sql);
  67. alter_sql = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + "password VARCHAR;";
  68. db.execSQL(alter_sql); // 执行完整的SQL语
  69. }
  70. }
  71. //根据指定条件删除记录
  72. public int delete(String condition) {
  73. return sdb.delete(TABLE_NAME, condition, null);
  74. }
  75. //删除全部记录
  76. public int deleteAll() {
  77. return sdb.delete(TABLE_NAME, "1=1", null);
  78. }
  79. //根据条件查询记录
  80. public List<UserInfo> query(String condition) {
  81. String sql = String.format("select rowid,_id,name,age,height,weight,married,update_time," +
  82. "phone,password from %s where %s;", TABLE_NAME, condition);
  83. //执行查询语句,该语句返回结果集的游标
  84. Cursor cursor = sdb.rawQuery(sql, null);
  85. ArrayList<UserInfo> userInfos = new ArrayList<>();
  86. //循环取出游标指向的结果集
  87. while (cursor.moveToNext()) {
  88. UserInfo userInfo = new UserInfo();
  89. userInfo.name = cursor.getString(2);
  90. userInfo.age = cursor.getInt(3);
  91. userInfos.add(userInfo);
  92. }
  93. cursor.close();
  94. return userInfos;
  95. }
  96. //往表里添加一条记录
  97. public long insert(UserInfo userinfo) {
  98. ArrayList<UserInfo> userInfos = new ArrayList<>();
  99. userInfos.add(userinfo);
  100. return insert(userInfos);
  101. }
  102. //往表里添加多条记录
  103. public long insert(List<UserInfo> userInfos) {
  104. long result = -1;
  105. for (UserInfo userInfo : userInfos) {
  106. //如果名字相同,则更新记录
  107. if (userInfo.name != null && userInfo.name.length() > 0) {
  108. String condition = String.format("name = '%s'", userInfo.name);
  109. List<UserInfo> dbUserInfoList = query(condition);
  110. if (dbUserInfoList != null && dbUserInfoList.size() > 0) {
  111. update(userInfo, condition);
  112. //返回其id
  113. result = dbUserInfoList.get(0).id;
  114. continue;
  115. }
  116. }
  117. //其余情况则说明记录不重复,添加新纪录
  118. ContentValues cv = new ContentValues();
  119. cv.put("name", userInfo.name);
  120. cv.put("age", userInfo.age);
  121. result = sdb.insert(TABLE_NAME, "", cv);
  122. if(result == -1){
  123. return result;
  124. }
  125. }
  126. return result;
  127. }
  128. //根据指定条件更新表记录
  129. public int update(UserInfo userInfo, String condition) {
  130. ContentValues cv = new ContentValues();
  131. cv.put("name", userInfo.name);
  132. cv.put("age", userInfo.age);
  133. return sdb.update(TABLE_NAME, cv, condition, null);
  134. }
  135. }

(4)优化记住密码:

上面通过SharedPreferences存储密码的方式还是存在一定的局限性,该方式只能记住一个用户的登录信息,当下一个用户登录后,上一个用户的信息将会被覆盖。正确的记住密码功能应该是输入手机号自动补充密码,因此,可以考虑使用数据库来进行存储。

主要的改造如下:

  1. 声明一个数据库的helper对象,在Activity的OnResume方法中获取数据库连接,在OnPause方法中关闭数据库连接。
  1. private UserDBHelper helper;
  2. @Override
  3. protected void onResume() {
  4. super.onResume();
  5. //获取数据库帮助器实例 (此处是单例,所以不怕重复获取)
  6. helper = UserDBHelper.getInstance(this, 1);
  7. //恢复页面时则获取连接
  8. helper.openWriteLink();
  9. }
  10. @Override
  11. protected void onPause() {
  12. super.onPause();
  13. //暂停页面时就断开连接
  14. helper.closeLink();
  15. }

  1. 登录成功后,如果用户勾选了记住密码功能,则保存到数据库。也就是在loginSuccess方法中添加如下:
  1. if (isRemember) {
  2. UserInfo info = new UserInfo(); // 创建一个用户信息对象
  3. info.phone = et_phone.getText().toString();
  4. info.password = et_password.getText().toString();
  5. info.update_time = DateUtil.getNowDateTime("yyyy-MM-dd HH:mm:ss");
  6. mHelper.insert(info); // 往用户数据库添加登录成功的用户信息
  7. }
  1. 用户进行登录时,根据输入手机号自动查找密码:
  1. // 根据手机号码查询指定记录
  2. public UserInfo queryByPhone(String phone) {
  3. UserInfo info = null;
  4. List<UserInfo> infoList = query(String.format("phone='%s'", phone));
  5. if (infoList.size() > 0) { // 存在该号码的登录信息
  6. info = infoList.get(0);
  7. }
  8. return info;
  9. }

3. 存储卡

(1)私有空间和公有空间

为了更规范地管理手机存储空间,Android从7.0开始将存储卡划分为私有存储和公共存储两大部分,也就是分区存储方式,系统给每个App都分配了默认的私有存储空间。App在私有空间上读写文件无须任何授权,但是若想在公共空间读写文件,则要在AndroidManifest.xml里面添加下述的权限配置。

  1. <!-- 存储卡读写 -->
  2. <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
  3. <uses-permission android:name="android.permission.READ_EXTERNAL_STORAG"/>

但是即使App声明了完整的存储卡操作权限,系统仍然默认禁止该App访问公共空间。打开手机的系统设置界面,进入到具体应用的管理页面,会发现该应用的存储访问权限被禁止了。

既然存储卡分为公共空间和私有空间两部分,它们的空间路径获取也就有所不同。若想获取公共空间的存储路径,调用的是Environment.getExternalStoragePublicDirectory方法;若想获取应用私有空间的存储路径,调用的是getExternalFilesDir方法。

  1. //获取系统的公共存储路径
  2. String publicPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString();
  3. //获取系统的私有存储路径
  4. String privatePath = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString();
  5. boolean isLegacy = true;
  6. if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
  7. //Android10的存储空间默认采用分区方式,这里是判断是使用传统方式还是分区方式
  8. isLegacy = Environment.isExternalStorageLegacy();
  9. }

(2)在存储卡上读写文件

文本文件的读写借助IO流 FileOutputStream(写文件)和 FileInputStream(读文件)

  1. // 把字符串保存到指定路径的文本文件
  2. public static void saveText(String path, String txt) {
  3. // 根据指定的文件路径构建文件输出流对象
  4. try (FileOutputStream fos = new FileOutputStream(path)) {
  5. fos.write(txt.getBytes()); // 把字符串写入文件输出流
  6. } catch (Exception e) {
  7. e.printStackTrace();
  8. }
  9. }
  10. // 从指定路径的文本文件中读取内容字符串
  11. public static String openText(String path) {
  12. String readStr = "";
  13. // 根据指定的文件路径构建文件输入流对象
  14. try (FileInputStream fis = new FileInputStream(path)) {
  15. byte[] b = new byte[fis.available()];
  16. fis.read(b); // 从文件输入流读取字节数组
  17. readStr = new String(b); // 把字节数组转换为字符串
  18. } catch (Exception e) {
  19. e.printStackTrace();
  20. }
  21. return readStr; // 返回文本文件中的文本字符串
  22. }

(3)在存储卡上读写 图片文件

文本文件可以转化为对字符串的读写,而图像的读写就需要借助专门的位图工具Bitmap处理。不同图像来源获取Bitmap的方式不同,有三种:

  1. 从指定资源文件中获取:decodeResource,例如从资源文件img.png获取位图对象:
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.img);
  1. 从指定路径下获取:decodeFile,但是要注意从Android10开始,该方法只能获取私有空间下的图片,公共空间下获取不了。
Bitmap bitmap = BitmapFactory.decodeFile("C:\\Users\\OYMN\\Pictures\\onepunch.jpg");
  1. 从指定的输入流中获取,比如使用IO流打开图片文件,然后作为参数传入decodeStream:
  1. public static Bitmap openImage(String path) {
  2. Bitmap bitmap = null; // 声明一个位图对象
  3. // 根据指定的文件路径构建文件输入流对象
  4. try (FileInputStream fis = new FileInputStream(path)) {
  5. bitmap = BitmapFactory.decodeStream(fis); // 从文件输入流中解码位图数据
  6. } catch (Exception e) {
  7. e.printStackTrace();
  8. }
  9. return bitmap; // 返回图片文件中的位图数据
  10. }

获取到图片之后就可以通过ImageView的setImageBitmap进行设置了。

有多种读取图片的方式,但是写图片只有一种方式。通过Bitmap的compress方法将位图数据压缩到文件输出流:

  1. public static void saveImage(String path, Bitmap bitmap){
  2. //根据文件路径构建文件输出流
  3. try(FileOutputStream fos = new FileOutputStream()){
  4. //将位图数据压缩到文件输出流
  5. bitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos);
  6. }catch(Exception e){
  7. e.printStackTrace();
  8. }
  9. }

以下演示一下完整的文件读写操作:

  1. // 获取当前App的私有下载目录
  2. String path = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() +
  3. "/";
  4. // 从指定的资源文件中获取位图对象
  5. Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.huawei);
  6. String file_path = path + DateUtil.getNowDateTime("") + ".jpeg";
  7. FileUtil.saveImage(file_path, bitmap); // 把位图对象保存为图片文件
  8. tv_path.setText("图片文件的保存路径为:\n" + file_path);
  1. // 获取当前App的私有下载目录
  2. mPath = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + "/";
  3. // 获得指定目录下面的所有图片文件
  4. mFilelist = FileUtil.getFileList(mPath, new String[]{".jpeg"});
  5. if (mFilelist.size() > 0) {
  6. // 打开并显示选中的图片文件内容
  7. String file_path = mFilelist.get(0).getAbsolutePath();
  8. tv_content.setText("找到最新的图片文件,路径为"+file_path);
  9. // 显示存储卡图片文件的第一种方式:直接调用setImageURI方法
  10. //iv_content.setImageURI(Uri.parse(file_path)); // 设置图像视图的路径对象
  11. // 第二种方式:先调用BitmapFactory.decodeFile获得位图,再调用setImageBitmap方法
  12. //Bitmap bitmap = BitmapFactory.decodeFile(file_path);
  13. //iv_content.setImageBitmap(bitmap); // 设置图像视图的位图对象
  14. // 第三种方式:先调用FileUtil.openImage获得位图,再调用setImageBitmap方法
  15. Bitmap bitmap = FileUtil.openImage(file_path);
  16. iv_content.setImageBitmap(bitmap); // 设置图像视图的位图对象

4. 应用组件Application

Application是Android的一大组件,在App运行期间只有一个Application对象贯穿整个应用的生命周期。因此,Application适合保存全局变量,主要是以下三类数据:

  • 会频繁读取的信息:如用户名,手机号码等

  • 不方便通过intent传递的数据,如位图对象,非字符串的集合对象等。

  • 容易因频繁分配内存而导致内存泄漏的对象,如Handler处理器实例等。

    image-20230115170457698

通过Application实现对全局内存的读写:

  1. 先继承Application,并获取唯一实例:
  1. public class MyApplication extends Application {
  2. private static MyApplication myApplication; //Application唯一实例
  3. public Map<String, String> map = new HashMap<>(); //当作全局变量,用来存储数据
  4. public static MyApplication getInstance(){
  5. return myApplication;
  6. }
  7. @Override
  8. public void onCreate() {
  9. super.onCreate();
  10. // 在打开应用时对静态的应用实例赋值
  11. myApplication = this;
  12. }
  13. }
  1. 在AndroidManifest.xml 通过name属性添加该Application

image-20230117121213401

  1. 接下来就可以通过该Application在整个App中存取数据了:

如在MainActivity6存储数据:

  1. @Override
  2. protected void onCreate(Bundle savedInstanceState) {
  3. super.onCreate(savedInstanceState);
  4. setContentView(R.layout.activity_main6);
  5. //存储数据
  6. MyApplication myApplication = MyApplication.getInstance();
  7. myApplication.map.put("myKey", "myValue");
  8. //跳转到MainActivity5
  9. View bt5 = findViewById(R.id.bt5);
  10. bt5.setOnClickListener(new View.OnClickListener() {
  11. @Override
  12. public void onClick(View v) {
  13. Intent intent = new Intent(MainActivity6.this, MainActivity5.class);
  14. startActivity(intent);
  15. }
  16. });
  17. }

在MainActivity5中获取数据:

  1. @Override
  2. protected void onCreate(Bundle savedInstanceState) {
  3. super.onCreate(savedInstanceState);
  4. setContentView(R.layout.activity_main5);
  5. TextView tv = findViewById(R.id.tv);
  6. tv.setText(MyApplication.getInstance().map.get("myKey")); //成功获取到数据
  7. }

6.4.3 利用Room简化数据库操作 虽然Android提供了数据库帮助器,但是开发者在进行数据库编程时仍有诸多不便,比如每次增加一张

新表,开发者都得手工实现以下代码逻辑: (1)重写数据库帮助器的onCreate方法,添加该表的建表语句。 (2)在插入记录之时,必须将数据实例的属性值逐一赋给该表的各字段。 (3)在查询记录之时,必须遍历结果集游标,把各字段值逐一赋给数据实例。 (4)每次读写操作之前,都要先开启数据库连接;读写操作之后,又要关闭数据库连接。

上述的处理操作无疑存在不少重复劳动,数年来引得开发者叫苦连连。为此各类数据库处理框架纷纷涌 现,包括GreenDao、OrmLite、Realm等,可谓百花齐放。眼见SQLite渐渐乏人问津,谷歌公司干脆整 了个自己的数据库框架—Room,该框架同样基于SQLite,但它通过注解技术极大地简化了数据库操 作,减少了原来相当一部分编码工作量。

5. 实战:购物车

五、内容共享

1. 在应用之间共享数据

接下来将介绍Android的四大组件之一ContentProvider,通过ContentProvider封装内部数据的外部访问接口,实现不同应用能够互相传输数据。

和ContentProvider搭配使用的还有:ContentResolver(内容解析器),ContentObserver(内容观察器)。

上面提到的SQLite可以操作自身的数据库,而ContentProvider则是作为中间接口,通过SQLiteOpenHelper和SQLiteDatabase间接操控数据库,实现为其他应用提供数据的功能。

image-20230119194835570

使用举例如下:

  1. 创建一个UserInfoProvider,用来提供用户信息给外界应用

    在弹出的右键菜单中依次选择New→Other→Content Provider

    此时会自动修改两处地方:

    (1)一是在AndroidManifest.xml中添加该Provider的配置信息:

    image-20230118224755856

    (2)二是创建的这个Provider会继承ContentProvider,并重写了一些方法。

Server端代码:

  1. public class UserInfoProvider extends ContentProvider {
  2. //这里是上面实现的dbHelper,用来操作本地数据库
  3. private UserDBHelper userDBHelper;
  4. //初始化
  5. @Override
  6. public boolean onCreate() {
  7. //初始化 dbHelper
  8. userDBHelper = UserDBHelper.getInstance(getContext());
  9. return true;
  10. }
  11. //插入
  12. //uri格式:content://com.example.secondandroidapp.UserInfoProvider/user
  13. @Override
  14. public Uri insert(Uri uri, ContentValues values) {
  15. //使用sqlite插入数据
  16. SQLiteDatabase db = userDBHelper.getWritableDatabase();
  17. db.insert(UserDBHelper.TABLE_NAME, null, values);
  18. return uri;
  19. }
  20. //查询
  21. @Override
  22. public Cursor query(Uri uri, String[] projection, String selection,
  23. String[] selectionArgs, String sortOrder) {
  24. SQLiteDatabase db = userDBHelper.getReadableDatabase();
  25. return db.query(UserDBHelper.TABLE_NAME, projection, selection, selectionArgs, null, null, null);
  26. }
  27. //删除
  28. @Override
  29. public int delete(Uri uri, String selection, String[] selectionArgs) {
  30. int count = 0;
  31. switch (uriMatcher.match(uri)) {
  32. //这种是uri不带参数:"content://com.example.secondandroidapp.UserInfoProvider/user"
  33. case USER:
  34. // 获取SQLite数据库的写连接
  35. SQLiteDatabase db = userDBHelper.getWritableDatabase();
  36. // 执行SQLite的删除操作,并返回删除记录的数目
  37. count = db.delete(UserDBHelper.TABLE_NAME, selection,
  38. selectionArgs);
  39. db.close();
  40. break;
  41. //这种是uri带参数:"content://com.example.secondandroidapp.UserInfoProvider/user/2"
  42. case USERS:
  43. String id = uri.getLastPathSegment();
  44. SQLiteDatabase db2 = userDBHelper.getWritableDatabase();
  45. count = db2.delete(UserDBHelper.TABLE_NAME, "id = ?", new String[]{id});
  46. db2.close();
  47. break;
  48. }
  49. return count;
  50. }
  51. @Override
  52. public String getType(Uri uri) {
  53. // TODO: Implement this to handle requests for the MIME type of the data
  54. // at the given URI.
  55. throw new UnsupportedOperationException("Not yet implemented");
  56. }
  57. @Override
  58. public int update(Uri uri, ContentValues values, String selection,
  59. String[] selectionArgs) {
  60. // TODO: Implement this to handle requests to update one or more rows.
  61. throw new UnsupportedOperationException("Not yet implemented");
  62. }
  63. }
  1. 利用ContentProvider只实现服务端App的数据封装,如果客户端App想访问对方的内部数据,就要通过内容解析器ContentResolver访问。

    image-20230119194953733

ContentProvider的Uri结构如下:content://authority/data_path/id

Client的代码如下:

  1. public class MainActivity7 extends AppCompatActivity {
  2. private static Uri ContentUri = Uri.parse("content://com.example.secondandroidapp.UserInfoProvider/user");
  3. @Override
  4. protected void onCreate(Bundle savedInstanceState) {
  5. super.onCreate(savedInstanceState);
  6. setContentView(R.layout.activity_main7);
  7. Button insertButton = findViewById(R.id.insertButton);
  8. insertButton.setOnClickListener(new View.OnClickListener() {
  9. @Override
  10. public void onClick(View v) {
  11. ContentValues values = new ContentValues();
  12. values.put("name", "陈鸿荣");
  13. values.put("age", "20");
  14. //获取到ContentResolver之后调用插入方法进行插入
  15. getContentResolver().insert(ContentUri, values);
  16. }
  17. });
  18. Button deleteButton = findViewById(R.id.deleteButton);
  19. deleteButton.setOnClickListener(new View.OnClickListener() {
  20. @Override
  21. public void onClick(View v) {
  22. // content://com.example.secondandroidapp.UserInfoProvider/user/2
  23. Uri uri = ContentUris.withAppendedId(ContentUri, 2);
  24. int count = getContentResolver().delete(uri, null, null);
  25. }
  26. });
  27. }
  28. }

出于安全考虑,Android11需要事先声明需要访问的其他应用:

在AndroidManifest.xml中添加如下:

  1. <queries>
  2. <!--服务端应用包名 -->
  3. <package android:name="com.example.secondandroidapp"/>
  4. <!--或者直接指定authorities-->
  5. <!-- <provider android:authorities="com.example.secondandroidapp.UserInfoProvider"/> -->
  6. </queries>

2. 使用内容组件获取通讯信息

(1)运行时动态申请权限

在上面讲公共存储空间与私有存储空间提到,App若想访问存储卡的公共空间,就要在AndroidManifest.xml里面添加下述的权限配置。

  1. <!-- 存储卡读写 -->
  2. <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
  3. <uses-permission android:name="android.permission.READ_EXTERNAL_STORAG" />

然而即使App声明了完整的存储卡操作权限,从Android 7.0开始,系统仍然默认禁止该App访问公共空间,必须到设置界面手动开启应用的存储卡权限才行。尽管此举是为用户隐私着想,可是人家咋知道要手工开权限呢?就算用户知道,去设置界面找到权限开关也颇费周折。为此Android支持在Java代码中处理权限,处理过程分为3个步骤:

  1. 检查App是否开启了指定权限:

    调用ContextCompat的checkSelfPermission方法

  2. 请求系统弹窗,以便用户选择是否开启权限:

    调用ActivityCompat的requestPermissions方法,即可命令系统自动弹出权限申请窗口。

  3. 判断用户的权限选择结果,是开启还是拒绝:

    重写活动页面的权限请求回调方法onRequestPermissionsResult,在该方法内部处理用户的权限选择结果

动态申请权限有两种方式:饿汉式 和 懒汉式。

接下来通过获取通讯权限和短信权限来进行举例说明:

首先是懒汉式:当需要某种权限的时候再去申请

  1. public class PermissionUtil {
  2. //检查权限,返回true表示完全启用权限,返回false则表示为完全启用所有权限
  3. public static boolean checkPermission(Activity activity, String[] permissions, int requestCode){
  4. //Android6.0之后采取动态权限管理
  5. if(Build.VERSION.SDK_INT > Build.VERSION_CODES.M){
  6. int check = PackageManager.PERMISSION_GRANTED; // 0
  7. for (String permission : permissions) {
  8. check = ContextCompat.checkSelfPermission(activity, permission);
  9. if(check != PackageManager.PERMISSION_GRANTED){
  10. break;
  11. }
  12. }
  13. //如果未开启该权限,则请求系统弹窗,好让用户选择是否开启权限
  14. if(check != PackageManager.PERMISSION_GRANTED){
  15. //请求权限
  16. ActivityCompat.requestPermissions(activity, permissions, requestCode);
  17. return false;
  18. }
  19. return true;
  20. }
  21. return false;
  22. }
  23. //检查权限数组,返回true表示都已经授权
  24. public static boolean checkGrant(int[] grantResults) {
  25. if(grantResults != null){
  26. for (int grant : grantResults) {
  27. if(grant != PackageManager.PERMISSION_GRANTED){
  28. return false;
  29. }
  30. }
  31. return true;
  32. }
  33. return false;
  34. }
  35. }

通过两个按钮模拟分别获取权限:

  1. public class PermissionLazyActivity extends AppCompatActivity {
  2. //通讯录的读写权限
  3. private static final String[] PERMISSION_CONTACT = {
  4. Manifest.permission.READ_CONTACTS,
  5. Manifest.permission.WRITE_CONTACTS
  6. };
  7. //短信的读写权限
  8. private static final String[] PERMISSION_SMS = {
  9. Manifest.permission.SEND_SMS,
  10. Manifest.permission.RECEIVE_SMS
  11. };
  12. private static final int REQUEST_CODE_CONTACTS = 1;
  13. private static final int REQUEST_CODE_SMS = 2;
  14. @Override
  15. protected void onCreate(Bundle savedInstanceState) {
  16. super.onCreate(savedInstanceState);
  17. setContentView(R.layout.activity_permission_lazy);
  18. //获取通讯录权限
  19. findViewById(R.id.btn_contact).setOnClickListener(new View.OnClickListener() {
  20. @Override
  21. public void onClick(View v) {
  22. PermissionUtil.checkPermission(PermissionLazyActivity.this, PERMISSION_CONTACT, REQUEST_CODE_CONTACTS);
  23. }
  24. });
  25. //获取短信权限
  26. findViewById(R.id.btn_sms).setOnClickListener(new View.OnClickListener() {
  27. @Override
  28. public void onClick(View v) {
  29. PermissionUtil.checkPermission(PermissionLazyActivity.this, PERMISSION_SMS, REQUEST_CODE_SMS);
  30. }
  31. });
  32. }
  33. // 用户选择权限结果后会调用该回调方法
  34. @Override
  35. public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
  36. super.onRequestPermissionsResult(requestCode, permissions, grantResults);
  37. switch (requestCode){
  38. case REQUEST_CODE_CONTACTS:
  39. if(PermissionUtil.checkGrant(grantResults)){
  40. Log.d("hhh", "通讯录获取成功");
  41. }else{
  42. Log.d("hhh", "通讯录获取失败");
  43. //跳转到设置界面
  44. jumpToSettings();
  45. }
  46. break;
  47. case REQUEST_CODE_SMS:
  48. if(PermissionUtil.checkGrant(grantResults)){
  49. Log.d("hhh", "短信权限获取成功");
  50. }else{
  51. Log.d("hhh", "短信权限获取失败");
  52. //跳转到设置界面
  53. jumpToSettings();
  54. }
  55. break;
  56. }
  57. }
  58. //跳转到设置界面
  59. private void jumpToSettings(){
  60. Intent intent = new Intent();
  61. intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
  62. intent.setData(Uri.fromParts("package", getPackageName(), null));
  63. intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  64. startActivity(intent);
  65. }
  66. }

另外还需要在AndroidManifest.xml中配置:(在低版本中只需要配置这些信息即可,高版本就需要上面的动态申请权限)

  1. <!-- 开启通讯录权限-->
  2. <uses-permission android:name="android.permission.READ_CONTACTS"/>
  3. <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
  4. <!-- 开启短信收发权限-->
  5. <uses-permission android:name="android.permission.SEND_SMS"/>
  6. <uses-permission android:name="android.permission.RECEIVE_SMS"/>

效果如下:

懒汉式:在页面打开之后就一次性需要用户获取所有权限。

  1. public class PermissionHungryActivity extends AppCompatActivity {
  2. //所需全部读写权限
  3. private static final String[] PERMISSIONS = {
  4. Manifest.permission.READ_CONTACTS,
  5. Manifest.permission.WRITE_CONTACTS,
  6. Manifest.permission.SEND_SMS,
  7. Manifest.permission.RECEIVE_SMS
  8. };
  9. //
  10. private static final int REQUEST_CODE_ALL = 0;
  11. @Override
  12. protected void onCreate(Bundle savedInstanceState) {
  13. super.onCreate(savedInstanceState);
  14. setContentView(R.layout.activity_permission_lazy);
  15. //检查是否拥有所有所需权限
  16. PermissionUtil.checkPermission(this, PERMISSIONS, REQUEST_CODE_ALL);
  17. }
  18. // 用户选择权限结果后会调用该回调方法
  19. @Override
  20. public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
  21. super.onRequestPermissionsResult(requestCode, permissions, grantResults);
  22. switch (requestCode){
  23. case REQUEST_CODE_ALL:
  24. if(PermissionUtil.checkGrant(grantResults)){
  25. Log.d("hhh", "所有权限获取成功");
  26. }else{
  27. //部分权限获取失败
  28. for (int i = 0; i < grantResults.length; i++) {
  29. if(grantResults[i] != PackageManager.PERMISSION_GRANTED){
  30. //判断是什么权限获取失败
  31. switch (permissions[i]){
  32. case Manifest.permission.WRITE_CONTACTS:
  33. case Manifest.permission.READ_CONTACTS:
  34. Log.d("hhh", "通讯录获取失败");
  35. jumpToSettings();
  36. break;
  37. case Manifest.permission.SEND_SMS:
  38. case Manifest.permission.RECEIVE_SMS:
  39. Log.d("hhh", "短信权限获取失败");
  40. jumpToSettings();
  41. break;
  42. }
  43. }
  44. }
  45. }
  46. break;
  47. }
  48. }
  49. //跳转到设置界面
  50. private void jumpToSettings(){
  51. Intent intent = new Intent();
  52. intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
  53. intent.setData(Uri.fromParts("package", getPackageName(), null));
  54. intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  55. startActivity(intent);
  56. }
  57. }

(2)使用ContentResolver读写联系人

手机中通讯录的主要表结构有:

raw_contacts表:

image-20230122114023016

data表:记录了用户的通讯录所有数据,包括手机号,显示名称等,但是里面的mimetype_id表示不同的数据类型,这与表mimetypes表中的id相对应,raw_contact_id 与上面的 raw_contacts表中的 id 相对应。

image-20230122114040243

mimetypes表:

image-20230122114056962

所以,插入步骤如下:

  • 首先往raw_contacts表中插入一条数据得到id
  • 接着由于一个联系人有姓名,电话号码,邮箱,因此需要分三次插入data表中,将raw_contact_id和上面得到的id进行关联

下面是往通讯录插入和查询联系人的代码:

  1. public class ContactActivity extends AppCompatActivity implements View.OnClickListener {
  2. private EditText et_contact_name;
  3. private EditText et_contact_phone;
  4. private EditText et_contact_email;
  5. @Override
  6. protected void onCreate(Bundle savedInstanceState) {
  7. super.onCreate(savedInstanceState);
  8. setContentView(R.layout.activity_contact);
  9. et_contact_name = findViewById(R.id.et_contact_name);
  10. et_contact_phone = findViewById(R.id.et_contact_phone);
  11. et_contact_email = findViewById(R.id.et_contact_email);
  12. findViewById(R.id.btn_add_contact).setOnClickListener(this);
  13. findViewById(R.id.btn_read_contact).setOnClickListener(this);
  14. }
  15. @Override
  16. public void onClick(View v) {
  17. switch (v.getId()) {
  18. case R.id.btn_add_contact:
  19. // 创建一个联系人对象
  20. Contact contact = new Contact();
  21. contact.name = et_contact_name.getText().toString().trim();
  22. contact.phone = et_contact_phone.getText().toString().trim();
  23. contact.email = et_contact_email.getText().toString().trim();
  24. // 方式一,使用ContentResolver多次写入,每次一个字段
  25. // addContacts(getContentResolver(), contact);
  26. // 方式二,批处理方式
  27. // 每一次操作都是一个 ContentProviderOperation,构建一个操作集合,然后一次性执行
  28. // 好处是,要么全部成功,要么全部失败,保证了事务的一致性
  29. addFullContacts(getContentResolver(), contact);
  30. Toast.makeText(this, "添加联系人成功!", Toast.LENGTH_SHORT).show();
  31. break;
  32. case R.id.btn_read_contact:
  33. readPhoneContacts(getContentResolver());
  34. break;
  35. }
  36. }
  37. //往通讯录添加一个联系人信息(姓名,号码,邮箱)
  38. private void addContacts(ContentResolver contentResolver, Contact contact) {
  39. //得到rawContentId
  40. ContentValues values = new ContentValues();
  41. //插入记录得到id
  42. Uri uri = contentResolver.insert(ContactsContract.RawContacts.CONTENT_URI, values);
  43. long rawContentId = ContentUris.parseId(uri);
  44. //插入名字
  45. ContentValues name = new ContentValues();
  46. //关联上面得到的联系人id
  47. name.put(ContactsContract.Contacts.Data.RAW_CONTACT_ID, rawContentId);
  48. //关联联系人姓名的类型
  49. name.put(ContactsContract.Contacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
  50. //关联联系人姓名
  51. name.put(ContactsContract.Data.DATA2, contact.name);
  52. contentResolver.insert(ContactsContract.Data.CONTENT_URI, name);
  53. //插入电话号码
  54. ContentValues phone = new ContentValues();
  55. //关联上面得到的联系人id
  56. phone.put(ContactsContract.Contacts.Data.RAW_CONTACT_ID, rawContentId);
  57. //关联联系人电话号码的类型
  58. phone.put(ContactsContract.Contacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
  59. //关联联系人电话号码
  60. phone.put(ContactsContract.Data.DATA1, contact.phone);
  61. //指定该号码是家庭号码还是工作号码 (家庭)
  62. phone.put(ContactsContract.Data.DATA2, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);
  63. contentResolver.insert(ContactsContract.Data.CONTENT_URI, phone);
  64. //插入邮箱
  65. ContentValues email = new ContentValues();
  66. //关联上面得到的联系人id
  67. email.put(ContactsContract.Contacts.Data.RAW_CONTACT_ID, rawContentId);
  68. //关联联系人邮箱的类型
  69. email.put(ContactsContract.Contacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE);
  70. //关联联系人邮箱
  71. email.put(ContactsContract.Data.DATA1, contact.email);
  72. //指定该号码是家庭邮箱还是工作邮箱
  73. email.put(ContactsContract.Data.DATA2, ContactsContract.CommonDataKinds.Phone.TYPE_WORK);
  74. contentResolver.insert(ContactsContract.Data.CONTENT_URI, email);
  75. }
  76. //事务操作,四个插入操作一次性提交
  77. private void addFullContacts(ContentResolver contentResolver, Contact contact) {
  78. //创建一个插入联系人主记录的内容操作器
  79. ContentProviderOperation op_main = ContentProviderOperation
  80. .newInsert(ContactsContract.RawContacts.CONTENT_URI)
  81. //没有实际意义,不加这个会报错(不加这个导致没有创建ContentValue,导致报错)
  82. .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null)
  83. .build();
  84. //创建一个插入联系人姓名记录的内容操作器
  85. ContentProviderOperation op_name = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
  86. //将第0个操作的id,即raw_contacts中的id作为data表中的raw_contact_id
  87. .withValueBackReference(ContactsContract.Contacts.Data.RAW_CONTACT_ID, 0)
  88. .withValue(ContactsContract.Contacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
  89. .withValue(ContactsContract.Data.DATA2, contact.name)
  90. .build();
  91. //创建一个插入联系人电话号码记录的内容操作器
  92. ContentProviderOperation op_phone = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
  93. //将第0个操作的id,即raw_contacts中的id作为data表中的raw_contact_id
  94. .withValueBackReference(ContactsContract.Contacts.Data.RAW_CONTACT_ID, 0)
  95. .withValue(ContactsContract.Contacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
  96. .withValue(ContactsContract.Data.DATA1, contact.phone)
  97. .withValue(ContactsContract.Data.DATA2, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE)
  98. .build();
  99. //创建一个插入联系人邮箱记录的内容操作器
  100. ContentProviderOperation op_email = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
  101. //将第0个操作的id,即raw_contacts中的id作为data表中的raw_contact_id
  102. .withValueBackReference(ContactsContract.Contacts.Data.RAW_CONTACT_ID, 0)
  103. .withValue(ContactsContract.Contacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
  104. .withValue(ContactsContract.Data.DATA1, contact.email)
  105. .withValue(ContactsContract.Data.DATA2, ContactsContract.CommonDataKinds.Phone.TYPE_WORK)
  106. .build();
  107. //全部放在集合中一次性提交
  108. ArrayList<ContentProviderOperation> operations = new ArrayList<>();
  109. operations.add(op_main);
  110. operations.add(op_name);
  111. operations.add(op_phone);
  112. operations.add(op_email);
  113. try {
  114. //批量提交四个操作
  115. contentResolver.applyBatch(ContactsContract.AUTHORITY, operations);
  116. } catch (OperationApplicationException e) {
  117. e.printStackTrace();
  118. } catch (RemoteException e) {
  119. e.printStackTrace();
  120. }
  121. }
  122. //读取联系人
  123. @SuppressLint("Range")
  124. private void readPhoneContacts(ContentResolver contentResolver) {
  125. //先查询raw_contacts表,再根据raw_contacts_id表 查询data
  126. Cursor cursor = contentResolver.query(ContactsContract.RawContacts.CONTENT_URI, new String[]{ContactsContract.RawContacts._ID}, null, null, null);
  127. while(cursor.moveToNext()){
  128. int rawContactId = cursor.getInt(0);
  129. Uri uri = Uri.parse("content://com.android.contacts/contacts/" + rawContactId + "/data");
  130. Cursor dataCursor = contentResolver.query(uri, new String[]{ContactsContract.Contacts.Data.MIMETYPE, ContactsContract.Contacts.Data.DATA1, ContactsContract.Contacts.Data.DATA2}, null, null, null);
  131. Contact contact = new Contact();
  132. while (dataCursor.moveToNext()) {
  133. String data1 = dataCursor.getString(dataCursor.getColumnIndex(ContactsContract.Contacts.Data.DATA1));
  134. String mimeType = dataCursor.getString(dataCursor.getColumnIndex(ContactsContract.Contacts.Data.MIMETYPE));
  135. switch (mimeType) {
  136. //是姓名
  137. case ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE:
  138. contact.name = data1;
  139. break;
  140. //邮箱
  141. case ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE:
  142. contact.email = data1;
  143. break;
  144. //手机
  145. case ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE:
  146. contact.phone = data1;
  147. break;
  148. }
  149. }
  150. dataCursor.close();
  151. // RawContacts 表中出现的 _id,不一定在 Data 表中都会有对应记录
  152. if (contact.name != null) {
  153. Log.d("hhh", contact.toString());
  154. }
  155. }
  156. cursor.close();
  157. }
  158. }
  • 页面如下:

(3)使用ContentObserver监听短信

ContentResolver获取数据采用的是主动查询方式,有查询就有数据,没查询就没数据。ContentResolver能够实时获取新增的数据,最常见的业务场景是短信验证码。为了替用户省事,App通常会监控手机刚收到的短信验证码,并自动填写验证码输入框。这时就用到了内容观察器ContentObserver,事先给目标内容注册一个观察器,目标内容的数据一旦发生变化,就马上触发观察器的监听事件,从而执行开发者预先定义的代码。

image-20230126164316484

示例代码如下:(记得在Manifest.xml中开启权限和动态开启权限)

  1. public class MonitorSmsActivity extends AppCompatActivity {
  2. private SmsGetObserver mObserver;
  3. @Override
  4. protected void onCreate(Bundle savedInstanceState) {
  5. super.onCreate(savedInstanceState);
  6. setContentView(R.layout.activity_monitor_sms);
  7. // 给指定Uri注册内容观察器,一旦发生数据变化,就触发观察器的onChange方法
  8. Uri uri = Uri.parse("content://sms");
  9. // notifyForDescendents:
  10. // false :表示精确匹配,即只匹配该Uri,true :表示可以同时匹配其派生的Uri
  11. mObserver = new SmsGetObserver(this);
  12. getContentResolver().registerContentObserver(uri, true, mObserver);
  13. }
  14. @Override
  15. protected void onDestroy() {
  16. super.onDestroy();
  17. //取消注册
  18. getContentResolver().unregisterContentObserver(mObserver);
  19. }
  20. private static class SmsGetObserver extends ContentObserver {
  21. private final Context mContext;
  22. public SmsGetObserver(Context context) {
  23. super(new Handler(Looper.getMainLooper()));
  24. this.mContext = context;
  25. }
  26. //回调
  27. @SuppressLint("Range")
  28. @Override
  29. public void onChange(boolean selfChange, @Nullable Uri uri) {
  30. super.onChange(selfChange, uri);
  31. // onChange会多次调用,收到一条短信会调用两次onChange
  32. // mUri===content://sms/raw/20
  33. // mUri===content://sms/inbox/20
  34. // 安卓7.0以上系统,点击标记为已读,也会调用一次
  35. // mUri===content://sms
  36. // 收到一条短信都是uri后面都会有确定的一个数字,对应数据库的_id,比如上面的20
  37. if (uri == null) {
  38. return;
  39. }
  40. if (uri.toString().contains("content://sms/raw") ||
  41. uri.toString().equals("content://sms")) {
  42. return;
  43. }
  44. // 通过内容解析器获取符合条件的结果集游标
  45. Cursor cursor = mContext.getContentResolver().query(uri, new String[]{"address", "body", "date"}, null, null, "date DESC");
  46. if (cursor.moveToNext()) {
  47. // 短信的发送号码
  48. String sender = cursor.getString(cursor.getColumnIndex("address"));
  49. // 短信内容
  50. String content = cursor.getString(cursor.getColumnIndex("body"));
  51. Log.d("ning", String.format("sender:%s,content:%s", sender, content));
  52. }
  53. cursor.close();
  54. }
  55. }
  56. }

3. 在应用之间共享文件

(1)使用相册图片发送彩信

(2)借助FileProvider发送彩信

(3)借助FileProvider安装应用

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/230935
推荐阅读
相关标签
  

闽ICP备14008679号