2024年鸿蒙最全Android基础教程——从入门到精通(上)_生皮鞣制工艺流程(3),2024年最新字节跳动HarmonyOS鸿蒙岗面试题

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!


img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化的资料的朋友,可以戳这里获取

public UserDBHelper(Context context, int version) {
super(context, DB_NAME, null, version);
}

//通过单例模式获取 UserDBHelper 的唯一实例
public static synchronized UserDBHelper getInstance(Context context, int version) {
if (version > 0 && helper == null) {
helper = new UserDBHelper(context, version);
} else if (helper == null) {
helper = new UserDBHelper(context);
}

return helper;
}

//打开读连接
public SQLiteDatabase openReadLink() {
if (sdb == null || !sdb.isOpen()) {
sdb = helper.getReadableDatabase();
}

return sdb;
}

//打开写连接
public SQLiteDatabase openWriteLink() {
if (sdb == null || !sdb.isOpen()) {
sdb = helper.getWritableDatabase();
}

return sdb;
}

//关闭数据库连接
public void closeLink() {
if (sdb != null && sdb.isOpen()) {
sdb.close();
sdb = null;
}
}

//创建数据库,执行建表语句
@Override
public void onCreate(SQLiteDatabase db) {
//先删除已存在表
String drop_sql = "drop table if exists " + TABLE_NAME + “;”;
db.execSQL(drop_sql);

//创建表
String create_sql = “CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (”

  • “_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,”
  • “name VARCHAR NOT NULL,” + “age INTEGER NOT NULL,”
  • “height INTEGER NOT NULL,” + “weight FLOAT NOT NULL,”
  • “married INTEGER NOT NULL,” + “update_time VARCHAR NOT NULL”
    //演示数据库升级时要先把下面这行注释
  • “,phone VARCHAR” + “,password VARCHAR”
  • “);”;

db.execSQL(create_sql);
}

//修改表结构
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (newVersion > 1) {
//Android的ALTER命令不支持一次添加多列,只能分多次添加
String alter_sql = “ALTER TABLE " + TABLE_NAME + " ADD COLUMN phone VARCHAR;”;
db.execSQL(alter_sql);

alter_sql = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + “password VARCHAR;”;
db.execSQL(alter_sql); // 执行完整的SQL语
}
}

//根据指定条件删除记录
public int delete(String condition) {
return sdb.delete(TABLE_NAME, condition, null);
}

//删除全部记录
public int deleteAll() {
return sdb.delete(TABLE_NAME, “1=1”, null);
}

//根据条件查询记录
public List query(String condition) {
String sql = String.format(“select rowid,_id,name,age,height,weight,married,update_time,” +
“phone,password from %s where %s;”, TABLE_NAME, condition);
//执行查询语句,该语句返回结果集的游标
Cursor cursor = sdb.rawQuery(sql, null);

ArrayList userInfos = new ArrayList<>();

//循环取出游标指向的结果集
while (cursor.moveToNext()) {
UserInfo userInfo = new UserInfo();
userInfo.name = cursor.getString(2);
userInfo.age = cursor.getInt(3);
userInfos.add(userInfo);
}

cursor.close();
return userInfos;
}

//往表里添加一条记录
public long insert(UserInfo userinfo) {
ArrayList userInfos = new ArrayList<>();
userInfos.add(userinfo);
return insert(userInfos);
}

//往表里添加多条记录
public long insert(List userInfos) {

long result = -1;

for (UserInfo userInfo : userInfos) {
//如果名字相同,则更新记录
if (userInfo.name != null && userInfo.name.length() > 0) {
String condition = String.format(“name = ‘%s’”, userInfo.name);
List dbUserInfoList = query(condition);
if (dbUserInfoList != null && dbUserInfoList.size() > 0) {
update(userInfo, condition);
//返回其id
result = dbUserInfoList.get(0).id;
continue;
}
}
//其余情况则说明记录不重复,添加新纪录
ContentValues cv = new ContentValues();
cv.put(“name”, userInfo.name);
cv.put(“age”, userInfo.age);
result = sdb.insert(TABLE_NAME, “”, cv);
if(result == -1){
return result;
}
}

return result;
}

//根据指定条件更新表记录
public int update(UserInfo userInfo, String condition) {

ContentValues cv = new ContentValues();
cv.put(“name”, userInfo.name);
cv.put(“age”, userInfo.age);

return sdb.update(TABLE_NAME, cv, condition, null);
}

}

