简介
该组件主要用于在不同应用之间实现数据共享,不过该数据共享可以实现部分数据共享,比如打电话拨号只共享电话本相关信息
运行时权限
介绍和使用
权限分为两种危险权限和普通权限,如果是普通权限如果你的应用需要使用的话只需要使用以下方式赋予就行,如果是危险权限就需要在应用使用该权限时来决定是否赋予,也就是使其进行运行时权限的赋予。
1. 普通权限赋予
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
2. 危险权限运行时赋予(打电话权限授予)
- 在AndroidManifest.xml中赋予危险权限,这边是打电话权限
<uses-feature
android:name="android.hardware.telephony"
android:required="false" />
<uses-permission android:name="android.permission.CALL_PHONE" />
2.然后通过以下方式实现
- 然后通过ContextCompat.checkSelfPermission方法进行判断权限是否被赋予,该方法需要上传2个参数(当前activity,需要赋予的权限)然后将该方法的返回结果与PackageManager的权限判断常量进行判断(PERMISSION_GRANTED代码通过,PERMISSION_DENIED代表不通过)。
- 如果通过就直接调用call()方法,如果不通过就调用 ActivityCompat.requestPermissions方法,进行权限申请该方法所需参数(当前activity,new String[]{所需要的权限,唯一授权码可以随便弄一个只要唯一即可);
- 然后手机就会弹出弹框来询问你是否赋予权限,然后不管你同意还是不同意都会出发回调方法onRequestPermissionsResult
- 这边重写了会调函数,进行判断授权结果,如果同意了就会进行打电话方法,拒绝就会弹出弹框表示你拒绝了授予权限。
package com.example.storageplanapplication.activity.permissionTest;
public class PermissionActivity extends AppCompatActivity {
static int CallPhoneRequestCode=10001;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
Button jumpToCall=findViewById(R.id.make_call);
jumpToCall.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (ContextCompat.checkSelfPermission(PermissionActivity.this, Manifest.permission.CALL_PHONE)!=PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(PermissionActivity.this,new String[]{Manifest.permission.CALL_PHONE},CallPhoneRequestCode);
}else {
call();
}
}
});
}
private void call(){
try {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_CALL);
Uri uri = Uri.parse("tel:" + "18951307053");
intent.setData(uri);
startActivity(intent);
}catch (SecurityException e){
e.printStackTrace();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case 10001:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
call();
} else {
Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show();
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
}
3. 实现效果
危险权限
危险权限需要在运行时动态请求用户授权,如果用户未授权,则应用程序无法使用该权限。
注意:
下面的权限分成了好几个权限组所以一旦该危险权限同意赋予权限,那么该权限组的其他权限也将被一并赋予。
(1)SMS短信权限
SEND_SMS:发送短信
RECEIVE_SMS:接收短信
READ_SMS:读取短信
RECEIVE_WAP_PUSH:接收WAP Push消息
RECEIVE_MMS:接收彩信
(2)存储权限
READ_EXTERNAL_STORAGE:读取SD卡中的内容
WRITE_EXTERNAL_STORAGE:向SD卡中写入内容
(3)联系人权限
READ_CONTACTS:读取联系人
WRITE_CONTACTS:写入联系人
GET_ACCOUNTS:访问帐户列表
(4)手机权限
READ_PHONE_STATE:读取手机状态
CALL_PHONE:拨打电话
READ_CALL_LOG:读取通话记录
WRITE_CALL_LOG:写入通话记录
ADD_VOICEMAIL:添加语音信箱
USE_SIP:使用SIP协议进行网络电话
PROCESS_OUTGOING_CALLS:处理呼出电话
(5)日历权限
READ_CALENDAR:读取日历
WRITE_CALENDAR:写入日历
(6)相机权限
CAMERA:访问摄像头
(7)位置权限
ACCESS_FINE_LOCATION:访问精准位置
ACCESS_COARSE_LOCATION:访问大致位置
(8)传感器权限
BODY_SENSORS:访问传感器
(9)麦克风权限
RECORD_AUDIO:录音
普通权限
普通权限不需要动态请求用户授权,只需要在AndroidManifest.xml文件中声明即可。
- ACCESS_CHECKIN_PROPERTIES:读取和写入“properties”表在checkin数据库中
- ACCESS_LOCATION_EXTRA_COMMANDS:访问额外的位置提供命令
- ACCESS_NETWORK_STATE:获取网络信息状态
- ACCESS_NOTIFICATION_POLICY:希望访问通知策略的应用程序的标记许可
- ACCESS_WIFI_STATE:获取当前WiFi接入的状态以及WLAN热点的信息
- ACCOUNT_MANAGER:通过账户验证方式访问账户管理ACCOUNT_MANAGER相关信息
- BATTERY_STATS:更新手机电池统计信息
- BIND_ACCESSIBILITY_SERVICE:请求accessibilityservice服务
- BIND_APPWIDGET:告诉appWidget服务需要访问小插件的数据库
- BIND_CARRIER_MESSAGING_SERVICE:绑定到运营商应用程序中的服务
- BIND_CARRIER_SERVICES:绑定到运营商应用程序中的服务
- BIND_CHOOSER_TARGET_SERVICE:由ChooserTargetService要求的服务
- BIND_DEVICE_ADMIN:请求系统管理员receiver
- BIND_DREAM_SERVICE:由一个DreamService要求的服务
- BIND_INCALL_SERVICE:请求MidiDeviceService服务
- BIND_INPUT_METHOD:请求InputMethodService服务
- BIND_MIDI_DEVICE_SERVICE:由一MidiDeviceService要求的服务
- BIND_NFC_SERVICE:由HostApduServiceOffHostApduService要求的服务
- BIND_NOTIFICATION_LISTENER_SERVICE:由notificationlistenerservice要求的服务
- BIND_PRINT_SERVICE:由printservice要求的服务
- BIND_REMOTEVIEWS:通过RemoteViewsService服务请求
- BIND_TELECOM_CONNECTION_SERVICE:由ConnectionService要求的服务
- BIND_TEXT_SERVICE:由textservice要求的服务
- BIND_TV_INPUT:由TvInputService要求的服务
- BIND_VOICE_INTERACTION:由VoiceInteractionService要求的服务
- BIND_VPN_SERVICE:通过VpnService服务请求
- BIND_WALLPAPER:通过WallpaperService服务请求
- BLUETOOTH:连接配对过的蓝牙设备
- BLUETOOTH_ADMIN:发现和配对新的蓝牙设备
- BLUETOOTH_PRIVILEGED:配对蓝牙设备,无需用户交互
- BROADCAST_PACKAGE_REMOVED:广播一个提示消息在一个应用程序包已经移除后
- BROADCAST_SMS:当收到短信时触发广播
- BROADCAST_STICKY:收到广播后快速收到下一个广播
- BROADCAST_WAP_PUSH:WAP PUSH服务收到后触发广播
- CALL_PRIVILEGED:拨打电话,替换系统的拨号器界面
- CAPTURE_AUDIO_OUTPUT:捕获音频输出
- CAPTURE_SECURE_VIDEO_OUTPUT:捕获视频输出
- CAPTURE_VIDEO_OUTPUT:捕获视频输出
- CHANGE_COMPONENT_ENABLED_STATE:改变组件是否启用状态
- CHANGE_CONFIGURATION:改变配置信息
- CHANGE_NETWORK_STATE:改变网络状态,如是否联网
- CHANGE_WIFI_MULTICAST_STATE:改变WiFi多播状态
- CHANGE_WIFI_STATE:改变WiFi状态
- CLEAR_APP_CACHE:清除应用缓存
- CONTROL_LOCATION_UPDATES:获得移动网络定位信息
- DELETE_CACHE_FILES:删除缓存文件
- DELETE_PACKAGES:删除应用
- DIAGNOSTIC:RW到诊断资源
- DISABLE_KEYGUARD:禁用键盘锁
- DUMP:获取系统dump信息
- EXPAND_STATUS_BAR:扩展或收缩状态栏
- FACTORY_TEST:运行工厂测试模式
- FLASHLIGHT:访问闪光灯
- GET_ACCOUNTS_PRIVILEGED:访问帐户服务中的帐户列表
- GET_PACKAGE_SIZE:获取任何package占用空间容量
- GET_TASKS:获取信息有关当前或最近运行的任务
- GLOBAL_SEARCH:允许全局搜索
- INSTALL_LOCATION_PROVIDER:安装定位提供
- INSTALL_PACKAGES:安装应用
- INSTALL_SHORTCUT:创建快捷方式
- INTERNET:访问网络连接
- KILL_BACKGROUND_PROCESSES:结束后台进程
- LOCATION_HARDWARE:使用定位功能的硬件
- MANAGE_DOCUMENTS:管理文档访问
- MASTER_CLEAR:执行软格式化,删除系统配置信息
- MEDIA_CONTENT_CONTROL:控制播放和内容
- MODIFY_AUDIO_SETTINGS:修改声音设置信息
- MODIFY_PHONE_STATE:修改电话状态
- MOUNT_FORMAT_FILESYSTEMS:格式化可移动文件系统
- MOUNT_UNMOUNT_FILESYSTEMS:挂载、反挂载外部文件系统
- NFC:执行NFC近距离通讯操作
- PACKAGE_USAGE_STATS:设置他的activities显示
- PERSISTENT_ACTIVITY:创建一个永久的Activity
- READ_FRAME_BUFFER:读取帧缓存
- READ_INPUT_STATE:读取当前键的输入状态
- READ_LOGS:读取系统底层日志
- READ_SYNC_SETTINGS:读取同步设置
- READ_SYNC_STATS:读取同步状态
- READ_VOICEMAIL:读取语音邮件
- REBOOT:重新启动设备
- RECEIVE_BOOT_COMPLETED:开机自动运行
- REORDER_TASKS:重新排序系统Z轴运行中的任务
- REQUEST_IGNORE_BATTERY_OPTIMIZATIONS:请求忽略电池优化
- REQUEST_INSTALL_PACKAGES:请求安装包
- RESTART_PACKAGES:结束任务
- SEND_RESPOND_VIA_MESSAGE:即时的短信息回复
- SET_ALARM:设置闹铃提醒
- SET_ALWAYS_FINISH:程序在后台是否总是退出
- SET_ANIMATION_SCALE:设置全局动画缩放
- SET_DEBUG_APP:设置调试程序
- SET_PREFERRED_APPLICATIONS:设置应用的参数
- SET_PROCESS_LIMIT:设置最大的进程数量的限制
- SET_TIME:设置系统时间
- SET_TIME_ZONE:设置系统时区
- SET_WALLPAPER:设置桌面壁纸
- SET_WALLPAPER_HINTS:设置壁纸建议
- SIGNAL_PERSISTENT_PROCESSES:发送一个永久的进程信号
- STATUS_BAR:打开、关闭、禁用状态栏
- SYSTEM_ALERT_WINDOW:显示系统窗口
- TRANSMIT_IR:使用设备的红外发射器
- UNINSTALL_SHORTCUT:删除快捷方式
- UPDATE_DEVICE_STATS:更新设备状态
- USE_FINGERPRINT:使用指纹硬件
- VIBRATE:允许程序振动
- WAKE_LOCK :允许程序在手机屏幕关闭后后台进程仍然运行
- WRITE_APN_SETTINGS:允许程序写入网络GPRS接入点设置
- WRITE_GSERVICES:允许程序修改Google服务地图
- WRITE_SECURE_SETTINGS:允许应用程序读取或写入安全系统设置
- WRITE_SETTINGS:允许程序读取或写入系统设置 WRITE_SYNC_SETTINGS:允许程序写入同步设置
- WRITE_VOICEMAIL:允许应用程序修改和删除系统中的现有的语音邮件,只有系统才能使用请输入代码块名称
相关名词解释
Uri
它就是一个应用ContentProvider的唯一标示通过它可以定位应用,
组成
URI 为系统中的每一个资源赋予一个名字,比方说通话记录。每一个 ContentProvider 都拥有一个公共的 URI,用于表示 ContentProvider 所提供的数据。URI 的格式如下:
content://com.wang.provider.myprovider/tablename/id:
- Scheme(方案): URI 的方案部分指定了要使用的协议,通常是 "content://",表示使用 ContentProvider 协议。其他常见的方案还包括 "http://"、"https://"、"file://" 等。
- Authority(权限):用于唯一标识这个 ContentProvider,外部调用者可以根据这个标识来找到它。对于第三方应用程序,为了保证 URI 标识的唯一性,它必须是一个完整的、小写的类名。这个标识在元素的authorities属性中说明,一般是定义该 ContentProvider 的包.类的名称;
- Path(路径): URI 的路径部分用于指定要操作的数据的路径。路径可以包含一个或多个路径段,每个路径段之间使用斜杠 "/" 分隔。路径段可以是表名、数据集的目录结构或其他标识符。
- id:通常指的是指定表的id
综合起来,一个完整的 ContentProvider URI 的格式为:
content://<authority>/<path>/id
// 查询table1中id为1的数据
content://com.example.sql/table1/1
通配符
- *:表示匹配任意长度的任何字符
- #:表示匹配任何长度的数字
## 1. 匹配任何表内容的URI,比如查询每个表中id为1的内容
content://com.example.app.provider/*
## 2. 匹配table1表任何一行数据的内容的URI,(比如id为1)
content://com.example.app.provider/table1/#*
MIME类型
MIME(Multipurpose Internet Mail Extensions)类型是一种用于标识文件格式和互联网媒体类型的标准。在 Android 中,MIME 类型通常用于描述数据的类型和格式,特别是在 ContentProvider 中用于标识数据的类型。MIME 类型通常由两部分组成:主类型(主要类别)和子类型(具体细分)。
MIME 类型的一般格式为:主类型/子类型
例如:
- text/plain 表示纯文本数据。
- image/jpeg 表示 JPEG 图像数据。
- audio/mpeg 表示 MP3 音频数据。
- video/mp4 表示 MP4 视频数据。
- application/pdf 表示 PDF 文档数据。
MIME 类型在 Android 中广泛用于标识数据的类型,比如在 Intent 中传递数据时,用于指定要传递的数据的类型;在 ContentProvider 中,用于指定数据的 MIME 类型以便其他应用程序能够正确处理数据。因此,了解和正确使用 MIME 类型是 Android 开发中的重要内容。
ContentResolver
概念
该类主要就是用于从其他应用的ContentProvider中查询或者处理相关数据库数据的,也就是说主要用于从其他应用的ContentProvder中读取ContentProvder能够提供给我们的数据.
使用方法
- 查询
// 打印查询结果
Uri uri=Uri.parse("content://com.example.app.provider/table");
// 2. 先在通过指针就可以查询之指定应用的数据了
Cursor cursor = getContentResolver().query(Uri uri, String[] projection, String[] selectionArgs, @Nullable String sortOrder,
CancellationSignal cancellationSignal) );
if (cursor != null && cursor.getCount() > 0) {
while (cursor.moveToNext()) {
String name = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
Log.d("Contact", "Name: " + name);
}
cursor.close();
}
- 添加数据
// 插入联系人信息
ContentResolver resolver = getContentResolver();
ContentValues values = new ContentValues();
values.put("colume", null);
Uri uri=Uri.parse("content://com.example.app.provider/table");
resolver.insert(uri, values);
- 修改
// 更新联系人信息
ContentResolver resolver = getContentResolver();
ContentValues values = new ContentValues();
values.put("column1", "Jane");
Uri uri=Uri.parse("content://com.example.app.provider/table");
resolver.update(uri,values,"column1=? and column2 =?",new String[]{"text","1"});
- 删除
// 删除联系人信息
ContentResolver resolver = getContentResolver();
Uri uri=Uri.parse("content://com.example.app.provider/table");
resolver.delete(uri,"column2 = ?",new String[]{"1"});
使用例子(实现获取电话本联系人电话号码和备注名字)
- 在AndroidManifest.xml中赋予危险权限,这边是获取电话本权限
<uses-permission android:name="android.permission.READ_CONTACTS" />
- 如下是activity的实现逻辑
<?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:id="@+id/main"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.ContentProviderTest.ContentResolverActivity">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/contacts"/>
</LinearLayout>
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import android.util.Log;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 获取 ContentResolver 对象
ContentResolver resolver = getContentResolver();
// 查询联系人信息
Cursor cursor = resolver.query(ContactsContract.Contacts.CONTENT_URI,
null, null, null, null);
// 打印查询结果
if (cursor != null && cursor.getCount() > 0) {
while (cursor.moveToNext()) {
String name = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
Log.d("Contact", "Name: " + name);
}
cursor.close();
}
// 插入联系人信息
ContentValues values = new ContentValues();
values.put(ContactsContract.RawContacts.ACCOUNT_TYPE, null);
values.put(ContactsContract.RawContacts.ACCOUNT_NAME, null);
Uri rawContactUri = resolver.insert(ContactsContract.RawContacts.CONTENT_URI, values);
long rawContactId = ContentUris.parseId(rawContactUri);
values.clear();
values.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId);
values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
values.put(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, "John");
resolver.insert(ContactsContract.Data.CONTENT_URI, values);
// 更新联系人信息
values.clear();
values.put(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, "Jane");
String selection = ContactsContract.Data.RAW_CONTACT_ID + " = ? AND " +
ContactsContract.Data.MIMETYPE + " = ?";
String[] selectionArgs = new String[]{String.valueOf(rawContactId),
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE};
resolver.update(ContactsContract.Data.CONTENT_URI, values, selection, selectionArgs);
// 删除联系人信息
resolver.delete(ContactsContract.RawContacts.CONTENT_URI,
ContactsContract.RawContacts._ID + " = ?", new String[]{String.valueOf(rawContactId)});
}
}
- 实现效果如下
自定义ContentProvider
生命周期方法:
- onCreate():
-
- 当ContentProvider被创建时调用。
- 你可以在这个方法中执行一些初始化工作,比如初始化数据库连接或者其他资源。
- query():
-
- 当其他应用程序请求查询数据时调用。
- 你需要在这个方法中实现查询逻辑,并返回查询结果的Cursor对象。
- insert():
-
- 当其他应用程序请求插入数据时调用。
- 你需要在这个方法中实现插入数据的逻辑,并返回新插入数据的URI。
- update():
-
- 当其他应用程序请求更新数据时调用。
- 你需要在这个方法中实现更新数据的逻辑,并返回受影响的行数。
- delete():
-
- 当其他应用程序请求删除数据时调用。
- 你需要在这个方法中实现删除数据的逻辑,并返回受影响的行数。
- getType():
-
- 当其他应用程序请求获取数据的MIME类型时调用。
- 你需要在这个方法中返回所请求数据的MIME类型。
- shutdown():
-
- 当ContentProvider不再需要使用时调用。
- 这个方法通常在ContentProvider不再使用时释放资源或者执行清理操作。
创建自定义ContentProvider
简要概括一下事实上还是在各个生命周期中书写前面SQLite的各种操纵数据库的语句根本没有区别.
重写ContentProvider的增删改查生命周期
public class MyContentProvider extends ContentProvider {
public static final int BOOK_DIR = 0;
public static final int BOOK_ITEM = 1;
public static final int CATEGORY_DIR = 2;
public static final int CATEGORY_ITEM = 3;
public static final String AUTHORITY = "com.example.databasetest.provider";
private static UriMatcher uriMatcher;
// 自定义的数据库控制类(详细过程见数据存储全方案章节)
private static MyDataBaseHelper dbHelper;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, "book", BOOK_DIR);
uriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM);
uriMatcher.addURI(AUTHORITY, "category", CATEGORY_DIR);
uriMatcher.addURI(AUTHORITY, "category/#", CATEGORY_ITEM);
}
public MyContentProvider() {
}
@Override
public boolean onCreate() {
dbHelper = new MyDataBaseHelper(getContext(), "BookStore.db", null, 1);
return true;
}
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor = null;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
cursor = db.query("Book", projection, selection, selectionArgs, null, null, sortOrder);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
cursor = db.query("Book", projection, "id = ?", new String[]{bookId}, null, null, sortOrder);
break;
case CATEGORY_DIR:
cursor = db.query("Category", projection, selection, selectionArgs, null, null, sortOrder);
break;
case CATEGORY_ITEM:
String CategoryId = uri.getPathSegments().get(1);
cursor = db.query("Category", projection, "id= ?", new String[]{CategoryId}, null, null, sortOrder);
break;
default:
break;
}
return cursor;
}
@Override
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
// Implement this to handle requests to delete one or more rows.
SQLiteDatabase db = dbHelper.getWritableDatabase();
int deleteRows = 0;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
deleteRows = db.delete("Book", selection, selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
deleteRows = db.delete("Book", "id=?", new String[]{bookId});
break;
case CATEGORY_DIR:
deleteRows = db.delete("Category", selection, selectionArgs);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
deleteRows = db.delete("Category", "id=?", new String[]{categoryId});
break;
default:
break;
}
return deleteRows;
}
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
Uri uriReturn = null;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
case BOOK_ITEM:
long newBookId = db.insert("Book", null, values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + newBookId);
break;
case CATEGORY_DIR:
case CATEGORY_ITEM:
long newCategoryId = db.insert("Book", null, values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/category/" + newCategoryId);
break;
default:
break;
}
return uriReturn;
}
@Override
public int update(@NonNull Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int updatedRows = 0;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
updatedRows = db.update("Book", values, selection, selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
updatedRows = db.update("Book", values, "id=?", new String[]{bookId});
break;
case CATEGORY_DIR:
updatedRows = db.update("Category", values, selection, selectionArgs);
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
updatedRows = db.update("Category", values, "id=?", new String[]{categoryId});
break;
default:
break;
}
return updatedRows;
}
}
重写getType()
重写该方法主要用于获得与Uri对应的MIME类型
public class MyContentProvider extends ContentProvider {
public static final int BOOK_DIR = 0;
public static final int BOOK_ITEM = 1;
public static final int CATEGORY_DIR = 2;
public static final int CATEGORY_ITEM = 3;
public static final String AUTHORITY = "com.example.databasetest.provider";
private static UriMatcher uriMatcher;
// 自定义的数据库控制类(详细过程见数据存储全方案章节)
private static MyDataBaseHelper dbHelper;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, "book", BOOK_DIR);
uriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM);
uriMatcher.addURI(AUTHORITY, "category", CATEGORY_DIR);
uriMatcher.addURI(AUTHORITY, "category/#", CATEGORY_ITEM);
}
....
@Override
public String getType(@NonNull Uri uri) {
switch (uriMatcher.match(uri)){
case BOOK_DIR:
return "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.Book";
case BOOK_ITEM:
return "vnd.android.cursor.item/vnd.com.example.databasetest.provider.Book";
case CATEGORY_DIR:
return "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.Category";
case CATEGORY_ITEM:
return "vnd.android.cursor.item/vnd.com.example.databasetest.provider.Category";
default:
break;
}
return null;
}
...
}
实现跨程序数据共享
大概意思就是我在自定义的一个应用上自定义数据库并且自定义ContentProvider,然后再创建一个自定义android应用通过ContentResolver去获取相关数据并进行增删改查操作,并且这些操作会同步到第一个应用中的数据库中去.
实现步骤
这边省略创建数据库相关类
1. 在第一个应用上创建自定义ContentProvider
package com.example.storageplanapplication.activity.broadcasttest;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import com.example.storageplanapplication.activity.SQLiteTest.MyDataBaseHelper;
public class MyContentProvider extends ContentProvider {
public static final int BOOK_DIR = 0;
public static final int BOOK_ITEM = 1;
public static final int CATEGORY_DIR = 2;
public static final int CATEGORY_ITEM = 3;
public static final String AUTHORITY = "com.example.databasetest.provider";
private static UriMatcher uriMatcher;
private static MyDataBaseHelper dbHelper;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, "book", BOOK_DIR);
uriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM);
uriMatcher.addURI(AUTHORITY, "category", CATEGORY_DIR);
uriMatcher.addURI(AUTHORITY, "category/#", CATEGORY_ITEM);
}
public MyContentProvider() {
}
@Override
public boolean onCreate() {
dbHelper = new MyDataBaseHelper(getContext(), "BookStore.db", null, 1);
return true;
}
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor = null;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
cursor = db.query("Book", projection, selection, selectionArgs, null, null, sortOrder);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
cursor = db.query("Book", projection, "id = ?", new String[]{bookId}, null, null, sortOrder);
break;
case CATEGORY_DIR:
cursor = db.query("Category", projection, selection, selectionArgs, null, null, sortOrder);
break;
case CATEGORY_ITEM:
String CategoryId = uri.getPathSegments().get(1);
cursor = db.query("Category", projection, "id= ?", new String[]{CategoryId}, null, null, sortOrder);
break;
default:
break;
}
return cursor;
}
@Override
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
// Implement this to handle requests to delete one or more rows.
SQLiteDatabase db = dbHelper.getWritableDatabase();
int deleteRows = 0;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
deleteRows = db.delete("Book", selection, selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
deleteRows = db.delete("Book", "id=?", new String[]{bookId});
break;
case CATEGORY_DIR:
deleteRows = db.delete("Category", selection, selectionArgs);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
deleteRows = db.delete("Category", "id=?", new String[]{categoryId});
break;
default:
break;
}
return deleteRows;
}
@Override
public String getType(@NonNull Uri uri) {
switch (uriMatcher.match(uri)){
case BOOK_DIR:
return "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.Book";
case BOOK_ITEM:
return "vnd.android.cursor.item/vnd.com.example.databasetest.provider.Book";
case CATEGORY_DIR:
return "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.Category";
case CATEGORY_ITEM:
return "vnd.android.cursor.item/vnd.com.example.databasetest.provider.Category";
default:
break;
}
return null;
}
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
Uri uriReturn = null;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
case BOOK_ITEM:
long newBookId = db.insert("Book", null, values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + newBookId);
break;
case CATEGORY_DIR:
case CATEGORY_ITEM:
long newCategoryId = db.insert("Book", null, values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/category/" + newCategoryId);
break;
default:
break;
}
return uriReturn;
}
@Override
public int update(@NonNull Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int updatedRows = 0;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
updatedRows = db.update("Book", values, selection, selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
updatedRows = db.update("Book", values, "id=?", new String[]{bookId});
break;
case CATEGORY_DIR:
updatedRows = db.update("Category", values, selection, selectionArgs);
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
updatedRows = db.update("Category", values, "id=?", new String[]{categoryId});
break;
default:
break;
}
return updatedRows;
}
}
2. 查看AndroidManifest.xml是否已经注册了contentProvider并且编写访问provider的权限,这边name随便定义,不过一般是应用包名+provider类名
<permission
android:name="com.example.storageplanapplication.permission._READ_PERMISSION"
android:protectionLevel="normal" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@drawable/zhibao"
android:label="@string/app_name"
android:roundIcon="@drawable/zhibao"
android:supportsRtl="true"
android:theme="@style/Theme.StoragePlanApplication"
tools:ignore="MissingClass"
tools:targetApi="31">
<provider
android:name=".activity.broadcasttest.MyContentProvider"
android:authorities="com.example.databasetest.provider"
android:enabled="true"
android:exported="true">
</provider>
....
</application>
3. 在第二个应用的AndroidManifest.xml文件中配置访问权限获取第一个应用的provider,<queries>中的name与上面应用中provider的authorities属性对应
<uses-permission android:name="com.example.storageplanapplication.permission._READ_PERMISSION" />
<!-- 指定要获取第一个应用的哪一个provider-->
<queries>
<provider android:authorities="com.example.databasetest.provider" />
</queries>
4. 最后就是通过contentResolver实现跨应用的数据查询和处理
package com.example.providertest;
import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.content.DialogInterface;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.example.providertest.adapter.MyAdapter;
import com.example.providertest.entity.Book;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MainActivity extends AppCompatActivity {
String result = null;
MyAdapter adapter;
private String newId;
private RecyclerView mRecyclerView;
List<Book> dataList = new ArrayList<>();
String bookUri = "content://com.example.databasetest.provider/book";
@SuppressLint("MissingInflatedId")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
Button addData = findViewById(R.id.addData);
/**
* 添加数据库
*/
addData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
openAddAlertDialog();
}
});
//查询数据
Uri uri = Uri.parse(bookUri);
dataList = query(uri);
RecyclerView recyclerView = findViewById(R.id.bookList);
adapter = new MyAdapter(dataList) {
// 修改数据
@Override
public void updateItem(int position) {
Book updateItem = adapter.getmDataList().get(position);
// 获取需要删除的数据项的URI
Uri updateUri = Uri.parse(bookUri + "/" + updateItem.getId());
// 使用ContentResolver删除数据
// 获取布局填充器
final LayoutInflater[] inflater = {LayoutInflater.from(MainActivity.this)};
// 加载自定义布局
View dialogView = inflater[0].inflate(R.layout.add_dialog_item, null);
// 获取输入框
final EditText author = dialogView.findViewById(R.id.author);
final EditText name = dialogView.findViewById(R.id.name);
final EditText price = dialogView.findViewById(R.id.price);
final EditText pages = dialogView.findViewById(R.id.pages);
author.setText(updateItem.getAuthor());
name.setText(updateItem.getName());
price.setText(String.valueOf(updateItem.getPrice()));
pages.setText(String.valueOf(updateItem.getPages()));
// 构建对话框
AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(MainActivity.this);
alertDialogBuilder.setTitle("修改数据");
alertDialogBuilder.setView(dialogView);
alertDialogBuilder.setCancelable(false);
alertDialogBuilder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
HashMap<String, String> hashMap = new HashMap();
hashMap.put("author", author.getText().toString());
hashMap.put("name", name.getText().toString());
hashMap.put("price", price.getText().toString());
hashMap.put("pages", pages.getText().toString());
String updateResult = null;
updateResult = String.valueOf(update(updateUri, hashMap, null, null));
if (updateResult != null) {
Book book = adapter.getmDataList().get(position);
book.setAuthor(author.getText().toString());
book.setName(name.getText().toString());
book.setPages(Integer.parseInt(pages.getText().toString()));
book.setPrice(Double.parseDouble((price.getText().toString())));
adapter.getmDataList().set(position,book);
notifyItemChanged(position);
Toast.makeText(MainActivity.this, "数据修改成功", Toast.LENGTH_SHORT).show();
}
}
});
alertDialogBuilder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
// 创建并显示对话框
AlertDialog alertDialog = alertDialogBuilder.create();
alertDialog.show();
}
// 删除数据
@Override
public void deleteItem(int position) {
Book deletedItem = adapter.getmDataList().get(position);
// 获取需要删除的数据项的URI
Uri deleteUri = Uri.parse(bookUri + "/" + deletedItem.getId());
// 使用ContentResolver删除数据
int rowsDeleted = delete(deleteUri, null, null);
if (rowsDeleted > 0) {
adapter.getmDataList().remove(position);
notifyItemRemoved(position);
Toast.makeText(MainActivity.this, "删除成功", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, "删除失败", Toast.LENGTH_SHORT).show();
}
}
};
recyclerView.setAdapter(adapter);
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL);
recyclerView.addItemDecoration(dividerItemDecoration);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
}
@Override
protected void onResume() {
super.onResume();
}
/**
* 添加数据
*
* @param uri
* @param hashMap
* @return book id
*/
public String insert(Uri uri, HashMap<String, String> hashMap) {
ContentValues values = new ContentValues();
for (Map.Entry<String, String> entry : hashMap.entrySet()) {
values.put(entry.getKey(), entry.getValue());
}
Uri newUri = getContentResolver().insert(uri, values);
newId = newUri.getPathSegments().get(1);
return newId;
}
/**
* 查询数据
*
* @param uri
* @param
* @return book列表
*/
public List<Book> query(Uri uri) {
List<Book> books = new ArrayList<>();
Cursor cursor = getContentResolver().query(uri, null, null, null, null, null);
if (cursor != null) {
while (cursor.moveToNext()) {
@SuppressLint("Range")
int id = cursor.getInt(cursor.getColumnIndex("id"));
@SuppressLint("Range")
String name = cursor.getString(cursor.getColumnIndex("name"));
@SuppressLint("Range")
String author = cursor.getString(cursor.getColumnIndex("author"));
@SuppressLint("Range")
int pages = cursor.getInt(cursor.getColumnIndex("pages"));
@SuppressLint("Range")
double price = cursor.getDouble(cursor.getColumnIndex("price"));
books.add(new Book(name, author, pages, price, id));
}
cursor.close();
}
return books;
}
/**
* 修改数据
*
* @param uri
* @param hashMap
* @param where
* @param selectionArgs
* @return
*/
public int update(Uri uri, HashMap<String, String> hashMap, String where,
String[] selectionArgs) {
ContentValues values = new ContentValues();
for (Map.Entry<String, String> entry : hashMap.entrySet()) {
values.put(entry.getKey(), entry.getValue());
}
return getContentResolver().update(uri, values, where, selectionArgs);
}
/**
* 删除
*
* @param uri
* @param where
* @param selectionArgs
* @return
*/
public int delete(Uri uri, String where,
String[] selectionArgs) {
return getContentResolver().delete(uri, where, selectionArgs);
}
/*
添加窗口
*/
private void openAddAlertDialog() {
// 获取布局填充器
final LayoutInflater[] inflater = {LayoutInflater.from(MainActivity.this)};
// 加载自定义布局
View dialogView = inflater[0].inflate(R.layout.add_dialog_item, null);
// 获取输入框
final EditText author = dialogView.findViewById(R.id.author);
final EditText name = dialogView.findViewById(R.id.name);
final EditText price = dialogView.findViewById(R.id.price);
final EditText pages = dialogView.findViewById(R.id.pages);
// 构建对话框
AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(MainActivity.this);
alertDialogBuilder.setTitle("添加数据");
alertDialogBuilder.setView(dialogView);
alertDialogBuilder.setCancelable(false);
alertDialogBuilder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
HashMap<String, String> hashMap = new HashMap();
hashMap.put("author", author.getText().toString());
hashMap.put("name", name.getText().toString());
hashMap.put("price", price.getText().toString());
hashMap.put("pages", pages.getText().toString());
Uri uri = Uri.parse("content://com.example.databasetest.provider/book");
result = insert(uri, hashMap);
if (result != null) {
dataList = query(uri);
adapter.addData(dataList.get(dataList.size() - 1));
Toast.makeText(MainActivity.this, "数据库添加成功", Toast.LENGTH_SHORT).show();
}
}
});
alertDialogBuilder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
// 创建并显示对话框
AlertDialog alertDialog = alertDialogBuilder.create();
alertDialog.show();
}
}
<?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:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
<Button
android:id="@+id/addData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="添加Book表数据" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="11"
android:orientation="vertical"
>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="显示列表"
android:textSize="20sp"
android:gravity="center"
/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/bookList"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>
</LinearLayout>
</LinearLayout>
5. 因为这边通过recycleView实现数据查询的显示,所以以下是RecyleView相关代码
- 实体类
package com.example.providertest.entity;
public class Book {
private int id;
private String name;
private String author;
private int pages;
private double price;
public Book(String book, String author, int pages, double price,int id) {
this.name = book;
this.author = author;
this.pages = pages;
this.price = price;
this.id=id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public int getPages() {
return pages;
}
public void setPages(int pages) {
this.pages = pages;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@Override
public String toString() {
return "Book{" +
"id=" + id +
", name='" + name + '\'' +
", author='" + author + '\'' +
", pages=" + pages +
", price=" + price +
'}';
}
}
- recylce自定义适配器类
package com.example.providertest.adapter;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.net.Uri;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.example.providertest.MainActivity;
import com.example.providertest.R;
import com.example.providertest.entity.Book;
import org.w3c.dom.Text;
import java.util.List;
public abstract class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
private List<Book> mDataList;
private MainActivity mainActivity;
// 构造函数,接收数据源
public MyAdapter(List<Book> dataList) {
this.mDataList = dataList;
this.mainActivity=new MainActivity();
}
public List<Book> getmDataList() {
return mDataList;
}
public void setmDataList(List<Book> mDataList) {
this.mDataList = mDataList;
}
// 创建ViewHolder
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycle_item, parent, false);
return new ViewHolder(view);
}
public void addData(Book item) {
mDataList.add(item);
notifyItemInserted(mDataList.size() - 1); // 通知插入了新的数据项
}
public abstract void updateItem(int position) ;
public abstract void deleteItem(int position) ;
// 绑定数据到ViewHolder
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, @SuppressLint("RecyclerView") int position) {
Book item = mDataList.get(position);
holder.bind(item);
holder.delete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i("position", String.valueOf(position));
Log.i("position1", String.valueOf(mDataList.get(position)));
deleteItem(position);
}
});
holder.update.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
updateItem(position);
}
});
}
// 返回数据项的数量
@Override
public int getItemCount() {
return mDataList.size();
}
// ViewHolder类
public static class ViewHolder extends RecyclerView.ViewHolder {
TextView name;
TextView author;
TextView pages;
TextView price;
TextView id;
Button delete;
Button update;
Book book;
public ViewHolder(View itemView) {
super(itemView);
name = itemView.findViewById(R.id.itemname);
author = itemView.findViewById(R.id.itemauthor);
pages = itemView.findViewById(R.id.itempages);
price = itemView.findViewById(R.id.itemprice);
id=itemView.findViewById(R.id.itemid);
delete=itemView.findViewById(R.id.deleteItem);
update=itemView.findViewById(R.id.updateItem);
}
// 将数据绑定到视图
@SuppressLint("SetTextI18n")
public void bind(Book book) {
this.book=book;
id.setText("id: "+String.valueOf(book.getId()));
name.setText(book.getName());
author.setText(book.getAuthor());
pages.setText(String.valueOf(book.getPages())+"页");
price.setText(String.valueOf(book.getPrice())+"元");
}
}
}
- recycle单个item类
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<TextView
android:id="@+id/itemid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginLeft="12dp"
android:layout_marginBottom="30dp"
android:text="1"
android:textSize="14sp" />
<ImageView
android:id="@+id/itemImage"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_marginLeft="30dp"
android:layout_marginTop="20dp"
android:layout_marginRight="20dp"
android:layout_marginBottom="20dp"
android:scaleType="fitXY"
android:src="@drawable/zhibao" />
<LinearLayout
android:layout_width="wrap_content"
android:orientation="vertical"
android:layout_gravity="center"
android:layout_height="wrap_content">
<TextView
android:id="@+id/itemname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="书名:"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/itemauthor"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="作者:"
android:textSize="15sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginTop="30dp"
android:layout_marginRight="150dp"
android:orientation="vertical">
<TextView
android:id="@+id/itempages"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="页数:"
android:textStyle="bold" />
<TextView
android:id="@+id/itemprice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="价格: "
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:layout_alignParentRight="true"
android:layout_width="wrap_content"
android:layout_marginRight="20dp"
android:orientation="vertical"
android:layout_height="wrap_content">
<Button
android:layout_width="wrap_content"
android:text="修改"
android:id="@+id/updateItem"
android:layout_height="40dp"/>
<Button
android:layout_marginTop="15dp"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="删除"
android:id="@+id/deleteItem"
/>
</LinearLayout>
</RelativeLayout>