乙巳🐍年

acc8226 的博客

ContentProvider

构建 content URI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class TaskContract {

/* COMPLETED (1) Add content provider constants to the Contract
Clients need to know how to access the task data, and it's your job to provide
these content URI's for the path to that data:
1) Content authority,
2) Base content URI,
3) Path(s) to the tasks directory
4) Content URI for data in the TaskEntry class
*/

// The authority, which is how your code knows which Content Provider to access
public static final String AUTHORITY = "com.example.android.todolist";

// The base content URI = "content://" + <authority>
public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + AUTHORITY);

// Define the possible paths for accessing data in this contract
// This is the path for the "tasks" directory
public static final String PATH_TASKS = "tasks";

/* TaskEntry is an inner class that defines the contents of the task table */
public static final class TaskEntry implements BaseColumns {

// TaskEntry content URI = base content URI + path
public static final Uri CONTENT_URI =
BASE_CONTENT_URI.buildUpon().appendPath(PATH_TASKS).build();

...
...
...

/*
The above table structure looks something like the sample table below.
With the name of the table and columns on top, and potential contents in rows
Note: Because this implements BaseColumns, the _id column is generated automatically
tasks
- - - - - - - - - - - - - - - - - - - - - -
| _id | description | priority |
- - - - - - - - - - - - - - - - - - - - - -
| 1 | Complete lesson | 1 |
- - - - - - - - - - - - - - - - - - - - - -
| 2 | Go shopping | 3 |
- - - - - - - - - - - - - - - - - - - - - -
.
.
.
- - - - - - - - - - - - - - - - - - - - - -
| 43 | Learn guitar | 2 |
- - - - - - - - - - - - - - - - - - - - - -
*/

}
}

构建 URiMatcher

  1. 为多行和单行数据定义final整型常量
  2. 为完整的UriMatcher声明静态全局变量
  3. 定义一个将URI与整型匹配项关联的buildUriMatcher方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// Verify that TaskContentProvider extends from ContentProvider and implements required methods
public class TaskContentProvider extends ContentProvider {

// COMPLETED (1) Define final integer constants for the directory of tasks and a single item.
// It's convention to use 100, 200, 300, etc for directories,
// and related ints (101, 102, ..) for items in that directory.
public static final int TASKS = 100;
public static final int TASK_WITH_ID = 101;

// COMPLETED (3) Declare a static variable for the Uri matcher that you construct
private static final UriMatcher sUriMatcher = buildUriMatcher();

// COMPLETED (2) Define a static buildUriMatcher method that associates URI's with their int match
/**
Initialize a new matcher object without any matches,
then use .addURI(String authority, String path, int match) to add matches
*/
public static UriMatcher buildUriMatcher() {

// Initialize a UriMatcher with no matches by passing in NO_MATCH to the constructor
UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

/*
All paths added to the UriMatcher have a corresponding int.
For each kind of uri you may want to access, add the corresponding match with addURI.
The two calls below add matches for the task directory and a single item by ID.
*/
uriMatcher.addURI(TaskContract.AUTHORITY, TaskContract.PATH_TASKS, TASKS);
uriMatcher.addURI(TaskContract.AUTHORITY, TaskContract.PATH_TASKS + "/#", TASK_WITH_ID);

return uriMatcher;
}

// Member variable for a TaskDbHelper that's initialized in the onCreate() method
private TaskDbHelper mTaskDbHelper;

/* onCreate() is where you should initialize anything you’ll need to setup
your underlying data source.
In this case, you’re working with a SQLite database, so you’ll need to
initialize a DbHelper to gain access to it.
*/
@Override
public boolean onCreate() {
// Complete onCreate() and initialize a TaskDbhelper on startup
// [Hint] Declare the DbHelper as a global variable

Context context = getContext();
mTaskDbHelper = new TaskDbHelper(context);
return true;
}


@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {

throw new UnsupportedOperationException("Not yet implemented");
}


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

throw new UnsupportedOperationException("Not yet implemented");
}


@Override
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {

throw new UnsupportedOperationException("Not yet implemented");
}