(4)优化记住密码:

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

主要的改造如下:

  1. 声明一个数据库的helper对象,在Activity的OnResume方法中获取数据库连接,在OnPause方法中关闭数据库连接。

private UserDBHelper helper;

@Override
protected void onResume() {
super.onResume();
//获取数据库帮助器实例 (此处是单例,所以不怕重复获取)
helper = UserDBHelper.getInstance(this, 1);
//恢复页面时则获取连接
helper.openWriteLink();
}

@Override
protected void onPause() {
super.onPause();
//暂停页面时就断开连接
helper.closeLink();
}

  1. 登录成功后,如果用户勾选了记住密码功能,则保存到数据库。也就是在loginSuccess方法中添加如下:

if (isRemember) {
UserInfo info = new UserInfo(); // 创建一个用户信息对象
info.phone = et_phone.getText().toString();
info.password = et_password.getText().toString();
info.update_time = DateUtil.getNowDateTime(“yyyy-MM-dd HH:mm:ss”);
mHelper.insert(info); // 往用户数据库添加登录成功的用户信息
}

  1. 用户进行登录时,根据输入手机号自动查找密码:

// 根据手机号码查询指定记录
public UserInfo queryByPhone(String phone) {
UserInfo info = null;
List infoList = query(String.format(“phone=‘%s’”, phone));
if (infoList.size() > 0) { // 存在该号码的登录信息
info = infoList.get(0);
}
return info;
}

3. 存储卡

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

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

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

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

//获取系统的公共存储路径
String publicPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString();

//获取系统的私有存储路径
String privatePath = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString();

boolean isLegacy = true;
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
//Android10的存储空间默认采用分区方式,这里是判断是使用传统方式还是分区方式
isLegacy = Environment.isExternalStorageLegacy();
}

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

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

// 把字符串保存到指定路径的文本文件
public static void saveText(String path, String txt) {
// 根据指定的文件路径构建文件输出流对象
try (FileOutputStream fos = new FileOutputStream(path)) {
fos.write(txt.getBytes()); // 把字符串写入文件输出流
} catch (Exception e) {
e.printStackTrace();
}
}
// 从指定路径的文本文件中读取内容字符串
public static String openText(String path) {
String readStr = “”;
// 根据指定的文件路径构建文件输入流对象
try (FileInputStream fis = new FileInputStream(path)) {
byte[] b = new byte[fis.available()];
fis.read(b); // 从文件输入流读取字节数组
readStr = new String(b); // 把字节数组转换为字符串
} catch (Exception e) {
e.printStackTrace();
}
return readStr; // 返回文本文件中的文本字符串
}

(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:

public static Bitmap openImage(String path) {
Bitmap bitmap = null; // 声明一个位图对象
// 根据指定的文件路径构建文件输入流对象
try (FileInputStream fis = new FileInputStream(path)) {
bitmap = BitmapFactory.decodeStream(fis); // 从文件输入流中解码位图数据
} catch (Exception e) {
e.printStackTrace();
}
return bitmap; // 返回图片文件中的位图数据
}

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

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

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

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

// 获取当前App的私有下载目录
String path = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() +
“/”;
// 从指定的资源文件中获取位图对象
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.huawei);
String file_path = path + DateUtil.getNowDateTime(“”) + “.jpeg”;
FileUtil.saveImage(file_path, bitmap); // 把位图对象保存为图片文件
tv_path.setText(“图片文件的保存路径为:\n” + file_path);

