第七章跨程序共享数据——探索内容提供器
Android持久化技术一章中所保存的数据都只能在当前应用程序中访问,但跨程序数据共享是由Content Provider提供的,譬如说:电话薄、短信、媒体库中的信息。
7.1内容提供器简介
不同程序之间共享数据,允许一个程序访问另一个程序内的数据,同时也能保证安全性。可以选择对哪一部分数据进行共享。
7.2运行时权限
Android 6.0之后引入运行时权限这一新功能。
(一)Android权限机制详解
在AndroidManifest.xml中添加两句权限声明。用来访问系统网络状态及监听开机广播。
低于6.0系统在安装该程序时,会告诉该程序一共申请了那些权限,从而是否安装。同时也会在应用程序管理界面告诉你权限的申请情况。但微信“店大欺客”,申请了很多权限,Android6.0之后加入运行时权限功能,在软件使用过程中对某一项权限进行授权。普通权限系统会帮我们授权(比如刚才的两个);危险权限(获取设备联系人信息、定位设备位置信息)需要我们手动授权。上百种权限,其中有9组24个危险权限。若权限名同意,则该权限组的其他权限也就同意了。
(二)在程序运行时申请权限
1.低于6.0的手机,实现拨号功能:
第一步,activity_main.xml中修改布局文件,增加Button按钮;
第二步,MainActivity中实现触发拨打电话的逻辑。构建隐式Intent,调用系统内置打电话工作,data部分制定了协议tel,号码是10086,为防止崩溃,所有操作在异常捕获代码块中。
try {
Intent intent01 = new Intent(Intent.ACTION_CALL);
intent01.setData(Uri.parse("tel:10086"));
startActivity(intent01);
} catch (SecurityException e) {
e.printStackTrace();
}
第三步:声明权限。
郭霖说6.0以上会显示报错,但我的坚果手机7.1.1没有报错,可能国产操作系统更改了一些东西吧,解决了动态权限处理中的问题吧。
2.Android 6.0上的动态权限处理(我的坚果手机7.1.1无需动态权限处理,直接可以读出通讯录;但我学长的小米手机7.0需要动态权限处理,看来动态适配方面还是有一定得问题啊)
第一步:判断用户有无授权,若授权,进行授权请求;若有授权,则调用call方法。
if(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CALL_PHONE)!= PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(MainActivity.this,new String[] {Manifest.permission.CALL_PHONE},1);
}else{
call();
}
第二步:若未授权,授权请求为onRequestPermissionsResult方法。复写这个方法。
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode){
case 1:
if(grantResults.length>0&& grantResults[0]== PackageManager.PERMISSION_GRANTED){
readContacts();
}else{
Toast.makeText(this,"You denied the permission",Toast.LENGTH_SHORT).show();
}
break;
default:
}
}
7.3访问其他程序中的数据
使用现有的内容提供器来读取和操作相应程序中的数据。
(一)ContentResolver的基本用法
1.构建内容URI字符串,com.example.app是包名,table是表名。加上Authority和协议名就会变成下面这个样子,将内容URI字符串解析成URI对象;
2.利用如下的方法进行查询,查询完之后为cursor对象。
3.利用如下代码将cursor对象的值一条条读出来
4. 增删改(本质一样,就是用法稍微有点区别)
(二)读取系统联系人
1.构建ListView,在MainActivity中获取ListView的实例,设置好适配器后,开始调用运行时权限的处理逻辑。参考7.2的(二)
2.复写ReadContacts方法,使用query来查询系统的联系人数据,URI参数为ContactsContract.CommonDataKinds.
Phone.CONTENT_URI,这是系统封装好的,这是Uri.parse已经解析过得;
private void readContacts() {
Cursor cursor = null;
try {
cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null);
if(cursor!=null){
while (cursor.moveToNext()){
//获取联系人名字
String displayname = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
//获取电话号码
String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
contactslist.add(displayname+"\n"+number);
}
adapter.notifyDataSetChanged();
}
}catch (Exception e){
e.printStackTrace();
}finally {
if(cursor!=null){
cursor.close();
}
}
}
3.对Cursor对象进行读取,将数据逐条读出。读条联系人名字和手机号后,对数据拼接,随后将数据添加到ListView数据源中,并通知刷新ListView,最后关闭Cursor对象;
4.给读取联系人权限。
7.4创建自己的内容提供器
自己封装数据供外部进行调用。
(一)提供内容提供器的步骤(理论部分)
1.新建类继承自ContentProvider类,该类中有6个抽象方法,复写六个方法。OnCreate():初始化内容提供器、完成数据库创建和升级等,存在Content Resolver时调用;query():在内容提供器中查询数据,使用uri参数来确定是那张表,每个参数代表不同意思;inseert()项内容提供器添加一条数据,根据uri和values来完成添加;update()更新数据;delete()删除数据;getType()根据传入URI来返回相应的MIME类型。URI具体可以这样写:
2.借助URIMatcher来实现匹配内容URI的功能,而且URIMatcher提供了一个addURI方法,并将authority、path和自定义代码传入。
3.复写query方法,利用UriMathcer的match()方法对传入URI进行匹配,得到相应的数据。UriMathcer的match()的输出是2步的自定义代码,代表的是表中的哪一列。
4.getType()方法用于获取Uri对象所对应的MIME类型。MIME格式如下:
(二)实现跨程序数据共享
2.1构建的ContentProvider的提供器,以供外部调用:
1.在OnCreate方法中完成创建MyDatabaseHelper的实例,然后返回true表示内容提供器初始化成功,完成数据库创建或升级工作。
2.在URIMathcer集合中增加URI对象,便于与后面传入的URI进行匹配,一共组建了四条URI数据。此处的AUTHORITY是com.example.hzk.provider。
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);
}
3.查询数据,使用uriMatcher.match(uri)来获取自定义代码。进而判断是哪一张表的什么数据。
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor = null;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
//3.1.按照数据库查询的方式进行查询,返回的是Curosr对象。
cursor = db.query("Book", projection, selection, selectionArgs, null, null, sortOrder);
break;
case BOOK_ITEM:
//3.2.uri.getPathSegments()将权限之后的部分进行分割,0位路径,第一个位置为ID。通过selection和
//selectionArgs进行约束。
String bookId = uri.getPathSegments().get(1);
cursor = db.query("Book", projection, "id=?", new String[]{bookId}, null, null, sortOrder);
break;}}
4.添加数据,根据URI参数判断往那张表里添加数据,然后调用insert方法表示能够返回这条数据的URI。调用URI.Parse来实现对内容URI的解析。
long newBookId = db.insert("Book", null, values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + newBookId);
5.更新数据,获取SQLiteBase实例,根据传入的URI参数来判断用户想要更新那张表里的数据,使用update实现更新的功能。
updateRows = db.update("Book", values,selection,selectionArgs);
2.2访问ContentProvider的数据:
新建项目,用以增删查改2.1项目中程序的数据。佩服啊,感觉很神奇。
1.在添加操作中,利用Uri.parse将内容URI转换为Uri对象,将添加数据存放至ContentValues对象中,然后利用getContentResolver的insert方法实现添加。
Uri uri = Uri.parse("content://com.example.hzk.provider/book");
ContentValues values = new ContentValues();
values.put("name", "A Clash of kings");
values.put("author", "George Martin");
values.put("pages", 1040);
values.put("price", 22.85);
Uri newUri = getContentResolver().insert(uri, values);
newId = newUri.getPathSegments().get(1);
2.查询数据时,将内容URI解析为Uri对象,利用getContentResolver().query方法去查询,查询结果放在Cursor对象中,之后对Cursor进行遍历并查询,最后记得关闭cursor流。
3.更新数据时,解析URI对象,将更新的数据放在ContentValues中,调用Update即可,newID的作用是对更新最新添加的数据
4.删除最新一行的数据。实验效果如下:
7.5 Git时间——版本控制工具进阶篇
先进入到该项目下创建代码仓库:
git init
(一)忽略文件
Build目录下的文件都是便以项目时候自动产生的,因此我们不应当将这部分文件添加到版本控制里面去。Git提供了可配性很强的机制允许用户将指定文件夹或目录排除在版本控制之外,它会检查代码仓库的目录下是否有.gitignore文件,会按行读取并忽略文件。AS在创建项目时候已经生成了.gitignore文件,一个在根目录下,一个在app模块下。我们可以对这两个文件进行修改。修改后如第三张图所示。忽略了:/src/test /src/androidTest
使用:
//添加所有文件
git add .
//执行commit完成提交
git commit -m “Fisrt Commit”
(二)查看修改内容:
如何使用Git查看上次提交之后修改的内容。只需要使用status命令.
1.可以看到代码的修改
git status
2.使用下面命令可以看到修改的内容
git diff
3.若只看MainActivity.java内的文件。可以使用:
git diff app/src/main/java/com/example/hzk/p386/MainActivity.java
(三)撤销未提交的修改
若代码未提交,修改的内容均可以撤销
1.适用于没有add过的文件。
//撤销修改使用:
git checkout app/src/main/java/com/example/hzk/p386/MainActivity.java
//检查下,之前添加过的已经取消啦:
git status
2.对于已经add过的。
//添加所有文件
git add .
//git status检查更新状态,发现.java文件有所更新
git status
//使用git reset重置上一次的修改。撤销修改。
git reset HEAD app/src/main/java/com/example/hzk/p386/MainActivity.java
//最后使用git status检查下。
git status
(四)查看提交记录
1.使用git log查看历史提交信息
git log
2.因为提交的记录可能比较多,如果我们只看这一行的提交记录,则使用git log 记录id -1
3.想看该条记录具体修改了什么内容,使用git log 记录id -1 -p,其中减号表示删除部分,加号表示增加部分。
第八章.丰富你的程序—运用手机多媒体
三大模块:使用通知(通知的基本用法、通知的进阶技巧/高级功能播放声音、震动、图片、长文字等等)、调用摄像头和相册及播放多媒体文件(音频、视频),有各自的缺陷,手机的兼容性问题是一个很严重的问题,尤其某锤手机,系统做的跟傻叉一样。
8.1将程序运行到手机上
没啥说的,点击AS的运行看手机型号和API有没有出来。Adb shell、adb devices查看有没有连接上。
8.2使用通知
通知可以在活动、服务、广播接收器中创建。一般程序进入后台我们才使用通知(但也不一定啊,譬如某讯体育,每次一打开 APP就弹出通知)
8.2.1通知的基本用法:
1.建立Button的布局和相应的点击事件;
2.需要NotificationManager对通知进行管理,getSystemService获取系统中的那个服务。
NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
3.用support-v4库提供的NotificationCompat类来创建Notification对象,确保了兼容性。
4.创建一个丰富的Notification对象,setContentTitle设置标题,setContentText设置内容,setWhen用于指定通知被创建的时间。使用Build方法进行生成。
Notification notification = new NotificationCompat.Builder(this).setContentTitle("This is content title").setContentText("This is content text").setWhen(System.currentTimeMillis()).setSmallIcon(R.mipmap.ic_launcher).setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)).build();
5. 使用notify方法将通知显示出来,notify()有两个参数,一个是id,一个是刚才创建的notification对象。
manager.notify(1, notification);
为了使通知能够被点击,我们接着搞:
6.先构建Intent对象,pendingIntent式延迟执行的Intent,根须需求选择是使用getActivity还是getService。
Intent intent = new Intent(this,NotificationActivity.class);
PendingIntent pi = PendingIntent.getActivity(this,0,intent,0);
7.在NotificationCompat.Builder中调用setContentIntent(pi)传入即可。
8.通知图标在产生点击时间时自动消失setAutoCancel(true)方法或者在新建的Activity中使用
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);manager.cancel(1);显示删除。
8.2.2通知的进阶技巧/高级功能。
8.允许播放音频。setSound(Uri.fromFile(new File("..."));
9.允许震动。setVibrate(new long[]{0,1000,1000,1000}).build();第一个参数是通知来时振动1s,静止1s,振动1s
10.完整显示通知内容,setContentText(“...”)会显示不完整。
setStyle(new NotificationCompat.BigTextStyle().bigText("....."))
11.除了显示长文字之外,还可以显示大图片:
setStyle(new NotificationCompat.BigPictureStyle().bigPicture(BitmapFactory.decodeResource
(getResources(),R.mipmap.ic_launcher))).
12.设置通知的优先级,有五个等级。MIN、LOW、HIGH、MAX和DEFAULT。
setPriority(NotificationCompat.PRIORITY_MAX).
8.3调用摄像头和相册
8.3.1 调用摄像头拍照:
1.创建点击事件按钮和用于显示拍照后图片的Imageview及实例。
2.创建File对象,用于存储拍照后的照片,将其存放在当前引用的缓存数据中,图片名字为output_image.jpg
File outputImage = new File(getExternalCacheDir(),"output_image.jpg");
3.如果其存在,则删除,并createNewFile创建新文件实例。
4.1.如果系统低于7.0,在使用Uri.fromFile()方法将File对象转为Uri对象,Uri对象标识jpg文件真实路径;
4.2.如果大于等于7.0,则使用FileProvider.getUriForFile()方法,里面三个参数分别是Context对象,任意字符串以及刚刚创建的Image对象。
if (Build.VERSION.SDK_INT >=24){
imageuri = FileProvider.getUriForFile(MainActivity.this,"com.example.cameraalbumtest.fileprovider",outputImage);
}else{
imageuri = Uri.fromFile(outputImage);
}
5.构建Intent对象,将Action指定为android.media.action.IMAGE_CAPTURE,putExtra()填入刚刚得到的URI对象,最后调用startActivityForResult来启动活动。
Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
intent.putExtra(MediaStore.EXTRA_OUTPUT,imageuri);
startActivityForResult(intent,TAKE_PHOTO);
6.在AndroidManifest.xml中对内容提供器进行注册;(1)android:name固定(2)android:authorities与ileProvider.getUriForFile()第二个参数一致(3)在provider标签中指定meta-data来制定共享路径,并引用xml文件下的资源
7.xml文件中external-path是指定Uri共享的,name属性可以随便填,path属性指的是共享的具体路径,设置空值就是将整个SD卡共享
8.访问SD卡在4.4之前需要声明访问SD卡的权限
8.3.2从相册中选择照片
1.建立点击事件及布局,进行运行时权限处理,WRITE_EXTERNAL_STORAGE为程序对SD卡的读写能力。大于6.0动态申请权限后调用openAlbum()方法,小于6.0直接调用openAlbum()方法
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
} else {
openAlbum();
}
2.openAlbum中构建Intent对象,指定action为android.intent.action.GET_CONTENT,为Intent设定必要参数,调用startActivityForResult来打开相册程序选择照片,其中第二个参数为CHOOSE_PHOTO的2
private void openAlbum() {
Intent intent01 = new Intent("android.intent.action.GET_CONTENT");
intent01.setType("image/*");
startActivityForResult(intent01, CHOOSE_PHOTO);
}
3.这样在onActivityResult中进入CHOOSE_PHOTO进行处理,为了兼容新旧版本,4.4以上调handleImageOnKitKat方法; 4.4以下调handleImageBeforeKitKat方法。因为4.4以后选择相册中的照片不在返回真实的Uri,因此需要解析。
case CHOOSE_PHOTO:
if (resultCode == RESULT_OK) {
if (Build.VERSION.SDK_INT >= 19) {
handleImageOnKitKat(data);
} else {
handleImageBeforeKitKat(data);
}
}
4.1.handleImageOnKitKat解析了封装的Uri,(1)如果是document类型的Uri,则通过document id处理。若URi的authority是media格式,还需要进一步的解析,通过字符串分割获得真实ID,用ID构建新Uri和判断语句,将其传至getImagePath方法中。可以获取真实路径了(2)如果是content类型的uri,则使用普通方法去处理(3)如果是File类型的uri,直接获取图片路径即可,最后调用displayImage根据图片路径显示图片。
4.2.handleImageBeforeKitKat方法中Uri未经封装,无需解析,直接getIamgePath获取真实路径,再调用displayImage方法显示于界面。
private void displayImage(String imagePath) {
if(imagePath!=null){
Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
iv_photos.setImageBitmap(bitmap);
}else {
Toast.makeText(this,"failed to get image",Toast.LENGTH_SHORT).show();
}
}
5.getIamgePath方法中通过Uri和selection来获取真实的路径,一列cursor对象的数据。
private String getIamgePath(Uri externalContentUri, String selection) {
String path = null;
//通过Uri和selection来获取真实的路径
Cursor cursor = getContentResolver().query(externalContentUri,null,selection,null,null);
if(cursor!=null){
if(cursor.moveToFirst()){
path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
}
cursor.close();
}
return path;
}
6.displayImage中显示图片至界面。
private void displayImage(String imagePath) {
if(imagePath!=null){
Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
iv_photos.setImageBitmap(bitmap);
}else {
Toast.makeText(this,"failed to get image",Toast.LENGTH_SHORT).show();
}
}
8.4播放多媒体文件
(一)播放音频
1.类初始化时创建一个MediaPlayer的实例,然后在OnCreate方法是进行动态权限处理,动态申请WRITE_EXTERNAL_STORAGE权限。若用户拒绝,则Finish掉。若用户同意,调用initMediaPlayer方法
2.为MediaPlayer设置初始化操作,创建File指定文件路径,接着调用了setDataSource和prepare方法。注意:目前手机都有内置得超大内部存储,使用Environment.getExternalStorageDirectory()获取路径的时候就不能定位到SD卡了,而是默认检测成内部存储路径。而且不同厂商手机SD卡的挂载路径不一样,所以SD卡挂在目录也就不能统一。
private void initMediaPlayer() {
try {
//Environment.getExternalStorageDirectory()为内置内存的根目录
File file = new File(Environment.getExternalStorageDirectory(),"music.mp3");
mediaPlayer.setDataSource(file.getPath());//设置要播放视频文件的位置
mediaPlayer.prepare();//让MediaPlayer进入准备状态。
} catch (IOException e) {
e.printStackTrace();
}
}
3.点击播放按钮、暂停按钮、停止按钮的响应事件,判断MediaPlayer当前有没有播放视频,若没有,则开始播放。
switch (v.getId()){
case R.id.btn_play:
if(!mediaPlayer.isPlaying()){
mediaPlayer.start();//开始播放
}
break;
4.最后在OnDestory方法中,分别调用stop和release方法以释放资源。
@Override
protected void onDestroy() {
super.onDestroy();
if(mediaPlayer!=null){
mediaPlayer.stop();
mediaPlayer.release();
}
}
这样就可以播放了。
(二)播放视频
几乎一模一样,只是把MediaPlayer改为VideoView来实现。
区别地方在于:
(1)无需再初始化Prepare,直接setVideoPath之后就可以start了;
(2)重头播放用Resume,
case R.id.btn_stop:
if(videoView.isPlaying()){
videoView.resume();
}
break;
(3)释放资源用suspend方法。
@Override
protected void onDestroy() {
super.onDestroy();
if(videoView!=null){
videoView.suspend();
}
}