@Override
public int update(@NonNull Uri uri, ContentValues values, String selection,
String[] selectionArgs) {

throw new UnsupportedOperationException("Not yet implemented");
}


@Override
public String getType(@NonNull Uri uri) {

throw new UnsupportedOperationException("Not yet implemented");
}

}

数据库解析流程

insert

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Implement insert to handle requests to insert a single new row of data
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
// Get access to the task database (to write new data to)
final SQLiteDatabase db = mTaskDbHelper.getWritableDatabase();

// Write URI matching code to identify the match for the tasks directory
int match = sUriMatcher.match(uri);
Uri returnUri; // URI to be returned

switch (match) {
case TASKS:
// Insert new values into the database
// Inserting values into tasks table
long id = db.insert(TABLE_NAME, null, values);
if ( id > 0 ) {
returnUri = ContentUris.withAppendedId(TaskContract.TaskEntry.CONTENT_URI, id);
} else {
throw new android.database.SQLException("Failed to insert row into " + uri);
}
break;
// Set the value for the returnedUri and write the default case for unknown URI's
// Default case throws an UnsupportedOperationException
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}

// Notify the resolver if the uri has been changed, and return the newly inserted URI
getContext().getContentResolver().notifyChange(uri, null);

// Return constructed uri (this points to the newly inserted row of data)
return returnUri;
}

query
以下是查询一个任务的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// Implement query to handle requests for data by URI
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {

// Get access to underlying database (read-only for query)
final SQLiteDatabase db = mTaskDbHelper.getReadableDatabase();

// Write URI match code
// Write a query for the tasks directory and default case

int match = sUriMatcher.match(uri);
Cursor retCursor;

switch (match) {
// Query for the tasks directory
case TASKS:
retCursor = db.query(TABLE_NAME,
projection,
selection,
selectionArgs,
null,
null,
sortOrder);
break;

// Add a case to query for a single row of data by ID
// Use selections and selectionArgs to filter for that ID
case TASK_WITH_ID:
// Get the id from the URI
String id = uri.getPathSegments().get(1);

// Selection is the _ID column = ?, and the Selection args = the row ID from the URI
String mSelection = "_id=?";
String[] mSelectionArgs = new String[]{id};

// Construct a query as you would normally, passing in the selection/args
retCursor = db.query(TABLE_NAME,
projection,
mSelection,
mSelectionArgs,
null,
null,
sortOrder);
break;

// Default exception
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}

// Set a notification URI on the Cursor
retCursor.setNotificationUri(getContext().getContentResolver(), uri);

// Return the desired Cursor
return retCursor;
}

update

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Update won't be used in the final ToDoList app but is implemented here for completeness
// This updates a single item (by it's ID) in the tasks directory
@Override
public int update(@NonNull Uri uri, ContentValues values, String selection,
String[] selectionArgs) {

//Keep track of if an update occurs
int tasksUpdated;

// match code
int match = sUriMatcher.match(uri);

switch (match) {
case TASK_WITH_ID:
//update a single task by getting the id
String id = uri.getPathSegments().get(1);
//using selections
tasksUpdated = mTaskDbHelper.getWritableDatabase().update(TABLE_NAME, values, "_id=?", new String[]{id});
break;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}

if (tasksUpdated != 0) {
//set notifications if a task was updated
getContext().getContentResolver().notifyChange(uri, null);
}

// return number of tasks updated
return tasksUpdated;
}

delete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Implement delete to delete a single row of data
@Override
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {

// Get access to the database and write URI matching code to recognize a single item
final SQLiteDatabase db = mTaskDbHelper.getWritableDatabase();

int match = sUriMatcher.match(uri);
// Keep track of the number of deleted tasks
int tasksDeleted; // starts as 0

// Write the code to delete a single row of data
// [Hint] Use selections to delete an item by its row ID
switch (match) {
// Handle the single item case, recognized by the ID included in the URI path
case TASK_WITH_ID:
// Get the task ID from the URI path
String id = uri.getPathSegments().get(1);
// Use selections/selectionArgs to filter for this ID
tasksDeleted = db.delete(TABLE_NAME, "_id=?", new String[]{id});
break;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}

// Notify the resolver of a change and return the number of items deleted
if (tasksDeleted != 0) {
// A task was deleted, set notification
getContext().getContentResolver().notifyChange(uri, null);
}

// Return the number of tasks deleted
return tasksDeleted;
}