// 获取当前App的私有下载目录
mPath = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + “/”;
// 获得指定目录下面的所有图片文件
mFilelist = FileUtil.getFileList(mPath, new String[]{“.jpeg”});
if (mFilelist.size() > 0) {
// 打开并显示选中的图片文件内容
String file_path = mFilelist.get(0).getAbsolutePath();
tv_content.setText(“找到最新的图片文件,路径为”+file_path);
// 显示存储卡图片文件的第一种方式:直接调用setImageURI方法
//iv_content.setImageURI(Uri.parse(file_path)); // 设置图像视图的路径对象
// 第二种方式:先调用BitmapFactory.decodeFile获得位图,再调用setImageBitmap方法
//Bitmap bitmap = BitmapFactory.decodeFile(file_path);
//iv_content.setImageBitmap(bitmap); // 设置图像视图的位图对象
// 第三种方式:先调用FileUtil.openImage获得位图,再调用setImageBitmap方法
Bitmap bitmap = FileUtil.openImage(file_path);
iv_content.setImageBitmap(bitmap); // 设置图像视图的位图对象

4. 应用组件Application

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

  • 会频繁读取的信息:如用户名,手机号码等
  • 不方便通过intent传递的数据,如位图对象,非字符串的集合对象等。
  • 容易因频繁分配内存而导致内存泄漏的对象,如Handler处理器实例等。

image-20230115170457698

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

  1. 先继承Application,并获取唯一实例:

public class MyApplication extends Application {

private static MyApplication myApplication; //Application唯一实例

public Map<String, String> map = new HashMap<>(); //当作全局变量,用来存储数据

public static MyApplication getInstance(){
return myApplication;
}

@Override
public void onCreate() {
super.onCreate();

// 在打开应用时对静态的应用实例赋值
myApplication = this;
}
}

  1. 在AndroidManifest.xml 通过name属性添加该Application

image-20230117121213401

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

如在MainActivity6存储数据:

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

//存储数据
MyApplication myApplication = MyApplication.getInstance();
myApplication.map.put(“myKey”, “myValue”);

//跳转到MainActivity5
View bt5 = findViewById(R.id.bt5);
bt5.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity6.this, MainActivity5.class);
startActivity(intent);
}
});

}

在MainActivity5中获取数据:

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

TextView tv = findViewById(R.id.tv);
tv.setText(MyApplication.getInstance().map.get(“myKey”)); //成功获取到数据
}

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端代码:

public class UserInfoProvider extends ContentProvider {

//这里是上面实现的dbHelper,用来操作本地数据库
private UserDBHelper userDBHelper;

//初始化
@Override
public boolean onCreate() {
//初始化 dbHelper
userDBHelper = UserDBHelper.getInstance(getContext());

return true;
}

//插入
//uri格式:content://com.example.secondandroidapp.UserInfoProvider/user
@Override
public Uri insert(Uri uri, ContentValues values) {
//使用sqlite插入数据
SQLiteDatabase db = userDBHelper.getWritableDatabase();
db.insert(UserDBHelper.TABLE_NAME, null, values);

return uri;
}

//查询
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {

SQLiteDatabase db = userDBHelper.getReadableDatabase();
return db.query(UserDBHelper.TABLE_NAME, projection, selection, selectionArgs, null, null, null);
}

//删除
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
int count = 0;
switch (uriMatcher.match(uri)) {
//这种是uri不带参数:“content://com.example.secondandroidapp.UserInfoProvider/user”
case USER:
// 获取SQLite数据库的写连接
SQLiteDatabase db = userDBHelper.getWritableDatabase();
// 执行SQLite的删除操作,并返回删除记录的数目
count = db.delete(UserDBHelper.TABLE_NAME, selection,
selectionArgs);
db.close();
break;
//这种是uri带参数:“content://com.example.secondandroidapp.UserInfoProvider/user/2”
case USERS:
String id = uri.getLastPathSegment();
SQLiteDatabase db2 = userDBHelper.getWritableDatabase();
count = db2.delete(UserDBHelper.TABLE_NAME, “id = ?”, new String[]{id});
db2.close();
break;
}
return count;
}

@Override
public String getType(Uri uri) {
// TODO: Implement this to handle requests for the MIME type of the data
// at the given URI.
throw new UnsupportedOperationException(“Not yet implemented”);
}

@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// TODO: Implement this to handle requests to update one or more rows.
throw new UnsupportedOperationException(“Not yet implemented”);
}
}

  1. 利用ContentProvider只实现服务端App的数据封装,如果客户端App想访问对方的内部数据,就要通过内容解析器ContentResolver访问。

image-20230119194953733

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

Client的代码如下:

public class MainActivity7 extends AppCompatActivity {

private static Uri ContentUri = Uri.parse(“content://com.example.secondandroidapp.UserInfoProvider/user”);

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

Button insertButton = findViewById(R.id.insertButton);
insertButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ContentValues values = new ContentValues();
values.put(“name”, “陈鸿荣”);
values.put(“age”, “20”);
//获取到ContentResolver之后调用插入方法进行插入
getContentResolver().insert(ContentUri, values);
}
});

