第007天:跨APP共享数据技术
在上一章中我们学了 Android数据持久化的技术,包括文件存储、SharedPreferences存储以 及数据库存储。不知道你有没有发现,使用这些持久化技术所保存的数据都只能在当前应用程序中 访问。虽然文件和 SharedPreferences 存储中提供了 MODE_WORLD_READABLE 和MODE_ WORLD_WRITEABLE这两种操作模式,用于供给其他的应用程序访问当前应用的数据,但这两 种模式在Android4.2版本中都已被废弃了。为什么呢?因为Android官方已经不再推荐使用这种方式来实现跨程序数据共享的功能,而是应该使用更加安全可靠的内容提供器技术。
可能你会有些疑惑,为什么要将我们程序中的数据共享给其他程序呢?当然,这个是要视情况而定的,比如说账号和密码这样的隐私数据显然是不能共享给其他程序的,不过一些可以让其他程序进行二次开发的基础性数据,我们还是可以选择将其共享的。例如系统的电话簿程序,它的数据库中保存了很多的联系人信息,如果这些数据都不允许第三方的程序进行访问的话,恐怕很多应用的功能都要大打折扣了。除了电话簿之外,还有短信、媒体库等程序都实现了跨程序数据共享的功能,而使用的技术当然就是内容提供器了,下面我们就来对这一技术进行深入的探讨。
7.1内容提供器简介
内容提供器(Content Provider)主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访数据的安全性。 目前,使用内容提供器是Android实现跨程序共享数据的标准方式。
不同于文件存储和SharedPreferences存储中的两种全局可读写操作模式,内容提供器可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险。
不过在正式开始学习内容提供器之前,我们需要先掌握另外一个非常重要的知识—— Android运行时权限,因为待会的内容提供器示例中会使用到运行时权限的功能。当然不光是内容提供器,以后我们的开发过程中也会经常使用到运行时权限,因此你必须能够牢牢掌握它才行。
7.2运行时权限
Android的权限机制并不是什么新鲜事物,从系统的第一个版本开始就已经存在了。但其实之前Android的权限机制在保护用户安全和隐私等方面起到的作用比较有限,尤其是一些大家都离不开的常用软件,非常容易“店大欺客”。为此,Android开发团队在Android 6.0系统中引用了运行时权限这个功能,从而更好地保护了用户的安全和隐私,那么本节我们就来详细学习一下这个6.0系统中引入的新特性。
7.2.1 Android权限机制详解
首先来回顾一下过去Android的权限机制是什么样的。我们在第5章写BroadcastTest项目的时候第一次接触了 Android权限相关的内容,当时为了要访问系统的网络状态以及监听开机广播, 于是在AndroidManifest.xml文件中添加了这样两句权限声明:
因为访问系统的网络状态以及监听开机广播涉及了用户设备的安全性,因此必须在 AndroidManifest.xml中加入权限声明,否则我们的程序就会崩溃。
那么现在问题来了,加入了这两句权限声明后,对于用户来说到底有什么影响呢?为什么这 样就可以保护用户设备的安全性了呢?
其实用户主要在以下两个方面得到了保护,一方面,如果用户在低于6.0系统的设备上安装该程序,会在安装界面给出如图7.1所示的提醒。这样用户就可以清楚地知晓该程序一共申请了哪些权限,从而决定是否要安装这个程序。

另一方面,用户可以随时在应用程序管理界面查看任意一个程序的权限申请情况,如图7.2 所示。这样该程序申请的所有权限就尽收眼底,什么都瞒不过用户的眼睛,以此保证应用程序不会出现各种滥用权限的情况。

这种权限机制的设计思路其实非常简单,就是用户如果认可你所申请的权限,那么就会安装 你的程序,如果不认可你所申请的权限,那么拒绝安装就可以了。但是理想是美好的,现实却很残酷,因为很多我们所离不开的常用软件普遍存在着滥用权限的情况,不管到底用不用得到,反正先把权限申请了再说。比如说微信所申请的权限列表如图 7.3所示。

这只是微信所申请的一半左右的权限,因为权限太多一屏截不下来。其中有一些权限我并不 认可,比如微信为什么要读取我手机的短信和彩信?但是我不认可又能怎样,难道我拒绝安装微 信?没错,这种例子比比皆是,当一些软件已经让我们产生依赖的时候就会容易“店大欺客”, 反正这个权限我就是要了,你自己看着办吧!
Android开发团队当然也意识到了这个问题,于是在6.0系统中加入了运行时权限功能。也就是说,用户不需要在安装软件的时候一次性授权所有申请的权限,而是可以在软件的使用过程中再对某一项权限申请进行授权。比如说一款相机应用在运行时申请了地理位置定位权限,就算我拒绝了这个权限,但是我应该仍然可以使用这个应用的其他功能,而不是像之前那样直接无法安装它。
当然,并不是所有权限都需要在运行时申请,对于用户来说,不停地授权也很烦琐。Android 现在将所有的权限归成了两类,一类是普通权限,一类是危险权限。普通权限指的是那些不会直 接威胁到用户的安全和隐私的权限,对于这部分权限申请,系统会自动帮我们进行授权,而不需 要用户再去手动操作了,比如在BroadcastTest项目中申请的两个权限就是普通权限。危险权限则 表示那些可能会触及用户隐私,或者对设备安全性造成影响的权限,如获取设备联系人信息、定 位设备的地理位置等,对于这部分权限申请,必须要由用户手动点击授权才可以,否则程序就无 法使用相应的功能。
但是Android中有一共有上百种权限,我们怎么从中区分哪些是普通权限,哪些是危险权限呢?其实并没有那么难,因为危险权限总共就那么几个,除了危险权限之外,剩余的就都是普通 权限了。下表列出了 Android中所有的危险权限,一共是9组24个权限。
这张表格你看起来可能并不会那么轻松,因为里面的权限全都是你没使用过的。不过没有关 系,你并不需要了解表格中每个权限的作用,只要把它当成一个参照表来查看就行了。每当要使 用一个权限时,可以先到这张表中来查一下,如果是属于这张表中的权限,那么就需要进行运行 时权限处理,如果不在这张表中,那么只需要在AndroidManifest.xml文件中添加一下权限声明就 可以了。
另外注意一下,表格中每个危险权限都属于一个权限组,我们在进行运行时权限处理时使用 的是权限名,但是用户一旦同意授权了,那么该权限所对应的权限组中所有的其他权限也会同时 被授权。访http://developer.android.com/reference/android/Manifest.permission.html 可以查看 Android 系统中完整的权限列表。好了,关于Android权限机制的内容就讲这么多,理论知识你已经了解得非常充足了。接下 来我们就学习一下到底如何在程序运行的时候申请权限。
7.2.2在程序运行时申请权限
首先新建一个RuntimePermissionTest项目,我们就在这个项目的基础上来学习运行时权限的 使用方法。在开始动手之前还需要考虑一下到底要申请什么权限,其实刚才表中列出的所有权限 都是可以申请的,这里简单起见我们就使用CALL_PHONE这个权限来作为本小节中的示例吧。
CALL_PHONE这个权限是编写拨打电话功能的时候需要声明的,因为拨打电话会涉及用户手 机的资费问题,因而被列为了危险权限。在Android 6.0系统出现之前,拨打电话功能的实现其 实非常简单,修改activity main.xml布局文件,如下所示:
我们在布局文件中只是定义了一个按钮,当点击按钮时就去触发拨打电话的逻辑。接着修改 MainActivity中的代码,如下所示:
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedlnstanceState) {super.onCreate(savedlnstanceState); setContentView(R.layout.activitymain); Button makeCall = (Button) findViewById(R.id.makecall); makeCall.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) {try {Intent intent = new Intent(Intent.ACTIONCALL); intent.setData(Uri.parse("tel:10086")); startActivity(intent);
} catch (SecurityException e) {e.printStackTrace();
)
}
可以看到,在按钮的点击事件中,我们构建了一个隐式Intent , Intent的action指定为Intent. ACTI0N_ CALL,这是一个系统内置的打电话的动作,然后在data部分指定了协议是tel, 号码是10086o其实这部分代码我们在2.3.3小节中就已经见过了,只不过当时指定的action是Intent. ACTI0N_DIAL,表示打开拨号界面,这个是不需要声明权限的,而Intent. ACTI0N_ CALL 则可以直接拨打电话,因此必须声明权限。另外为了防止程序崩溃,我们将所有操作都放在了异常捕获代码块当中。
那么接下来修改AndroidManifest.xml文件,在其中声明如下权限:
landroid.permission. CALL_PHONE** />
这样我们就将拨打电话的功能成功实现了,并且在低于Android 6.0系统的手机上都是可以 正常运行的,但是如果我们在6.0或者更高版本系统的手机上运行,点击Make Call按钮就没有 任何效果,这时观察logcat中的打印日志,你会看到如图7.4所示的错误信息。