getType
以下是 ToDo list 应用的 getType 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* getType() handles requests for the MIME type of data
We are working with two types of data:
1) a directory and 2) a single row of data.
This method will not be used in our app, but gives a way to standardize the data formats
that your provider accesses, and this can be useful for data organization.
For now, this method will not be used but will be provided for completeness.
*/
@Override
public String getType(@NonNull Uri uri) {
int match = sUriMatcher.match(uri);

switch (match) {
case TASKS:
// directory
return "vnd.android.cursor.dir" + "/" + TaskContract.AUTHORITY + "/" + TaskContract.PATH_TASKS;
case TASK_WITH_ID:
// single item type
return "vnd.android.cursor.item" + "/" + TaskContract.AUTHORITY + "/" + TaskContract.PATH_TASKS;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
}

  • 创建 Entry 的内部类, 该类实现 BaseColumns
  • tableName 定义常量字符串
  • 为 table 的每一项什么常量字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.android.waitlist.data;

import android.provider.BaseColumns;

public class WaitlistContract {

// COMPLETED (1) Create an inner class named WaitlistEntry class that implements the BaseColumns interface
public static final class WaitlistEntry implements BaseColumns {
// COMPLETED (2) Inside create a static final members for the table name and each of the db columns
public static final String TABLE_NAME = "waitlist";
public static final String COLUMN_GUEST_NAME = "guestName";
public static final String COLUMN_PARTY_SIZE = "partySize";
public static final String COLUMN_TIMESTAMP = "timestamp";
}

}
阅读全文 »

设置可接受的范围

要将可接受的值限制在 0(不包括)和 3(包括)之间,我们选择使用 PreferenceChangeListener - 它与 SharedPreferenceChangeListener 的不同之处为:

  • SharedPreferenceChangeListener 在任何值保存到 SharedPreferences 文件后被触发。
  • PreferenceChangeListener 在值保存到 SharedPreferences 文件前被触发。因此,可以防止对偏好设置做出无效的更新。PreferenceChangeListeners 也附加到了单个偏好设置上。

流程通常如下所示:

  1. 用户更新偏好设置。
  2. 该偏好设置触发了 PreferenceChangeListener。
  3. 新的值保存到了 SharedPreference 文件。
  4. onSharedPreferenceChanged 监听器被触发。

除此之外,它们的行为很相似。你将在你的 Activity中实现Preference.OnPreferenceChangeListener,覆盖onPreferenceChange(Preference preference, Object newValue)。 onPreferenceChange 方法将返回 true 或 false,取决于偏好设置实际上是否要被保存。

若要妥善管理 Activity 生命周期,我们建议您在 onResume() 和 onPause() 回调期间分别注册和注销SharedPreferences.OnSharedPreferenceChangeListener

阅读全文 »

常见容器视图示例:

类名称 description
LinearLayout 在一行或一列里显示视图。
RelativeLayout 相对某个视图放置其他视图。
FrameLayout ViewGroup 包含一个子视图。
ScrollView 一种 FrameLayout,旨在让用户能够在视图中滚动查看内容。
ConstraintLayout 这是更新的 viewgroup;可以灵活地放置视图。在这节课的稍后阶段,我们将学习 ConstraintLayout。

Activity 会创建视图来向用户显示信息,并使用户与 Activity 互动。视图是 Android UI 框架中的类。它们占据了屏幕上的方形区域,负责绘制并处理事件。Activity 通过读取 XML 布局文件确定要创建哪些视图(并放在何处)。这些 XML 文件存储在标记为 layouts 的 res 文件夹内。

  • onCreate 的 super 方法放在第一句, onPause 以后的 super 的方法放在最后一句.

onCreate
onStart
onResume

onPaush
onStop
onDestroy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
package com.example.android.lifecycle;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

//udacity官方实例activity生命周期代碼
public class MainActivity extends AppCompatActivity {

/*
* This tag will be used for logging. It is best practice to use the class's name using
* getSimpleName as that will greatly help to identify the location from which your logs are
* being posted.
*/
private static final String TAG = MainActivity.class.getSimpleName();

/* Constant values for the names of each respective lifecycle callback */
private static final String ON_CREATE = "onCreate";
private static final String ON_START = "onStart";
private static final String ON_RESUME = "onResume";
private static final String ON_PAUSE = "onPause";
private static final String ON_STOP = "onStop";
private static final String ON_RESTART = "onRestart";
private static final String ON_DESTROY = "onDestroy";
private static final String ON_SAVE_INSTANCE_STATE = "onSaveInstanceState";

/*
* This TextView will contain a running log of every lifecycle callback method called from this
* Activity. This TextView can be reset to its default state by clicking the Button labeled
* "Reset Log"
*/
private TextView mLifecycleDisplay;

/**
* Called when the activity is first created. This is where you should do all of your normal
* static set up: create views, bind data to lists, etc.
*
* Always followed by onStart().
*
* @param savedInstanceState The Activity's previously frozen state, if there was one.
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

mLifecycleDisplay = (TextView) findViewById(R.id.tv_lifecycle_events_display);

// COMPLETED (1) Use logAndAppend within onCreate
logAndAppend(ON_CREATE);
}

// COMPLETED (2) Override onStart, call super.onStart, and call logAndAppend with ON_START
/**
* Called when the activity is becoming visible to the user.
*
* Followed by onResume() if the activity comes to the foreground, or onStop() if it becomes
* hidden.
*/
@Override
protected void onStart() {
super.onStart();

logAndAppend(ON_START);
}

// COMPLETED (3) Override onResume, call super.onResume, and call logAndAppend with ON_RESUME
/**
* Called when the activity will start interacting with the user. At this point your activity
* is at the top of the activity stack, with user input going to it.
*
* Always followed by onPause().
*/
@Override
protected void onResume() {
super.onResume();

logAndAppend(ON_RESUME);
}

// COMPLETED (4) Override onPause, call super.onPause, and call logAndAppend with ON_PAUSE
/**
* Called when the system is about to start resuming a previous activity. This is typically
* used to commit unsaved changes to persistent data, stop animations and other things that may
* be consuming CPU, etc. Implementations of this method must be very quick because the next
* activity will not be resumed until this method returns.
*
* Followed by either onResume() if the activity returns back to the front, or onStop() if it
* becomes invisible to the user.
*/
@Override
protected void onPause() {
super.onPause();

logAndAppend(ON_PAUSE);
}

// COMPLETED (5) Override onStop, call super.onStop, and call logAndAppend with ON_STOP
/**
* Called when the activity is no longer visible to the user, because another activity has been
* resumed and is covering this one. This may happen either because a new activity is being
* started, an existing one is being brought in front of this one, or this one is being
* destroyed.
*
* Followed by either onRestart() if this activity is coming back to interact with the user, or
* onDestroy() if this activity is going away.
*/
@Override
protected void onStop() {
super.onStop();

logAndAppend(ON_STOP);
}

// COMPLETED (6) Override onRestart, call super.onRestart, and call logAndAppend with ON_RESTART
/**
* Called after your activity has been stopped, prior to it being started again.
*
* Always followed by onStart()
*/
@Override
protected void onRestart() {
super.onRestart();

logAndAppend(ON_RESTART);
}

// COMPLETED (7) Override onDestroy, call super.onDestroy, and call logAndAppend with ON_DESTROY
/**
* The final call you receive before your activity is destroyed. This can happen either because
* the activity is finishing (someone called finish() on it, or because the system is
* temporarily destroying this instance of the activity to save space. You can distinguish
* between these two scenarios with the isFinishing() method.
*/
@Override
protected void onDestroy() {
super.onDestroy();

logAndAppend(ON_DESTROY);
}

/**
* Logs to the console and appends the lifecycle method name to the TextView so that you can
* view the series of method callbacks that are called both from the app and from within
* Android Studio's Logcat.
*
* @param lifecycleEvent The name of the event to be logged.
*/
private void logAndAppend(String lifecycleEvent) {
Log.d(TAG, "Lifecycle Event: " + lifecycleEvent);

mLifecycleDisplay.append(lifecycleEvent + "\n");
}

/**
* This method resets the contents of the TextView to its default text of "Lifecycle callbacks"
*
* @param view The View that was clicked. In this case, it is the Button from our layout.
*/
public void resetLifecycleDisplay(View view) {
mLifecycleDisplay.setText("Lifecycle callbacks:\n");
}
}
Intent 匹配

Data 本身又分为两种方式进行匹配:MIMEType 和 URI。

MIMEType 就是指要访问的组件处理的数据类型,例如 video/mpeg4、video/mp4、video/avi 等。
MIMEType 也可以用通配符()匹配某一类型的数据,例如“audio/”表示所有的音频数据格式.

URI 有些类似我们经常使用的Web地址,但要比Web地址范围更广,例如,下面的3行字符串都属于 URI。
http: //www.google.com
content: //mobile.android.data/cities
ftp: //192.168.17.168
总结
定位窗口:

  • 通过Componentname、Action、Category 和 Data 可以定位一个或多个窗口。
  • 传递数据:通过 Data 和 Extra。
  • 控制访问组件的行为(窗口、服务和广播):通过Flags。

注意显示调用过程中action为null, 这可作为判断是否显式调用/隐式调用的条件.

从常理推测<category>标签应该是可选的,不过实际上<category>也是必须加的,原因就是一个特殊的Category:android.intent.category.DEFAULT。如果调用者不添加Category,按常理会认为过滤条件中不包含Category,系统自然也就不考虑Category了。不过Android系统并不是这样认为的。不管调用者是否添加Category,系统都会认为有一个默认的Category已经被添加。相当于调用者执行如下的代码。intent.addCategory(Intent.CATEGORY_DEFAULT);
既然调用者默认加入了一个Category,那么被调用这自然也需要在过滤器(<intent-filter>标签)中加入如下的<category>标签了。

常用 Intent.FLAG

如果同时设置了 launchMode 和标志动作,则标志动作优先于相应的launchMode 属性值.

  • singgletop: FLAG_ACTIVITY_SINGLE_ TOP
  • singletask: FLAG_ACTIVITY_SINGLE_TOP + FLAG_ACTIVITY_CLEAR_TOP

FLAG_ACTIVITY_CLEAR_TASK
如果在调用 Context.startActivity 时传递这个标记,将会导致任何用来放置该 activity 的已经存在的 task 里面的已经存在的 activity 先清空,然后该 activity 再在该 task 中启动,也就是说,这个新启动的 activity 变为了这个空 tas 的根 activity.所有老的 activity 都结束掉。该标志必须和 FLAG_ACTIVITY_NEW_TASK 一起使用。

集中式 or 分布式

集中化的版本控制系统( Centralized Version Control Systems,简称 CVCS)

缺点:最显而易见的缺点是中央服务器的单点故障。如果宕机一小时,那么在这一小时内,谁都无法提交更新,也就无法协同工作。要是中央服务器的磁盘发生故障,碰巧没做备份,或者备份不够及时,就会有丢失数据的风险。最坏的情况是彻底丢失整个项目的所有历史更改记录,而被客户端偶然提取出来的保存在本地的某些快照数据就成了恢复数据的希望。但这样的话依然是个问题,你不能保证所有的数据都已经有人事先完整提取出来过。本地版本控制系统也存在类似问题,只要整个项目的历史记录被保存在单一位置,就有丢失所有历史更新记录的风险。

分布式版本控制系统(Distributed Version Control System,简称 DVCS)面世了,客户端并不只提取最新版本的文件快照,而是把代码仓库完整地镜像下来。这么一来,任何一处协同工作用的服务器发生故障,事后都可以用任何一个镜像出来的本地仓库恢复。因为每一次的提取操作,实际上都是一次对代码仓库的完整备份。更进一步,许多这类系统都可以指定和若干不同的远端代码仓库进行交互。籍此,你就可以在同一个项目中,分别和不同工作小组的人相互协作。你可以根据需要设定不同的协作流程,比如层次模型式的工作流,而这在以前的集中式系统中是无法实现的。

阅读全文 »
0%