Button deleteButton = findViewById(R.id.deleteButton);
deleteButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// content://com.example.secondandroidapp.UserInfoProvider/user/2
Uri uri = ContentUris.withAppendedId(ContentUri, 2);
int count = getContentResolver().delete(uri, null, null);
}
});
}
}

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

在AndroidManifest.xml中添加如下:

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

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

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

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

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

调用ContextCompat的checkSelfPermission方法
2. 请求系统弹窗,以便用户选择是否开启权限:

调用ActivityCompat的requestPermissions方法,即可命令系统自动弹出权限申请窗口。
3. 判断用户的权限选择结果,是开启还是拒绝:

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

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

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

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

public class PermissionUtil {

//检查权限,返回true表示完全启用权限,返回false则表示为完全启用所有权限
public static boolean checkPermission(Activity activity, String[] permissions, int requestCode){

//Android6.0之后采取动态权限管理
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.M){
int check = PackageManager.PERMISSION_GRANTED; // 0

for (String permission : permissions) {
check = ContextCompat.checkSelfPermission(activity, permission);
if(check != PackageManager.PERMISSION_GRANTED){
break;
}
}
//如果未开启该权限,则请求系统弹窗,好让用户选择是否开启权限
if(check != PackageManager.PERMISSION_GRANTED){
//请求权限
ActivityCompat.requestPermissions(activity, permissions, requestCode);
return false;
}

return true;
}

return false;
}

//检查权限数组,返回true表示都已经授权
public static boolean checkGrant(int[] grantResults) {

if(grantResults != null){
for (int grant : grantResults) {
if(grant != PackageManager.PERMISSION_GRANTED){
return false;
}
}
return true;
}

return false;
}
}

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

public class PermissionLazyActivity extends AppCompatActivity {

//通讯录的读写权限
private static final String[] PERMISSION_CONTACT = {
Manifest.permission.READ_CONTACTS,
Manifest.permission.WRITE_CONTACTS
};

//短信的读写权限
private static final String[] PERMISSION_SMS = {
Manifest.permission.SEND_SMS,
Manifest.permission.RECEIVE_SMS
};

private static final int REQUEST_CODE_CONTACTS = 1;
private static final int REQUEST_CODE_SMS = 2;

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

//获取通讯录权限
findViewById(R.id.btn_contact).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
PermissionUtil.checkPermission(PermissionLazyActivity.this, PERMISSION_CONTACT, REQUEST_CODE_CONTACTS);
}
});

//获取短信权限
findViewById(R.id.btn_sms).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
PermissionUtil.checkPermission(PermissionLazyActivity.this, PERMISSION_SMS, REQUEST_CODE_SMS);
}
});
}

// 用户选择权限结果后会调用该回调方法
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);

switch (requestCode){
case REQUEST_CODE_CONTACTS:
if(PermissionUtil.checkGrant(grantResults)){
Log.d(“hhh”, “通讯录获取成功”);
}else{
Log.d(“hhh”, “通讯录获取失败”);
//跳转到设置界面
jumpToSettings();
}
break;
case REQUEST_CODE_SMS:
if(PermissionUtil.checkGrant(grantResults)){
Log.d(“hhh”, “短信权限获取成功”);
}else{
Log.d(“hhh”, “短信权限获取失败”);
//跳转到设置界面
jumpToSettings();
}
break;
}
}

//跳转到设置界面
private void jumpToSettings(){
Intent intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.fromParts(“package”, getPackageName(), null));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
}

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

效果如下:

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

public class PermissionHungryActivity extends AppCompatActivity {

//所需全部读写权限
private static final String[] PERMISSIONS = {
Manifest.permission.READ_CONTACTS,
Manifest.permission.WRITE_CONTACTS,
Manifest.permission.SEND_SMS,
Manifest.permission.RECEIVE_SMS
};

//
private static final int REQUEST_CODE_ALL = 0;

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

//检查是否拥有所有所需权限
PermissionUtil.checkPermission(this, PERMISSIONS, REQUEST_CODE_ALL);
}

// 用户选择权限结果后会调用该回调方法
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);