错误信息中提醒我们“PermissionDenial”,可以看出,是由于权限被禁止所导致的,因为6.0 及以上系统在使用危险权限时都必须进行运行时权限处理。
那么下面我们就来尝试修复这个问题,修改MainActivity中的代码,如下所示:
public class MainActivity extends AppCompatActivity {protected void onCreate(Bundle savedlnstanceState) {super.onCreate(savedlnstanceState); setContentView(R.layout.activitymain); Button makeCall = (Button) findViewById(R.id.makecall); makeCall.setOnClickListener(new View.OnClickListenerf) { (aOverride public void onClick(View v) (if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest, permission.CALL_PHONE) != PackageManager.PERMISSION__GRANTED) { ActivityCompat.requestPermissions(MainActivity.thisf newString!]{ Manifest.permission.CALL_PHONE }, 1);} else { caU();}
)});
}
private void call() {try {Intent intent = new Intent(Intent.ACTION_CALL);intent. setData(Uri. parse ("tel: 10686**)); sta rtActivity(intent);} catch (SecurityException e) {e.printStackT race();}}©Override
public void onRequestPermissionsResult(int requestcode, String[] permissions, int[] grantResults) { switch (requestcode) {case 1:if (grantResults.length > G && grantResults[6] == PackageManager. PERMISSION__GRANTED) {caU(); } else {Toast.makeText(this, "You denied the permission", Toast.LENGTH_ SHORT).show(); "} break;default:}}
}
上面的代码将运行时权限的完整流程都覆盖了,下面我们来具体解析一下。说白了,运行时 权限的核心就是在程序运行过程中由用户授权我们去执行某些危险操作,程序是不可以擅自做主 去执行这些危险操作的。因此,第一步就是要先判断用户是不是已经给过我们授权了,借助的是 ContextCompat. checkSelfPermission ()方法。checkSelfPermission()方法接收两个参数, 第一个参数是Context,这个没什么好说的,第二个参数是具体的权限名,比如打电话的权限名
就是Manifest.permission.CALL PHONE,然后我们使用方法的返回值和PackageManager. PERMISSION_GRANTED做比较,相等就说明用户已经授权,不等就表示用户没有授权。
如果已经授权的话就简单了,直接去执行拨打电话的逻辑操作就可以了,这里我们把拨打电 话的逻辑封装到了 call()方法当中。如果没有授权的话,则需要调用ActivityCompat. requestpermissions ()方法来向用户申请授权,requestpermissions ()方法接收3个参数, 第一个参数要求是Activity的实例,第二个参数是一个String数组,我们把要申请的权限名放 在数组中即可,第三个参数是请求码,只要是唯一值就可以了,这里传入了 1。
调用完了 requestpermissions()方法之后,系统会弹出一个权限申请的对话框,然后用户 可以选择同意或拒绝我们的权限申请,不论是哪种结果,最终都会回调到onRequest- PermissionsResult ()方法中,而授权的结果则会封装在grantResults参数当中。这里我们 只需要判断一下最后的授权结果,如果用户同意的话就调用call()方法来拨打电话,如果用户 拒绝的话我们只能放弃操作,并且弹出一条失败提示。
现在重新运行一下程序,并点击Make Call按钮,效果如图7.5所示。
由于用户还没有授权过我们拨打电话权限,因此第一次运行会弹出这样一个权限申请的对话 框,用户可以选择同意或者拒绝,比如说这里点击了 DENY,结果如图7.6所示。

由于用户没有同意授权,我们只能弹出一个操作失败的提示。下面我们再次点击Make Call 按钮,仍然会弹出权限申请的对话框,这次点击ALLOW,结果如图7.7所示。

可以看到,这次我们就成功进入到拨打电话界面了,并且由于用户已经完成了授权操作,之 后再点击Make Call按钮就不会再弹出权限申请对话框了,而是可以直接拨打电话。那可能你会 担心,万一以后我又后悔了怎么办?没有关系,用户随时都可以将授予程序的危险权限进行关闭, 进入 Settings Apps —> RuntimePermissionTest —> Permissions,界面如图 7.8 所示。

在这里我们就可以对任何授予过的危险权限进行关闭了。
好了,关于运行时权限的内容就讲到这里,现在你已经有能力处理Android±各种关于权限 的问题了,下面我们就来进入本章的正题一一内容提供器。
7.3访问其他程序中的数据
内容提供器的用法一般有两种,一种是使用现有的内容提供器来读取和操作相应程序中的数 据,另一种是创建自己的内容提供器给我们程序的数据提供外部访问接口。那么接下来我们就一 个一个开始学习吧,首先从使用现有的内容提供器开始。
如果一个应用程序通过内容提供器对其数据提供了外部访问接口,那么任何其他的应用程序 就都可以对这部分数据进行访问。Android系统中自带的电话簿、短信、媒体库等程序都提供了 类似的访问接口,这就使得第三方应用程序可以充分地利用这部分数据来实现更好的功能。下面 我们就来看一看,内容提供器到底是如何使用的。
7.3.1 ContentResolver 的基本用法
对于每一个应用程序来说,如果想要访问内容提供器中共享的数据,就一定要借助Content- Resolver 类,可以通过Context中的getContentResolver()方法获取到该类的实例。Content- Resolver 中提供了一系列的方法用于对数据进行CRUD操作,其中insertO方法用于添加数据, updateO方法用于更新数据,delete。方法用于删除数据,query()方法用于查询数据。有没有似曾相识的感觉?没错,SQLiteDatabase中也是使用这几个方法来进行CRUD操作的,只不过它们在方法参数上稍微有一些区别。
不同于SQLiteDatabase, ContentResolver中的增删改查方法都是不接收表名参数的,而是使用一个Uri参数代替,这个参数被称为内容URI。内容URI给内容提供器中的数据建立了唯一 标识符,它主要由两部分组成:authority和patho authority是用于对不同的应用程序做区分的, 一般为了避免冲突,都会采用程序包名的方式来进行命名。比如某个程序的包名是com.example. app,那么该程序对应的authority就可以命名为com.example.app. providero path则是用于对同一 应用程序中不同的表做区分的,通常都会添加到authority的后面。比如某个程序的数据库里存在 两张表:table 1和table2,这时就可以将path分别命名为/table 1和/table2,然后把authority和path 进行组合,内容 URI 就变成了 com.example.app.provider/table 1 和 com.example.app.provider/table2o 不过,目前还很难辨认出这两个字符串就是两个内容URI,我们还需要在字符串的头部加上协议 声明。因此,内容URI最标准的格式写法如下:
content://com.example.app.provider/tablel content://com.example.app.provider/table2 有没有发现,内容URI可以非常清楚地表达出我们想要访问哪个程序中哪张表里的数据。 也正是因此,ContentResolver中的增删改查方法才都接收对象作为参数,因为如果使用表名 的话,系统将无法得知我们期望访问的是哪个应用程序里的表。
在得到了内容URI字符串之后,我们还需要将它解析成UH对象才可以作为参数传入。解 析的方法也相当简单,代码如下所示:
Uri uri = Uri.parse("content://com.example.app.provider/tablel") 只需要调用Uri.parse()方法,就可以将内容URI字符串解析成Uri对象了。
现在我们就可以使用这个U「i对象来查询tablet表中的数据了,代码如下所示:
Cursor cursor = getContentResolver().query(uri,projection,selection,selectionArgs, sortOrder); 这些参数和SQLiteDatabase中query ()方法里的参数很像,但总体来说要简单一些,毕竟这 是在访问其他程序中的数据,没必要构建过于复杂的查询语句。下表对使用到的这部分参数进行 了详细的解释。

查询完成后返回的仍然是一个Cursor对象,这时我们就可以将数据从Cursor对象中逐个 读取出来了。读取的思路仍然是通过移动游标的位置来遍历Cursor的所有行,然后再取出每一 行中相应列的数据,代码如下所示:
if (cursor != null) {while (cursor.moveToNext()) (String columnl = cursor.getString(cursor.getColumnIndex("columnl")); int column2 = cursor.getInt(cursor.getColumnIndex("column?"));
}cursor.close();
} 掌握了最难的查询操作,剩下的增加、修改、删除操作就更不在话下了。我们先来看看如何 向tablel表中添加一条数据,代码如下所示:
Contentvalues values = new Contentvalues();
values.put("columnl", "text");
values.put("column?", 1);
getContentResolver().insert(uri, values); 可以看到,仍然是将待添加的数据组装到ContentValues中,然后调用ContentResolver的insert ()方法,将Uri和ContentValues作为参数传入即可。
现在如果我们想要更新这条新添加的数据,把column 1的值清空,可以借助ContentResolver 的update()方法实现,代码如下所示:
Contentvalues values = new Contentvalues();
values.put("columnl","");
getContentResolver().update(uri, values, "columnl = ? and column2 = ?", new String!] {''text", "I"}); 注意上述代码使用了 selection和selectionArgs参数来对想要更新的数据进行约束,以 防止所有的行都会受影响O
最后,可以调用ContentResolver的delete。方法将这条数据删除掉,代码如下所示:
getContentResolver().delete(uri, "column2 = ?", new String[] { "1" }); 到这里为止,我们就把ContentResolver中的增删改查方法全部学完了。是不是感觉一看就 懂?因为这些知识早在上一章中学习SQLiteDatabase的时候你就已经掌握了 ,所需特别注意的就 只有这个参数而已。那么接下来,我们就利用目前所学的知识,看一看如何读取系统电话簿 中的联系人信息。
7.3.2读取系统联系人
由于我们之前一直使用的都是模拟器,电话簿里面并没有联系人存在,所以现在需要自己手 动添加几个,以便稍后进行读取。打开电话簿程序,界面如图7.9所示。

可以看到,目前电话簿里是没有任何联系人的,我们可以通过点击ADD A CONTACT按钮 来对联系人进行创建。这里就先创建两个联系人吧,分别填入他们的姓名和手机号,如图7.10 所示。

这样准备工作就做好了,现在新建一个ContactsTest项目,让我们开始动手吧。
首先还是来编写一下布局文件,这里我们希望读取出来的联系人信息能够在ListView中显 示,因此,修改activity main.xml中的代码,如下所示:
简单起见,LinearLayout里就只放置了一个ListView o这里使用ListView而不是RecyclerView, 是因为我们要将关注的重点放在读取系统联系人上面,如果使用RecyclerView的话,代码偏多, 会容易让我们找不着重点。接着修改MainActivity中的代码,如下所示:
public class MainActivity extends AppCompatActivity {
ArrayAdapter adapter;
List contactsList = new ArrayList<>();
@0verride
protected void onCreate(Bundle savedlnstanceState) {
super.onCreate(savedlnstanceState); setContentView(R.layout.activitymain);
ListView contactsView = (Listview) findViewById(R.id.contactsview); adapter = new ArrayAdapter(this, android.R.layout. simple_list_ contactsList);
contactsview.setAdapter(adapter);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_ CONTACTS) != PackageManager.PERMISSION_GRANTED) { -
ActivityCompat.requestpermissions(this, new String!]{ Manifest. permission.READ_CONTACTS }, 1);
} else { readContacts();
}
}
private void readContacts() (
Cursor cursor = null;
try {
//查询联系人数据
cursor = getContentResolver().query(ContactsContract.CommonDataKinds. Phone.CONTENTJJRI, null, null, null, null);
if (cursor != null) {
while (cursor.moveToNext()) {
//获取联系人姓名
String displayName = cursor.getString(cursor.getColumnlndex (ContactsContract.CommonDataKinds.Phone.DISPLAYNAME));
//获取联系人手机号 ~
String number = cursor.getString(cursor.getColumnlndex (ContactsContract.CommonDataKinds.Phone.NUMBER)); contactsList.add(displayName + "\n" + number);
} adapter.notifyDataSetChanged();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cursor != null) { cursor.close();
}
}
}
©Override
public void onRequestPermissionsResult(int requestcode, Stringl] permissions, int[] grantResults) ( switch (requestcode) {
case 1:
if (grantResults,length > 0 && grantResults[0] == PackageManager.
PERMISSIONGRANTED) { readContacts();
} else {
Toast.makeText(this, "You denied the permission", Toast.LENGTH_ SHORT).show();
}
break;
default:
}
}
}
在onCreateO方法中,我们首先获取了 ListView控件的实例,并给它设置好了适配器,然 后开始调用运行时权限的处理逻辑,因为READ_CONTACTS权限是属于危险权限的。关于运行 时权限的处理流程相信你已经熟练掌握了,这里我们在用户授权之后调用readContacts ()方法 来读取系统联系人信息。
下面重点看一下readContacts()方法,可以看到,这里使用了 ContentResolver的query() 方法来查询系统的联系人数据。不过传入的Uri参数怎么有些奇怪啊?为什么没有调用 Uri.parse()方法去解析一个内容UR[字符串呢?这是因为ContactsContract .CommonData- Kinds.Phone类已经帮我们做好了封装,提供了一个CONTENT_URI常量,而这个常量就是使用 Uri.parseO方法解析出来的结果。接着我们对Cursor对象进行遍历,将联系人姓名和手机号 这些数据逐个取出,联系人姓名这一列对应的常量是ContactsContract.CommonDataKinds. Phone . DISPLAY NAME,联系人手机号这一列对应的常量是 ContactsContract. CommonData- Kinds.Phone.NUMBERO两个数据都取出之后,将它们进行拼接,并且在中间加上换行符,然后 将拼接后的数据添加到ListView的数据源里,并通知刷新一下ListViewo最后千万不要忘记将 Cursor对象关闭掉。
这样就结束了吗?还差一点点,读取系统联系人的权限千万不能忘记声明。修改 AndroidManifest.xml中的代码,如下所示:
加入了 android.permission.READ CONTACTS权限,这样我们的程序就可以访问到系统的 联系人数据了。现在才算是大功告成了,让我们来运行一下程序吧,效果如图7.11所示。

首先弹出了申请访问联系人权限的对话框,我们点击ALLOW,然后结果如图7.12所示。
ContactsTest
John
(098) 76S-4321
Tom
1 234-567-8^0
图7.12展示系统联系人信息
刚刚添加的两个联系人的数据都成功读取出来了!这说明跨程序访问数据的功能确实是实现了。
7.4创建自己的内容提供器
在上一节当中,我们学习了如何在自己的程序中访问其他应用程序的数据。总体来说思路还是非常简单的,只需要获取到该应用程序的内容URI,然后借助ContentResolver进行CRUD操 作就可以了。可是你有没有想过,那些提供外部访问接口的应用程序都是如何实现这种功能的 呢?它们又是怎样保证数据的安全性,使得隐私数据不会泄漏出去?学习完本节的知识后,你的 疑惑将会被——解开。
7.4.1创建内容提供器的步骤
前面已经提到过,如果想要实现跨程序共享数据的功能,官方推荐的方式就是使用内容提供器,可以通过新建一个类去继承Contentprovider的方式来创建一个自己的内容提供器。 Contentprovider类中有6个抽象方法,我们在使用子类继承它的时候,需要将这6个方法全部重写。新建MyProvider继承自Contentprovider,代码如下所示:
public class MyProvider extends Contentprovider {
(QOverride
public boolean onCreate() {
return false;
}
(QOverride
public Cursor query(Uri uri, St ring[] projection, String selection, String[] selectionArgs, String sortOrder) {
return null;
}
(QOverride
public Uri insert(Uri uri, ContentValues values) {
return null;
}
^Override
public int update(Uri uri, Contentvalues values, String selection, String[] selectionArgs) { return 0;
}
(aOverride
public int delete(Uri uri, String selection, String[] selectionArgs) return 0;
}
^Override
public String getType(Uri uri) { return null;
}
}
在这6个方法中,相信大多数你都已经非常熟悉了,我再来简单介绍一下吧。
初始化内容提供器的时候调用。通常会在这里完成对数据库的创建和升级等操作,返回true 表示内容提供器初始化成功,返回false则表示失败。注意,只有当存在ContentResolver尝试 访问我们程序中的数据时,内容提供器才会被初始化。
从内容提供器中查询数据。使用uri参数来确定查询哪张表,projection参数用于确定查 询哪些列,selection和selectionArgs参数用于约束查询哪些行,sortOrder参数用于对结 果进行排序,查询的结果存放在Cursor对象中返回。
向内容提供器中添加一条数据。使用uri参数来确定要添加到的表,待添加的数据保存在 values参数中。添加完成后,返回一个用于表示这条新记录的URI。
更新内容提供器中已有的数据。使用uri参数来确定更新哪一张表中的数据,新数据保存在 values参数中,selection和selectionArgs参数用于约束更新哪些行,受影响的行数将作 为返回值返回。
从内容提供器中删除数据。使用uri参数来确定删除哪一张表中的数据,selection和 selectionArgs参数用于约束删除哪些行,被删除的行数将作为返回值返回。
根据传入的内容URI来返回相应的MIME类型。
可以看到,几乎每一个方法都会带有Uri这个参数,这个参数也正是调用ContentResolver 的增删改查方法时传递过来的。而现在,我们需要对传入的Uri参数进行解析,从中分析出调用 方期望访问的表和数据。
回顾一下,一个标准的内容URI写法是这样的:
content://com.example.app.provider/tablel
这就表示调用方期望访问的是com.example.app这个应用的table 1表中的数据。除此之外, 我们还可以在这个内容URI的后面加上一个id,如下所示:
content://com.example.app.provider/tablel/1
这就表示调用方期望访问的是com.example.app这个应用的table 1表中id为1的数据。
内容URI的格式主要就只有以上两种,以路径结尾就表示期望访问该表中所有的数据,以 id结尾就表示期望访问该表中拥有相应id的数据。我们可以使用通配符的方式来分别匹配这两
种格式的内容URI,规则如下。
- *:表示匹配任意长度的任意字符。
- #:表示匹配任意长度的数字。
所以,一个能够匹配任意表的内容URI格式就可以写成:
content://com.example.app.provider/*
而一个能够匹配tablel表中任意一行数据的内容URI格式就可以写成:
content://com.example.app.provider/tablel/#
接着,我们再借助UriMatcher这个类就可以轻松地实现匹配内容URI的功能。UriMatcher 中提供了一个addURIO方法,这个方法接收3个参数,可以分别把authority, path和一个自 定义代码传进去。这样,当调用UriMatcher的match()方法时,就可以将一个Uri对象传入, 返回值是某个能够匹配这个Uri对象所对应的自定义代码,利用这个代码,我们就可以判断出调 用方期望访问的是哪张表中的数据了。修改MyProvider中的代码,如下所示:
case TABLE2_DIR:
//查询table2表中的所有数据 break;
case TABLE2_ITEM:
//查询table2表中的单条数据 break;
default:
break;
}
}
}"
可以看到,MyProvider中新增了 4个整型常量,其中TABLE1_DIR表示访问tablel表中的所 有数据,TABLE1_ITEM表示访问tablel表中的单条数据,TABLE2_DIR表示访问table2表中的所 有数据,TABLE2_ITEM表示访问table2表中的单条数据。接着在静态代码块里我们创建了 UriMatcher的实例,并调用addURIO方法,将期望匹配的内容URI格式传递进去,注意这里传 入的路径参数是可以使用通配符的。然后当query()方法被调用的时候,就会通过UriMatcher 的match()方法对传入的Uri对象进行匹配,如果发现UriMatcher中某个内容URI格式成功匹 配了该UH对象,则会返回相应的自定义代码,然后我们就可以判断出调用方期望访问的到底是 什么数据了。
上述代码只是以query()方法为例做了个示范,其实insert。、update。、deleteO这 几个方法的实现也是差不多的,它们都会携带Uri这个参数,然后同样利用UriMatcher的match() 方法判断出调用方期望访问的是哪张表,再对该表中的数据进行相应的操作就可以了。
除此之外,还有一个方法你会比较陌生,即getTypeO方法。它是所有的内容提供器都必须 提供的一个方法,用于获取Uri对象所对应的MIME类型。一个内容URI所对应的MIME字符 串主要由3部分组成,Android对这3个部分做了如下格式规定。
□必须以vnd开头。
□如果内容URI以路径结尾,则后接android.cursor.dir/,如果内容URI以id结尾, 贝U后接 android. cursor.item/o
□最后接上 vnd.
所以,对于 content://com.example.app.provider/tablel 这个内容 URI,它所对应的 MIME类型 就可以写成:
vnd.android.cursor.dir/vnd.com.example.app.provider.tablel
对于 content://com.example.app.provider/tablel/l 这个内容 URI,它所对应的 MIME类型就可 以写成:
vnd.android.cu rso r.item/vnd.com.example.app.p rovider.tablel
现在我们可以继续完善MyProvider中的内容了,这次来实现getTypeO方法中的逻辑,代 码如下所示:
public class MyProvider extends Contentprovider {
(aoverride
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) (
case TABLE1_DIR: return "vnd・android.cursor.dir/vnd.com.example.app.provider.tablel";
case TABLE1_ITEM: return "vnd・android・cursor.item/vnd.com・example.app.provider.tablel";
case TABLE2_DIR: return "vnd・android.cursor・dir/vnd.com・example.app.provider.tableZ";
case TABLE2_ITEM: return "vnd.android・cursor.item/vnd.com.example・app.provider.table2u;
default: break;
} return null;
}
}
到这里,一个完整的内容提供器就创建完成了,现在任何一个应用程序都可以使用 ContentResolver来访问我们程序中的数据。那么前面所提到的,如何才能保证隐私数据不会泄漏 出去呢?其实多亏了内容提供器的良好机制,这个问题在不知不觉中已经被解决了。因为所有的 CRUD操作都一定要匹配到相应的内容URI格式才能进行的,而我们当然不可能向UriMatcher 中添加隐私数据的URI,所以这部分数据根本无法被外部程序访问到,安全问题也就不存在了。
好了,创建内容提供器的步骤你也已经清楚了,下面就来实战一下,真正体验一回跨程序数 据共享的功能。
7.4.2实现跨程序数据共享
简单起见,我们还是在上一章中DatabaseTest项目的基础上继续开发,通过内容提供器来给 它加入外部访问接口。打开DatabaseTest项目,首先将MyDatabaseHelper中使用Toast弹出创建 数据库成功的提示去除掉,因为跨程序访问时我们不能直接使用Toasto然后创建一个内容提供 器,右击 com.example.broadcasttest>New—>Other->Content Provider,会弹出如图 7.13 所示的 窗口。
可以看到,这里我们将内容提供器命名为DatabaseProviderauthority指定为com.example, databasetest .provider, Exported属性表示是否允许外部程序访问我们的内容提供器, Enabled属性表示是否启用这个内容提供器。将两个属性都勾中,点击Finish完成创建。
接着我们修改DatabaseProvider中的代码,如下所示:
public class DatabaseProvider extends Contentprovider {
private static UriMatcher uriMatcher;
private MyDatabaseHelper dbHelper;
static {
uriMatcher = new UriMatcher(UriMatcher.NOMATCH); uriMatcher.addURI(AUTHORITY, ~
uriMatcher.addURI(AUTHORITY, uriMatcher.addURI(AUTHORITY, uriMatcher.addURI(AUTHORITY,
7.4创建自己的内容提供器 267
(aOverride
public boolean onCreate() {
dbHelper = new MyDatabaseHelper(getContext(), "Bookstore.db", null, 2); return true;
} ^Override
public Cursor query(Uri uri, String!] projection, String selection, St ring[] selectionArgs, String sortOrder) {
//查询数据
SQLiteDatabase db = dbHelper.getReadableDatabase(); Cursor cursor = null;
switch (uriMatcher.match(uri)) {
case BOOKDIR:
cursor = db.query("Book", projection, selection, selectionArgs, null, null, sortOrder);
break;
case B00K_ITEM:
String bookid = uri.getPathSegments().get(1);
cursor = db.query( "Book", projection, '*id = ?" r new St ring [] { bookid }, null, null, sortOrder);
break;
case CATEGORY_DIR:
cursor = db.query("Category"f projection, selection, selectionArgs, null, null, sortOrder);
break;
case CATEGORY_ITEM:
String categoryld = uri.getPathSegments().get(l);
cursor = db.query("Category", projection, "id = ?", new String[] ( categoryld }, null, null, sortOrder);
break;
default:
break;
}
return cursor;
} ^Override
public Uri insert(Uri uri, Contentvalues values) {
//添加数据
SQLiteDatabase db = dbHelper.getWritableDatabase();
Uri uriReturn = null;
switch (uriMatcher.match(uri)) {
case BOOKDIR:
case BOOK^ITEM:
long newBookld = db.insert("Book", null, values); uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + newBookld);
break;
case CATEGORY_DIR:
case CATEGORYllTEM:
long newCategoryld = db.insert("Category", null, values); uriReturn = Uri-parseCcontent://** + AUTHORITY + “/category/” + newCategoryld);
break; default:
break;
}
return uriReturn;
} ^Override
public int update(Uri uri, Contentvalues values, String selection, String[] selectionArgs) {
//更新数据
SQLiteDatabase db = dbHelper.getWritableDatabase();
int updatedRows = 0;
switch (uriMatcher.match(uri)) {
case BOOKDIR:
updatedRows = db.update("Book", values, selection, selectionArgs); break;
case BOOKITEM:
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);
break;
case CATEGORY_ITEM:
String categoryld = uri.getPathSegments().get(l);
updatedRows = db.update("Category", values, "id = ?", new String[] { categoryld });
break;
default:
break;
}
return updatedRows;
} (QOverride
public int delete(Uri uri, String selection, String[] selectionArgs) {
//删除数据
SQLiteDatabase db = dbHelper.getWritableDatabase();
int deletedRows = 0;
switch (uriMatcher.match(uri)) {
case B00K_DIR: deletedRows = db.delete("Book", selection, selectionArgs); break;
case BOOKITEM:
String bookid = uri.getPathSegments().get(1); deletedRows = db.delete("Book", "id = ?", new St ring[] { bookid }); break;
case CATEGORY DIR:
deletedRows = db.delete("Category", selection, selectionArgs); break;
case CATEGORY ITEM:
String categoryld = uri.getPathSegments().get(l); deletedRows = db.delete("Category", "id = ?", new St ring[] { categoryld });
break; default:
break;
}
return deletedRows;
}
^Override public String getType(Uri uri) { switch (uriMatcher.match(uri)) {
case BOOK_DIR:
return "vnd.android.cursor.dir/vnd.com.example.databasetest. provider.book";
case B00K_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";
)
return null;
}
}
代码虽然很长,不过不用担心,这些内容都非常容易理解,因为使用到的全部都是上一小节 中我们学到的知识。首先在类的一开始,同样是定义了 4个常量,分别用于表示访问Book表中 的所有数据、访问Book表中的单条数据、访问Category表中的所有数据和访问Category表中的 单条数据。然后在静态代码块里对UriMatcher进行了初始化操作,将期望匹配的几种URI格式 添加了进去。
接下来就是每个抽象方法的具体实现了,先来看下onCreateO方法,这个方法的代码很短, 就是创建了一个MyDatabaseHelper的实例,然后返回true表示内容提供器初始化成功,这时数 据库就已经完成了创建或升级操作。
接着看一下query ()方法,在这个方法中先获取到了 SQLiteDatabase的实例,然后根据传入 的Uri参数判断出用户想要访问哪张表,再调用SQLiteDatabase的query()进行查询,并将 Cursor对象返回就好了。注意当访问单条数据的时候有一个细节,这里调用了 Uri对象的 getPathSegmentsO方法,它会将内容URI权限之后的部分以T符号进行分割,并把分割后 的结果放入到一个字符串列表中,那这个列表的第0个位置存放的就是路径,第1个位置存放的 就是id 了。得到了 id之后,再通过selection和selectionArgs参数进行约束,就实现了查 询单条数据的功能。
再往后就是insert()方法,同样它也是先获取到了 SQLiteDatabase的实例,然后根据传入 的Uri参数判断出用户想要往哪张表里添加数据,再调用SQLiteDatabase的insert()方法进行 添加就可以了。注意insert()方法要求返回一个能够表示这条新增数据的URI,所以我们还需 要调用Uri.parse()方法来将一个内容URI解析成Uri对象,当然这个内容URI是以新增数据 的id结尾的。
接下来就是updateO方法了,相信这个方法中的代码已经完全难不倒你了。也是先获取 SQLiteDatabase的实例,然后根据传入的Uri参数判断出用户想要更新哪张表里的数据,再调用 SQLiteDatabase的update。方法进行更新就好了,受影响的行数将作为返回值返回。
下面是deleteO方法,是不是感觉越到后面越轻松了?因为你已经渐入佳境,真正地找到 窍门了。这里仍然是先获取到SQLiteDatabase的实例,然后根据传入的Uri参数判断出用户想要 删除哪张表里的数据,再调用SQLiteDatabase的delete。方法进行删除就好了,被删除的行数 将作为返回值返回。
最后是getTypeO方法,这个方法中的代码完全是按照上一节中介绍的格式规则编写的,相 信已经没有什么解释的必要了。这样我们就将内容提供器中的代码全部编写完了。
另外还有一点需要注意,内容提供器一定要在AndroidManifest.xml文件中注册才可以使用。 不过幸运的是,由于我们是使用Android Studi。的快捷方式创建的内容提供器,因此注册这一步 已经被自动完成了。打开AndroidManifest.xml文件瞧一瞧,代码如下所示:
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/appname"
android:supportsRtl="true"
and roid :theme="(asty'le/AppTheme,,>
android:name=".DatabaseProvider"
android:authorities="com.example.databasetest.provider"
android:enabled="true" android:exported="true">
可以看到,
现在DatabaseTest这个项目就已经拥有了跨程序共享数据的功能了,我们赶快来尝试一下。 首先需要将DatabaseTest程序从模拟器中删除掉,以防止上一章中产生的遗留数据对我们造成干 扰。然后运行一下项目,将DatabaseTest程序重新安装在模拟器上了。接着关闭掉DatabaseTest 这个项目,并创建一个新项目ProviderTest,我们就将通过这个程序去访问DatabaseTest中的数据。
还是先来编写一下布局文件吧,修改activity main.xml中的代码,如下所示:
and roid: id=,,@+id/add_data"
and roid: layout_width=,,match_pa rent" android:layout_height="wrap_content" android:text="Add To Book" />
android:id="@+id/query data"
android :layout_width=,,match_parent,'
android:layout_height="wrap_content" android:text="Query From Book" />
android:id="@+id/update_data"
android:layout_width="match_parent" android :layout_height="wrap_content', android:text="Update Book" 7>
android:id="@+id/delete_data ”
android:layout_width="match_parent"
and roid: l_ayout_height="wrap_content" android:text="Delete From Book" />
布局文件很简单,里面放置了 4个按钮,分别用于添加、查询、修改和删除数据。然后修改 MainActivity中的代码,如下所示:
public class MainActivity extends AppCompatActivity {
private String newld;
^Override
protected void onCreate(Bundle savedlnstanceState) {
super.onCreate(savedlnstanceState); setContentView(R.layout.activitymain); Button addData = (Button) findViewByld(R.id.adddata); addData.setOnClickListener(new View.OnClickListener() {
(QOverride
public void onClick(View v) {
//添加数据
Uri uri = Uri.parse("content://com.example.databasetest. provider/ book");
Contentvalues values = new Contentvalues(); values.put("name", "A Clash of Kings"); values.put("author", "George Martin"); values.put("pages", 1040);
values.putCprice", 22.85);
Uri newUri = getContentResolver().insert(uri, values); newld = newUri.getPathSegments().get(l);
}
Button queryData.setOnClickListener(new View.OnClickListener() (
^Override public void onClick(View v) (
//查询数据
Uri uri = Uri.parse("content://com.example.databasetest. provider/ book");
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
(cursor != null) { while (cursor.moveToNext()) {
String name = cursor.getString(cursor. getColumnlndex
("name"));
String author = cursor.getString(cursor. getColumnlndex
("author"));
int pages = cursor.getInt(cursor.getColumnlndex ("pages")); double price = cursor.getDouble(cursor. getColumnlndex ("price"));
Log.d("MainActivity",
Log.d("MainActivity", Log.dCMainActivity11, } cursor.close();
}
});
updateData.setOnClickListener(new View.OnClickListener() {
^Override
public void onClick(View v) {
//更新数据
Uri uri = Uri.parse("content://com.example.databasetest. provider/ book/" -v newld);
Contentvalues values = new ContentValues();
values.put("name", "A Storm of Swords");
values.put("pages", 1216); values.putCprice", 24.05); getContentResolver().update(uri, values, null, null);
}
));
Button deleteData = (Button) findViewByld(R.id.deletedata); deleteData.setOnClickListener(new View.OnClickListener() {
@0verride
public void onClick(View v) {
//删除数据
Uri uri = Uri.parse("content://com.example.databasetest. provider/ book/" + newld);
getContentResolver().deletefuri, null, null);
}
});
}
}
可以看到,我们分别在这4个按钮的点击事件里面处理了增删改查的逻辑。添加数据的时候, 首先调用了 Uri.parseO方法将一个内容URI解析成对象,然后把要添加的数据都存放到 Contentvalues对象中,接着调用ContentResolver的insert()方法执行添加操作就可以了。 注意insertO方法会返回一个Uri对象,这个对象中包含了新增数据的id,我们通过getPath- SegmentsO方法将这个id取出,稍后会用到它。
查询数据的时候,同样是调用了 Uri.parseO方法将一个内容URI解析成Uri对象,然后 调用ContentResolver的query()方法去查询数据,查询的结果当然还是存放在Cursor对象 中的。之后对Cursor进行遍历,从中取出查询结果,并一一打印出来。
更新数据的时候,也是先将内容URI解析成Uri对象,然后把想要更新的数据存放到 Contentvalues对象中,再调用ContentResolver的updateO方法执行更新操作就可以了。 注意这里我们为了不想让Book表中的其他行受到影响,在调用U「i.parse()方法时,给内容 URI的尾部增加了一个id,而这个id正是添加数据时所返回的。这就表示我们只希望更新刚刚 添加的那条数据,Book表中的其他行都不会受影响。
删除数据的时候,也是使用同样的方法解析了一个以id结尾的内容URI,然后调用 ContentResolver的deleteO方法执行删除操作就可以了。由于我们在内容URI里指定了一个 id,因此只会删掉拥有相应id的那行数据,Book表中的其他数据都不会受影响。
现在运行一下ProviderTest项目,会显示如图7.14所示的界面。
ProvtderTest
图 7.14 Provider?est 主界面
点击一下Add To Book按钮,此时数据就应该已经添加到DatabaseTest程序的数据库中了, 我们可以通过点击Query From Book按钮来检查一下,打印日志如图7.15所示。
com. example, providertest D/MainActivity: book name is A Clash of Kings
com. example, providertest D/MainActivity: book author is George Martin com. example, providertest D/MainActivity: book pages is 1040
com. example, providertest D/MainActivity: book price is 22.85
图7.15查询添加的数据
然后点击一下Update Book按钮来更新数据,再点击一下Query From Book按钮进行检查, 结果如图7.16所示。
— ~~ :二,二二二二二二二: 4;=盘工;二….■!. “二貞:=.
com. example, providertest D'MainActivity: book name is A Storm of Swords com. example, providertest D/MainActivity: book author is George Martin com. example, providertest D/MainActivity: book pages is 1216
com.example, providertest D/MainActivity: book price is 24. 05
图7.16 查询更新后的数据
最后点击Delete From Book按钮删除数据,此时再点击Query From Book按钮就查询不到数 据了。由此可以看出,我们的跨程序共享数据功能已经成功实现了!现在不仅是ProviderTest程 序,任何一个程序都可以轻松访问DatabaseTest中的数据,而且我们还丝毫不用担心隐私数据泄 漏的问题。
到这里,与内容提供器相关的重要内容就基本全部介绍完了,下面就让我们再次进入本书的 特殊环节,学习更多关于Git的用法。
7.5 Git时间——版本控制工具进阶
在上一次的Git时间里,我们学习了关于Git最基本的用法,包括安装Git、创建代码仓库, 以及提交本地代码。本节中我们将要学习更多的使用技巧,不过在开始之前先要把准备工作做好。
所谓的准备工作就是要给一个项目创建代码仓库,这里就选择在ProviderTest项目中创建吧,
打开Git Bash,进入到这个项目的根目录下面,然后执行git init命令,如图7.17所示。
图7.17创建代码仓库
这样准备工作就已经完成了 ,让我们继续开始Git之旅吧。
7.5.1忽略文件
代码仓库现在已经创建好了,接下来我们应该去提交ProviderTest项目中的代码。不过在提 交之前你也许应该思考一下,是不是所有的文件都需要加入到版本控制当中呢?
在第1章介绍Android项目结构的时候有提到过,build目录下的文件都是编译项目时自动生 成的,我们不应该将这部分文件添加到版本控制当中,那么如何才能实现这样的效果呢?
Git提供了一种可配性很强的机制来允许用户将指定的文件或目录排除在版本控制之外,它 会检查代码仓库的目录下是否存在一个名为.gitignore的文件,如果存在的话,就去一行行读取这 个文件中的内容,并把每一行指定的文件或目录排除在版本控制之外。注意.gitignore中指定的文 件或目录是可以使用“*”通配符的。
神奇的是,我们并不需要自己去创建.gitignore文件,Android Studio在创建项目的时候会自 动帮我们创建出两个.gitignore文件,一个在根目录下面,一个在app模块下面。首先看一下根目 录下面的.gitignore文件,如图7.18所示。
題.gitignore x [ j
*. iml J.
.gradle j
/local.properties
/. i deorkspace. xml
:/. i dea/librari es
.DS_St©re
/build
■ /captures
图7.18根目录下面的.gitignore文件
这是Android Studi。自动生成的一些默认配置,通常情况下,这部分内容都是不用添加到版 本控制当中的。我们来简单阅读一下这个文件,除了*.iml表示指定任意以.iml结尾的文件,其他 都是指定的具体的文件名或者目录名,上面配置中的所有内容都不会被添加到版本控制当中,因 为基本都是一些由IDE自动生成的配置。
再来看一下app模块下面的.gitignore文件,这个就简单多了,如图7.19所示。
目 appVgitignore X |
:/build ¥
图7.19 app模块下面的.gitignore文件
由于app模块下面基本都是我们编写的代码,因此默认情况下只有其中的build目录不会被 添加到版本控制当中。
当然,我们完全可以对以上两个文件进行任意地修改,来满足特定的需求。比如说,app模 块下面的所有测试文件都只是给我自己使用的,我并不想把它们添加到版本控制中,那么就可以 这样修改app/.gitignore文件中的内容:
/build
/src/test
/src/androidTest
没错,只需添加这样两行配置,因为所有的测试文件都是放在这两个目录下的。现在我们可 以提交代码了,先使用add命令将所有的文件进行添加,如下所示:
git add .
然后执行commit命令完成提交,如下所示:
git commit -m "First commit."
7.5.2查看修改内容
在进行了第一次代码提交之后,我们后面还可能会对项目不断地进行维护或添加新功能等。 比较理想的情况是每当完成了一小块功能,就执行一次提交。但是如果某个功能牵扯到的代码 比较多,有可能写到后面的时候我们就已经忘记前面修改了什么东西了。遇到这种情况时不用 担心,Git全都帮你记着呢!下面我们就来学习一下如何使用Git来查看自上次提交后文件修改 的内容。
查看文件修改情况的方法非常简单,只需要使用status命令就可以了,在项目的根目录下 输入如下命令:
git status
然后Git会提示目前项目中没有任何可提交的文件,因为我们刚刚才提交过嘛。现在对 ProviderTest项目中的代码稍做一下改动,修改MainActivity中的代码,如下所示:
public class MainActivity extends AppCompatActivity {
@0verride
protected void onCreate(Bundle savedlnstanceState) {
addData.setOnClickListener(new OnClickListener() {
^Override
public void onClick(View v) {
values.put("price"f 55.55);
)…
});
}
}
这里仅仅是在添加数据的时候,将书的价格由22.85改成了 55.55。然后重新输入git status 命令,这次结果如图7.20所示。
图7.20查看文件变动情况
可以看到,Git提醒我们MainActivity.java这个文件已经发生了更改,那么如何才能看到更改 的内容呢?这就需要借助diff命令了,用法如下所示:
git diff
这样可以查看到所有文件的更改内容,如果你只想查看MainActivity.java这个文件的更改内 容,可以使用如下命令:
git diff app/src/main/java/com/example/providertest/MainActivity.java
命令的执行结果如图7.21所示。
图7.21查看修改的具体内容
其中,减号代表删除的部分,加号代表添加的部分。从图中我们就可以明显地看出,书的价 格由22.85被修改成了 55.55。
7.5.3撤销未提交的修改
有时候我们的代码可能会写得过于草率,以至于原本正常的功能,结果反倒被我们改出了问 题。遇到这种情况时也不用着急,因为只要代码还未提交,所有修改的内容都是可以撤销的。
比如在上一小节中我们修改了 MainActivity里一本书的价格,现在如果想要撤销这个修改就 可以使用checkout命令,用法如下所示:
git checkout app/s rc/main/j ava/com/example/providertest/MainActivity.j ava
执行了这个命令之后,我们对MainActivity.java这个文件所做的一切修改就应该都被撤销了。 重新运行git status命令检查一下,结果如图7.22所示。
图7.22重新查看文件变动情况
可以看到,当前项目中没有任何可提交的文件,说明撤销操作确实是成功了。
不过这种撤销方式只适用于那些还没有执行过add命令的文件,如果某个文件已经被添加过 了,这种方式就无法撤销其更改的内容,我们来做个试验瞧一瞧。
首先仍然是将MainActivity中那本书的价格改成55.55,然后输入如下命令:
git add .
这样就把所有修改的文件都进行了添加,可以输入git status来检查一下,结果如图7.23 所示。
图7.23再次查看文件变动情况
现在我们再执行一遍checkout命令,你会发现MainActivity仍然是处于已添加状态,所修 改的内容无法撤销掉。
这种情况应该怎么办?难道我们还没法后悔了?当然不是,只不过对于已添加的文件我们应 该先对其取消添加,然后才可以撤回提交。取消添加使用的是「eset命令,用法如下所示:
git reset HEAD app/src/main/java/com/example/providertest/MainActivity.java
然后再运行一遍git status命令,你就会发现MainActivity.java这个文件重新变回了未添 加状态,此时就可以使用checkout命令来将修改的内容进行撤销了。
7.5.4查看提交记录
当ProviderTest这个项目开发了几个月之后,我们可能已经执行过上百次的提交操作了,这 个时候估计你早就已经忘记每次提交都修改了哪些内容。不过没关系,忠实的Git一直都帮我们 清清楚楚地记录着呢!可以使用log命令查看历史提交信息,用法如下所示:
git log
由于目前我们只执行过一次提交,所以能看到的信息很少,如图7.24所示。
图7.24查看提交记录
可以看到,每次提交记录都会包含提交id、提交人、提交日期以及提交描述这4个信息。那 么我们再次将书价修改成55.55,然后执行一次提交操作,如下所示:
git add .
git commit -m "Change price."
现在重新执行git log命令,结果如图7.25所示。
图7.25重新查看提交记录
当提交记录非常多的时候,如果我们只想查看其中一条记录,可以在命令中指定该记录的id, 并加上-1参数表示我们只想看到一行记录,如下所示:
git log lfa380b5O2a0Ob82bfc8d84c5ab5el5b8fbf7dac -1
而如果想要查看这条提交记录具体修改了什么内容,可以在命令中加入・p参数,命令如下:
git log Ifa380b502a00b82bfc8d84c5ab5el5b8fbf7dac -1 -p
查询出的结果如图7.26所示,其中减号代表删除的部分,加号代表添加的部分。
图7.26查看提交记录的具体修改内容
好了,本次的Git时间就到这里,下面我们来对本章中所学的知识做个回顾吧。
7.6小结与点评
本章的内容不算多,而且很多时候都是在使用上一章中学习的数据库知识,所以理解这部分 内容对你来说应该是比较轻松的吧。在本章中,我们一开始先了解了 Android的权限机制,并且学会了如何在6.0以上的系统中使用运行时权限,然后又重点学习了内容提供器的相关内容,以实现跨程序数据共享的功能。现在你不仅知道了如何去访问其他程序中的数据,还学会了怎样创建自己的内容提供器来共享数据,收获还是挺大的吧。
不过每次在创建内容提供器的时候,你都需要提醒一下自己,我是不是应该这么做?因为只 有真正需要将数据共享出去的时候我们才应该创建内容提供器,仅仅是用于程序内部访问的数据 就没有必要这么做,所以千万别对它进行滥用。
在连续学了几章系统机制方面的内容之后是不是感觉有些枯燥?那么下一章中我们就来换换口味,学习一下Android多媒体方面的知识吧。
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