switch (requestCode){
case REQUEST_CODE_ALL:
if(PermissionUtil.checkGrant(grantResults)){
Log.d(“hhh”, “所有权限获取成功”);
}else{
//部分权限获取失败
for (int i = 0; i < grantResults.length; i++) {
if(grantResults[i] != PackageManager.PERMISSION_GRANTED){
//判断是什么权限获取失败
switch (permissions[i]){
case Manifest.permission.WRITE_CONTACTS:
case Manifest.permission.READ_CONTACTS:
Log.d(“hhh”, “通讯录获取失败”);
jumpToSettings();
break;
case Manifest.permission.SEND_SMS:
case Manifest.permission.RECEIVE_SMS:
Log.d(“hhh”, “短信权限获取失败”);
jumpToSettings();
break;
}
}
}
}
break;
}
}

//跳转到设置界面
private void jumpToSettings(){
Intent intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.fromParts(“package”, getPackageName(), null));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
}

(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进行关联

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

public class ContactActivity extends AppCompatActivity implements View.OnClickListener {

private EditText et_contact_name;
private EditText et_contact_phone;
private EditText et_contact_email;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_contact);
et_contact_name = findViewById(R.id.et_contact_name);
et_contact_phone = findViewById(R.id.et_contact_phone);
et_contact_email = findViewById(R.id.et_contact_email);
findViewById(R.id.btn_add_contact).setOnClickListener(this);
findViewById(R.id.btn_read_contact).setOnClickListener(this);
}

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_add_contact:
// 创建一个联系人对象
Contact contact = new Contact();
contact.name = et_contact_name.getText().toString().trim();
contact.phone = et_contact_phone.getText().toString().trim();
contact.email = et_contact_email.getText().toString().trim();

// 方式一,使用ContentResolver多次写入,每次一个字段
// addContacts(getContentResolver(), contact);

// 方式二,批处理方式
// 每一次操作都是一个 ContentProviderOperation,构建一个操作集合,然后一次性执行
// 好处是,要么全部成功,要么全部失败,保证了事务的一致性
addFullContacts(getContentResolver(), contact);

Toast.makeText(this, “添加联系人成功!”, Toast.LENGTH_SHORT).show();
break;

case R.id.btn_read_contact:
readPhoneContacts(getContentResolver());
break;
}
}

//往通讯录添加一个联系人信息(姓名,号码,邮箱)
private void addContacts(ContentResolver contentResolver, Contact contact) {
//得到rawContentId
ContentValues values = new ContentValues();
//插入记录得到id
Uri uri = contentResolver.insert(ContactsContract.RawContacts.CONTENT_URI, values);
long rawContentId = ContentUris.parseId(uri);

//插入名字
ContentValues name = new ContentValues();
//关联上面得到的联系人id
name.put(ContactsContract.Contacts.Data.RAW_CONTACT_ID, rawContentId);
//关联联系人姓名的类型
name.put(ContactsContract.Contacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
//关联联系人姓名
name.put(ContactsContract.Data.DATA2, contact.name);
contentResolver.insert(ContactsContract.Data.CONTENT_URI, name);

//插入电话号码
ContentValues phone = new ContentValues();
//关联上面得到的联系人id
phone.put(ContactsContract.Contacts.Data.RAW_CONTACT_ID, rawContentId);
//关联联系人电话号码的类型
phone.put(ContactsContract.Contacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
//关联联系人电话号码
phone.put(ContactsContract.Data.DATA1, contact.phone);
//指定该号码是家庭号码还是工作号码 (家庭)
phone.put(ContactsContract.Data.DATA2, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);
contentResolver.insert(ContactsContract.Data.CONTENT_URI, phone);

//插入邮箱
ContentValues email = new ContentValues();
//关联上面得到的联系人id
email.put(ContactsContract.Contacts.Data.RAW_CONTACT_ID, rawContentId);
//关联联系人邮箱的类型
email.put(ContactsContract.Contacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE);
//关联联系人邮箱
email.put(ContactsContract.Data.DATA1, contact.email);
//指定该号码是家庭邮箱还是工作邮箱
email.put(ContactsContract.Data.DATA2, ContactsContract.CommonDataKinds.Phone.TYPE_WORK);
contentResolver.insert(ContactsContract.Data.CONTENT_URI, email);
}

//事务操作,四个插入操作一次性提交
private void addFullContacts(ContentResolver contentResolver, Contact contact) {
//创建一个插入联系人主记录的内容操作器
ContentProviderOperation op_main = ContentProviderOperation
.newInsert(ContactsContract.RawContacts.CONTENT_URI)
//没有实际意义,不加这个会报错(不加这个导致没有创建ContentValue,导致报错)
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null)
.build();
//创建一个插入联系人姓名记录的内容操作器
ContentProviderOperation op_name = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
//将第0个操作的id,即raw_contacts中的id作为data表中的raw_contact_id
.withValueBackReference(ContactsContract.Contacts.Data.RAW_CONTACT_ID, 0)
.withValue(ContactsContract.Contacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.Data.DATA2, contact.name)
.build();
//创建一个插入联系人电话号码记录的内容操作器
ContentProviderOperation op_phone = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
//将第0个操作的id,即raw_contacts中的id作为data表中的raw_contact_id
.withValueBackReference(ContactsContract.Contacts.Data.RAW_CONTACT_ID, 0)
.withValue(ContactsContract.Contacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.Data.DATA1, contact.phone)
.withValue(ContactsContract.Data.DATA2, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE)
.build();

//创建一个插入联系人邮箱记录的内容操作器
ContentProviderOperation op_email = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
//将第0个操作的id,即raw_contacts中的id作为data表中的raw_contact_id
.withValueBackReference(ContactsContract.Contacts.Data.RAW_CONTACT_ID, 0)
.withValue(ContactsContract.Contacts.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.Data.DATA1, contact.email)
.withValue(ContactsContract.Data.DATA2, ContactsContract.CommonDataKinds.Phone.TYPE_WORK)
.build();

//全部放在集合中一次性提交
ArrayList operations = new ArrayList<>();
operations.add(op_main);
operations.add(op_name);
operations.add(op_phone);
operations.add(op_email);

try {
//批量提交四个操作
contentResolver.applyBatch(ContactsContract.AUTHORITY, operations);
} catch (OperationApplicationException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
}
}

//读取联系人
@SuppressLint(“Range”)
private void readPhoneContacts(ContentResolver contentResolver) {
//先查询raw_contacts表,再根据raw_contacts_id表 查询data表
Cursor cursor = contentResolver.query(ContactsContract.RawContacts.CONTENT_URI, new String[]{ContactsContract.RawContacts._ID}, null, null, null);
while(cursor.moveToNext()){
int rawContactId = cursor.getInt(0);
Uri uri = Uri.parse(“content://com.android.contacts/contacts/” + rawContactId + “/data”);
Cursor dataCursor = contentResolver.query(uri, new String[]{ContactsContract.Contacts.Data.MIMETYPE, ContactsContract.Contacts.Data.DATA1, ContactsContract.Contacts.Data.DATA2}, null, null, null);
Contact contact = new Contact();
while (dataCursor.moveToNext()) {
String data1 = dataCursor.getString(dataCursor.getColumnIndex(ContactsContract.Contacts.Data.DATA1));
String mimeType = dataCursor.getString(dataCursor.getColumnIndex(ContactsContract.Contacts.Data.MIMETYPE));
switch (mimeType) {
//是姓名
case ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE:
contact.name = data1;
break;

//邮箱
case ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE:
contact.email = data1;
break;

//手机
case ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE:
contact.phone = data1;
break;
}
}

dataCursor.close();

// RawContacts 表中出现的 _id,不一定在 Data 表中都会有对应记录
if (contact.name != null) {
Log.d(“hhh”, contact.toString());
}
}
cursor.close();
}

}

页面如下:

image-20230126163716728

(3)使用ContentObserver监听短信

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

image-20230126164316484

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

public class MonitorSmsActivity extends AppCompatActivity {

private SmsGetObserver mObserver;

@Override

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

data1;
break;

//手机
case ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE:
contact.phone = data1;
break;
}
}

dataCursor.close();

// RawContacts 表中出现的 _id,不一定在 Data 表中都会有对应记录
if (contact.name != null) {
Log.d(“hhh”, contact.toString());
}
}
cursor.close();
}

}

页面如下:

[外链图片转存中…(img-lbvzvGL1-1715723353091)]

(3)使用ContentObserver监听短信

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

image-20230126164316484

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

public class MonitorSmsActivity extends AppCompatActivity {

private SmsGetObserver mObserver;

@Override

[外链图片转存中…(img-ddwl6byq-1715723353091)]
[外链图片转存中…(img-9BUrhx3i-1715723353091)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 21
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值