• <noscript id="e0iig"><kbd id="e0iig"></kbd></noscript>
  • <td id="e0iig"></td>
  • <option id="e0iig"></option>
  • <noscript id="e0iig"><source id="e0iig"></source></noscript>
  • Content Provider與SQLite結合使用

    標簽: 數據庫  android  sqlite

    前言

      雖然推薦使用數據庫保存結構化的復雜數據,但是數據共享是一個挑戰,因為數據庫只允許創建它的應用訪問。

    在Android中共享數據

      在Android中,推薦使用content provider方法在不同包之間共享數據。content provider可以被視為一個數據倉庫。它如何存儲數據與應用如何使用它無關。然而,應用如何使用一致的編程接口在它的里面的數據非常重要。content provider的行為與數據庫非常相似——你可以查詢它、編輯它的內容、添加或者刪除內容。然而,與數據庫不同的是,一個content provider可以使用不同的方式存儲它的數據。數據可以被存儲在數據庫中、在文件中、甚至在網絡上。
      Android提供了許多非常有用的content provider,包括以下幾種:

    類型 說明
    Browser 存儲瀏覽器數據,比如瀏覽器的書簽、瀏覽器訪問歷史等。
    CallLog 存儲通話數據,比如未接電話、通話詳情等。
    Contacts 存儲通訊錄詳情。
    MediaStore 存儲多媒體文件,比如音頻、視頻和圖片。
    Settings 存儲設備的設置和偏好設置。

      除了許多內置的content provider以外,可以創建自定義的content provider。
      要查詢一個content provider,需要以統一的資源標識符(Uniform Resource Identifier,URI)的形式制定查詢字符串,以及一個可選的特定行說明符。以下是查詢URI的形式:

    <standard_prefix>://<authority>/<data_path>/<id>

      URI的各種組成部分如下:

    組成 說明
    content provider content provider的標準前綴是content://
    authority 指定content provider的名稱。例如內置Contacts的content provider的名稱為contacts。對于第三方content provider來說,它可以是完整的合法名稱,例如com.wrox.provider。
    data_path 指定請求的數據種類。例如,如果你要從Contacts的content provider中獲取所有的通訊錄,那么data_path應該是people,URI將類似與:content://contacts/people
    id 指定請求特定的記錄。例如,如果你要獲取Contacts的content provider中第2條通訊錄,URI將類似類似于:content://contact/people/2
    查詢字符串 描述
    content://media/internal/images 返回設備上保存在內部存儲中的圖片列表。
    content://media/external/images 返回設備上保存在外部存儲(例如SD卡)中的圖片列表。
    content://call_log/calls 返回登記在通話記錄中的通話記錄。
    content://browser/bookmarks 返回存儲在瀏覽器中的書簽列表。

    使用content provider

      理解content provider的最佳方法是在實踐中使用它。

    Main2Activity

    public class Main2Activity extends ListActivity {
        final private int REQUEST_READ_CONTACTS = 123;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main2);
    
            if (ContextCompat.checkSelfPermission(this,
                    Manifest.permission.READ_CONTACTS)
                    != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(this,
                        new String[]{Manifest.permission.READ_CONTACTS},
                        REQUEST_READ_CONTACTS);
            } else {
                listContacts();
            }
        }
    
        @Override
        public void onRequestPermissionsResult(int requestCode
                , @NonNull String[] permissions, @NonNull int[] grantResults) {
            switch (requestCode) {
                case REQUEST_READ_CONTACTS:
                    if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                        listContacts();
                    } else {
                        Toast.makeText(Main2Activity.this
                                , "Permission Denied", Toast.LENGTH_SHORT).show();
                    }
                    break;
                default:
                    super.onRequestPermissionsResult(requestCode
                            , permissions, grantResults);
            }
        }
    
        protected void listContacts() {
            Uri allContacts = Uri.parse("content://contacts/people");
            CursorLoader cursorLoader = new CursorLoader(
                    this,
                    allContacts,
                    null,
                    null,
                    null,
                    null);
            Cursor cursor = cursorLoader.loadInBackground();
            String[] columns = new String[]{
                    ContactsContract.Contacts.DISPLAY_NAME,
                    ContactsContract.Contacts._ID};
            int[] views = new int[]{R.id.contactName, R.id.contactID};
            SimpleCursorAdapter adapter;
            adapter = new SimpleCursorAdapter(
                    this, R.layout.activity_main2, cursor, columns, views,
                    CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
            this.setListAdapter(adapter);
        }
    }

      本示例獲取了存儲在Contacts應用中的通訊錄并且將其顯示在ListView中。
      首先指定訪問Contacts應用的URI:

    Uri allContacts = Uri.parse("content://contacts/people");

      其次,檢查應用是否由訪問Contacts的權限:

    if (ContextCompat.checkSelfPermission(this,
        Manifest.permission.READ_CONTACTS)
        != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(this,
            new String[]{Manifest.permission.READ_CONTACTS},
            REQUEST_READ_CONTACTS);
    } else {
        listContacts();
    }

      如果沒有訪問權限,就會發出一條權限請求(使Android彈出一個權限請求對話框)。如果應用由相應的訪問權限,ListContacts()方法會被調用。
      getContentResolver()方法返回一個ContentResolver對象,它會使用適當content provider解析內容URI。
      CursorLoader類(Android API level 11及以上版本可用)在后再線程中執行cursor查詢操作,因此不會阻塞應用UI。

    CursorLoader cursorLoader = new CursorLoader(
        this,
        allContacts,
        null,
        null,
        null,
        null);
    Cursor cursor = cursorLoader.loadInBackground();

      SimpleCursorAdapter對象將一個Cursor數據映射到XML文件(activity_main2.xml)中定義的TextView(或者ImageView)。它將數據(對應于代碼中的columns變量)映射到視圖(對應于代碼中的view變量)中:

    String[] columns = new String[]{
        ContactsContract.Contacts.DISPLAY_NAME,
        ContactsContract.Contacts._ID
    };
    int[] views = new int[]{
        R.id.contactName, R.id.contactID
    };
    SimpleCursorAdapter adapter = new SimpleCursorAdapter(
        this, R.layout.activity_main2, cursor, columns, views,
        CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
    this.setListAdapter(adapter);

      類似于managedQuery()方法,SimpleCursorAdapter類的一個構造函數已經被棄用。對于運行Honeycomb及后續版本的設備,需要使用新的SimpleCursorAdapter類的構造函數,與舊版本相比,該構造函數多一個參數:

     SimpleCursorAdapter adapter = new SimpleCursorAdapter(
        this, R.layout.activity_main2, cursor, columns, views,
        CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);

      新的標志參數將該適配器注冊為觀察者,當Content Provider發生變化時,該適配器會被通知。
      注意,如果你的應用要訪問Contacts應用,需要在AndroidManifest.xml文件中添加READ_CONTACTS權限。

    CursorLoader說明

    CursorLoader (Context context, 
                    Uri uri, 
                    String[] projection, 
                    String selection, 
                    String[] selectionArgs, 
                    String sortOrder)
    參數 類型 說明
    context Context
    uri Uri Uri: The URI, using the content:// scheme, for the content to retrieve. This value must never be null.
    projection String String: A list of which columns to return. Passing null will return all columns, which is inefficient.
    selection String String: A filter declaring which rows to return, formatted as an SQL WHERE clause (excluding the WHERE itself). Passing null will return all rows for the given URI.
    selectionArgs String String: You may include ?s in selection, which will be replaced by the values from selectionArgs, in the order that they appear in the selection. The values will be bound as Strings.This value may be null.
    sortOrder String String: How to order the rows, formatted as an SQL ORDER BY clause (excluding the ORDER BY itself). Passing null will use the default sort order, which may be unordered.

    預定義查詢字符串常量

    Uri allContacts = Uri.parse("content://contacts/people");

      等同于:

    Uri allContacts = ContactsContract.Contacts.CONTENT_URI;

      在下面的示例中,通過訪問ContactsContract.Contacts._ID字段獲取一條通訊錄的ID,通過訪問ContactsContract.Contacts.DISPLAY_NAME字段獲取一條通訊錄的姓名。如果想要顯示通訊錄電話號碼,可以再次查詢content provider,因為這個信息存儲在另外一個表中:

    private void printContacts(Cursor c) {
        if (c.moveToFirst()) {
            do {
                String contactID = c.getString(c.getColumnIndex(ContactsContract.Contacts._ID));
                String contactDisplayName = c.getString(c.getColumnIndex(
                    ContactsContract.Contacts.DISPLAY_NAME));
                Log.v("ContentProviders", contactID + ", " +
                    contactDisplayName);
                    // 獲取電話號碼
                Cursor phoneCursor =
                getContentResolver().query(
                    ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                    null,
                    ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = " +
                    contactID,
                    null,
                    null);
                assert phoneCursor != null;
                while (phoneCursor.moveToNext()) {
                    Log.v("ContentProviders", phoneCursor.getString(phoneCursor.getColumnIndex(
                        ContactsContract.CommonDataKinds.Phone.NUMBER)));
                }
                phoneCursor.close();
            } while (c.moveToNext());
        }
    }

    注意,要訪問通訊錄中的電話號碼,需要使用保存在ContactsContract.CommonDataKinds.Phone.CONTENT_URI常量中的URI查詢。

      顯示結果:

    V/ContentProviders: 1, I think is okay
    V/ContentProviders: 100
    V/ContentProviders: 2, Java
    V/ContentProviders: 1 234-567-9
    V/ContentProviders: 3, Kotlin
    V/ContentProviders: 1 23
    V/ContentProviders: 4, Scala
    V/ContentProviders: 45
    V/ContentProviders: 5, Python
    V/ContentProviders: 78
    V/ContentProviders: 6, Ruby
    V/ContentProviders: 36
    V/ContentProviders: 7, Gradle
    V/ContentProviders: 258-
    V/ContentProviders: 8, JavaScript
    V/ContentProviders: 1 4
    V/ContentProviders: 9, Haskell
    V/ContentProviders: 1 5
    V/ContentProviders: 10, C/C+
    V/ContentProviders: 35
    V/ContentProviders: 11, Html+CSS
    V/ContentProviders: 248-6

    指定查詢字段

      CursorLoader類的第三個參數控制查詢返回多少列。這個參數稱為Projections(映射)。

    String[] projection = new String[]{
        ContactsContract.Contacts._ID,
        ContactsContract.Contacts.DISPLAY_NAME
    };
    CursorLoader cursorLoader = new CursorLoader(
        this,
        allContacts,
        projection,
        null,
        null,
        null);
    Cursor cursor = cursorLoader.loadInBackground();

    篩選

      CursorLoader類的第四和第五個參數指定一個SQL WHERE語句用來篩選查詢的結果。例如,以下示例代碼僅僅獲取姓名以Lee結尾的通訊錄:

    String[] projection = new String[]{
            ContactsContract.Contacts._ID,
            ContactsContract.Contacts.DISPLAY_NAME
    };
    CursorLoader cursorLoader = new CursorLoader(
            this,
            allContacts,
            projection,
            ContactsContract.Contacts.DISPLAY_NAME + " LIKE ?",
            new String[]{"%Lee"},
            null;
    Cursor cursor = cursorLoader.loadInBackground();

    排序

      CursorLoader類的最后一個參數指定SQL ORDER BY語句用來排序查詢結果。例如,以下示例代碼將通訊錄姓名以升序排序:

    String[] projection = new String[]{
            ContactsContract.Contacts._ID,
            ContactsContract.Contacts.DISPLAY_NAME
    };
    CursorLoader cursorLoader = new CursorLoader(
            this,
            allContacts,
            projection,
            ContactsContract.Contacts.DISPLAY_NAME + " LIKE ?",
            new String[]{"%Lee"},
            ContactsContract.Contacts.DISPLAY_NAME + " ASC");
    Cursor cursor = cursorLoader.loadInBackground();

    自定義Content Provider

    AndroidManifest.xml

      先在AndroidManifest.xml中添加如下語句:

    <provider
            android:name=".BooksProvider"
            android:authorities="link_work.myapplication.provider.Books"
            android:exported="false" />

      說明:android:authorities="<包名>.provider.Books"

    BooksProvider

    方法 說明
    getType() 返回給定URI的數據的MIME類型。
    onCreate() 當provider啟動時調用。
    query() 收到一條客戶的請求。結果將以Cursor對象返回。
    insert() 向content provider插入一條新的記錄。
    delete() 從content provider中刪除一條已存在的記錄。
    update() 從content provider中更新一條已存在的記錄。

      在自定義的content provider中,可以自由地選擇如何存儲數據——使用傳統文件系統、XML文件、數據庫,或者通過Web服務。在本示例中,使用的是SQLite數據庫方案。

    @SuppressLint("Registered")
    public class BooksProvider extends ContentProvider {
        /**
         * 常量
         */
        static final String PROVIDER_NAME = "link_work.myapplication.provider.Books";
        static final Uri CONTENT_URI = Uri.parse("content://" + PROVIDER_NAME + "/books");
        static final String ID = "_id";
        static final String TITLE = "title";
        static final String ISBN = "isbn";
        static final int BOOKS = 1;
        static final int BOOK_ID = 2;
        private static final UriMatcher URI_MATCHER;
    
        /* *
         * 使用URI_MATCHER對象解析內容URI,將內容URI通過ContentResolver傳遞給
         * content provider。例如,以下內容URI表示請求content provider中的所
         * 有圖書。
         * */
        static {
            URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
            URI_MATCHER.addURI(PROVIDER_NAME, "books", BOOKS);
            URI_MATCHER.addURI(PROVIDER_NAME, "books/#", BOOK_ID);
        }
    
        /**
         * ---數據庫實例---
         */
        SQLiteDatabase booksDB;
        static final String DATABASE_NAME = "Books";
        static final String DATABASE_TABLE = "titles";
        static final int DATABASE_VERSION = 1;
        static final String DATABASE_CREATE = "create table " + DATABASE_TABLE +
                " (_id integer primary key autoincrement, "
                + "title text not null, isbn text not null);";
    
        /**
         * 要刪除一本書,需要重寫delete方法。
         * 同樣,在刪除成功后調用ContentResolver的notifyChange方法。
         * 它會通知已注冊的觀察者有一條記錄要刪除。
         * */
        @Override
        public int delete(@NonNull Uri arg0, String arg1, String[] arg2) {
    //      arg0 = uri
    //      arg1 = selection
    //      arg2 = selectionArgs
            int count;
            switch (URI_MATCHER.match(arg0)) {
                case BOOKS:
                    count = booksDB.delete(DATABASE_TABLE, arg1, arg2);
                    break;
                case BOOK_ID:
                    String id = arg0.getPathSegments().get(1);
                    count = booksDB.delete(DATABASE_TABLE, ID + " = " + id +
                            (!TextUtils.isEmpty(arg1) ? " AND (" + arg1 + ')' : ""), arg2);
                    break;
                default:
                    throw new IllegalArgumentException("Unknown URI " + arg0);
            }
            getContext().getContentResolver().notifyChange(arg0, null);
            return count;
        }
    
        /**
         * 重寫getType方法,為自定義Content Provider描述數據類型。使用UriMatcher對象解析
         * URI,vnd.android.cursor.item/vnd.<包名>.books表示返回一條圖書記錄,
         * vnd.android.cursor.dir/vnd.<包名>.books表示返回多條圖書記錄。
         * */
        @Override
        public String getType(@NonNull Uri uri) {
            switch (URI_MATCHER.match(uri)) {
                //---獲取所有的書籍---
                case BOOKS:
                    return "vnd.android.cursor.dir/vnd.link_work.myapplication.books ";
                //---獲取指定的書籍---
                case BOOK_ID:
                    return "vnd.android.cursor.item/vnd.link_work.myapplication.books ";
                default:
                    throw new IllegalArgumentException("Unsupported URI: " + uri);
            }
        }
    
        /**
         * 默認情況下,查詢的結果按照title字段排序。查詢結果以Cursor對象返回。
         * 為了能夠向content Provider中插入新的圖書記錄,需要重寫insert方法。
         * 當數據插入成功后,調用ContentResolver的notifyChange方法。它會通
         * 知已注冊的觀察者由一條記錄更新。
         * */
        @Override
        public Uri insert(@NonNull Uri uri, ContentValues values) {
            //---添加一本書---
            long rowID = booksDB.insert(DATABASE_TABLE, "", values);
            //---如果添加成功的話---
            if (rowID > 0) {
                Uri tempUri = ContentUris.withAppendedId(CONTENT_URI, rowID);
                getContext().getContentResolver().notifyChange(tempUri, null);
                return tempUri;
            }
            //---添加不成功---
            throw new SQLException("Failed to insert row into " + uri);
        }
    
        /**
         * 重寫onCreate方法,當content Provider啟動的時候,打開數據庫連接。
         * */
        @Override
        public boolean onCreate() {
            Context context = getContext();
            DatabaseHelper dbHelper = new DatabaseHelper(context);
            booksDB = dbHelper.getWritableDatabase();
            return booksDB != null;
        }
    
        /**
         * 重寫query方法,使得用戶可以查詢圖書。
         * */
        @Override
        public Cursor query(@NonNull Uri uri, String[] projection, String selection,
                            String[] selectionArgs, String sortOrder) {
            SQLiteQueryBuilder sqlBuilder = new SQLiteQueryBuilder();
            sqlBuilder.setTables(DATABASE_TABLE);
            //---如果要獲取制定的圖書信息--
            if (URI_MATCHER.match(uri) == BOOK_ID) {
                sqlBuilder.appendWhere(ID + " = " + uri.getPathSegments().get(1));
            }
            if (sortOrder == null || Objects.equals(sortOrder, "")) {
                sortOrder = TITLE;
            }
            Cursor c = sqlBuilder.query(
                    booksDB,
                    projection,
                    selection,
                    selectionArgs,
                    null,
                    null,
                    sortOrder);
            //---注冊一個觀察者來監視Uri的變化---
            c.setNotificationUri(getContext().getContentResolver(), uri);
            return c;
        }
    
        /**
         * 要更新一本書,需要重寫update方法。
         * 如同insert方法和delete方法,更新成功后你需要調用ContentResolver
         * 的notifyChange方法。它會通知已注冊的觀察者有一條記錄被更新。
         * */
        @Override
        public int update(@NonNull Uri uri, ContentValues values, String selection,
                          String[] selectionArgs) {
            int count;
            switch (URI_MATCHER.match(uri)) {
                case BOOKS:
                    count = booksDB.update(
                            DATABASE_TABLE,
                            values,
                            selection,
                            selectionArgs);
                    break;
                case BOOK_ID:
                    count = booksDB.update(
                            DATABASE_TABLE,
                            values,
                            ID + " = " + uri.getPathSegments().get(1) +
                                    (!TextUtils.isEmpty(selection) ? " AND (" +
                                            selection + ')' : ""),
                            selectionArgs);
                    break;
                default:
                    throw new IllegalArgumentException("Unknown URI " + uri);
            }
            getContext().getContentResolver().notifyChange(uri, null);
            return count;
        }
    
        /**
         * 本示例中Content Provider使用SQLite數據庫存儲圖書數據。
         * */
        private static class DatabaseHelper extends SQLiteOpenHelper {
            DatabaseHelper(Context context) {
                super(context, DATABASE_NAME, null, DATABASE_VERSION);
            }
    
            @Override
            public void onCreate(SQLiteDatabase db) {
                db.execSQL(DATABASE_CREATE);
            }
    
            @Override
            public void onUpgrade(SQLiteDatabase db, int oldVersion,
                                  int newVersion) {
                Log.w("Provider database", "Upgrading database from version " +
                        oldVersion + " to " + newVersion + ", which will destroy all old data");
                db.execSQL("DROP TABLE IF EXISTS titles");
                onCreate(db);
            }
        }
    }

    使用自定義的Content Provider

      Main3Activity

    public class Main3Activity extends AppCompatActivity {
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main3);
        }
    
        public void onClickAddTitle(View view) {
            //---添加一本書---
            ContentValues values = new ContentValues();
            values.put(BooksProvider.TITLE, ((EditText) findViewById(R.id.txtTitle)).getText().toString());
            values.put(BooksProvider.ISBN, ((EditText) findViewById(R.id.txtISBN)).getText().toString());
            Uri uri = getContentResolver().insert(BooksProvider.CONTENT_URI, values);
            assert uri != null;
            Toast.makeText(getBaseContext(), uri.toString(), Toast.LENGTH_LONG).show();
        }
    
        public void onClickRetrieveTitles(View view) {
            //---檢索書名---
            Uri allTitles = Uri.parse("content://link_work.myapplication.provider.Books/books");
            CursorLoader cursorLoader = new CursorLoader(
                    this,
                    allTitles,
                    null,
                    null,
                    null,
                    // 默認以title列從大到小排序
                    "title desc"
            );
            Cursor c = cursorLoader.loadInBackground();
            if (c.moveToFirst()) {
                do {
                    String string = c.getString(c.getColumnIndex(BooksProvider.ID))
                            + ", " + c.getString(c.getColumnIndex(BooksProvider.TITLE)) + ", " +
                            c.getString(c.getColumnIndex(BooksProvider.ISBN));
                    Toast.makeText(this, string, Toast.LENGTH_SHORT).show();
                } while (c.moveToNext());
            }
        }
    }

      activity_main3.xml

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout 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/activity_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".Main3Activity">
    
        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="ISBN"
            app:layout_constraintLeft_toLeftOf="@+id/activity_main"
            app:layout_constraintRight_toRightOf="@+id/activity_main"
            app:layout_constraintTop_toTopOf="@+id/activity_main"
            tools:layout_constraintLeft_creator="1"
            tools:layout_constraintRight_creator="1" />
    
        <EditText
            android:id="@+id/txtISBN"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="8dp"
            android:layout_marginTop="8dp"
            android:ems="10"
            android:inputType="text"
            app:layout_constraintBottom_toTopOf="@+id/textView2"
            app:layout_constraintHorizontal_bias="0.46"
            app:layout_constraintLeft_toLeftOf="@+id/activity_main"
            app:layout_constraintRight_toRightOf="@+id/activity_main"
            app:layout_constraintTop_toBottomOf="@+id/textView"
            app:layout_constraintVertical_bias="0.100000024"
            tools:layout_constraintLeft_creator="1"
            tools:layout_constraintRight_creator="1" />
    
        <TextView
            android:id="@+id/textView2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="142dp"
            android:text="Title"
            app:layout_constraintLeft_toLeftOf="@+id/activity_main"
            app:layout_constraintRight_toRightOf="@+id/activity_main"
            app:layout_constraintTop_toTopOf="@+id/activity_main"
            tools:layout_constraintLeft_creator="1"
            tools:layout_constraintRight_creator="1"
            tools:layout_constraintTop_creator="1" />
    
        <EditText
            android:id="@+id/txtTitle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:ems="10"
            android:inputType="text"
            app:layout_constraintLeft_toLeftOf="@+id/activity_main"
            app:layout_constraintRight_toRightOf="@+id/activity_main"
            app:layout_constraintTop_toBottomOf="@+id/textView2"
            tools:layout_constraintLeft_creator="1"
            tools:layout_constraintRight_creator="1" />
    
        <Button
            android:id="@+id/btnAdd"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="112dp"
            android:onClick="onClickAddTitle"
            android:text="添加標題"
            app:layout_constraintLeft_toLeftOf="@+id/activity_main"
            app:layout_constraintRight_toRightOf="@+id/activity_main"
            app:layout_constraintTop_toBottomOf="@+id/txtTitle"
            tools:layout_constraintLeft_creator="1"
            tools:layout_constraintRight_creator="1" />
    
        <Button
            android:id="@+id/btnRetrieve"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="32dp"
            android:onClick="onClickRetrieveTitles"
            android:text="檢索標題"
            app:layout_constraintLeft_toLeftOf="@+id/activity_main"
            app:layout_constraintRight_toRightOf="@+id/activity_main"
            app:layout_constraintTop_toBottomOf="@+id/btnAdd"
            tools:layout_constraintLeft_creator="1"
            tools:layout_constraintRight_creator="1" />
    </android.support.constraint.ConstraintLayout>

    這里寫圖片描述

    附錄

    • 《Beginning Android Programming with Android Studio, 4th Edition》
    版權聲明:本文為Notzuonotdied原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。
    本文鏈接:https://blog.csdn.net/Notzuonotdied/article/details/78882602

    智能推薦

    Android開發(7)數據庫和Content Provider

    問題聚焦: 思想:應用程序數據的共享 對數據庫的訪問僅限于創建它的應用程序,但是事情不是絕對的 Content Provider提供了一個標準的接口,可供其他應用程序訪問和使用其他程序的數據 下面我們由下向上來分析ContentProvider的處理流程: 1 數據庫——SQLite 使用者:ContentProvider 映射:Content Value映射為數據庫中的表...

    安卓Hacking Part 2: Content Provider攻防

    在上一期中,我們討論了攻擊及加固Activity組件的方法,今天我們來看看所謂的”Content Provider Leakage”。 什么是Content Provider? 按照Google為Android設計的安全模型,應用的數據屬于私有數據,故默認情況下,應用無法訪問其他應用的私有數據。但當應用需要與其他應用共享數據時,Content Provider就扮演著應用間...

    Android基礎(七)內容提供器(Content Provider)

    內容提供器的概念   內容提供器(ContentProvider)主要用于在不同的應用程序之間實現數據共享的功能,它提 供了一套完整的機制,允許一個程序訪問另一個程序中的數據,同時還能保證被訪數據的安全性   必備知識點-運行時權限   未保護用戶的安全和隱私,戶不需要在安裝軟件的時候一次性授權所有申請的權限,而是可以在軟件的使用過程中再對某一項權限申請進行授權 &n...

    HTML中常用操作關于:頁面跳轉,空格

    1.頁面跳轉 2.空格的代替符...

    freemarker + ItextRender 根據模板生成PDF文件

    1. 制作模板 2. 獲取模板,并將所獲取的數據加載生成html文件 2. 生成PDF文件 其中由兩個地方需要注意,都是關于獲取文件路徑的問題,由于項目部署的時候是打包成jar包形式,所以在開發過程中時直接安照傳統的獲取方法沒有一點文件,但是當打包后部署,總是出錯。于是參考網上文章,先將文件讀出來到項目的臨時目錄下,然后再按正常方式加載該臨時文件; 還有一個問題至今沒有解決,就是關于生成PDF文件...

    猜你喜歡

    電腦空間不夠了?教你一個小秒招快速清理 Docker 占用的磁盤空間!

    Docker 很占用空間,每當我們運行容器、拉取鏡像、部署應用、構建自己的鏡像時,我們的磁盤空間會被大量占用。 如果你也被這個問題所困擾,咱們就一起看一下 Docker 是如何使用磁盤空間的,以及如何回收。 docker 占用的空間可以通過下面的命令查看: TYPE 列出了docker 使用磁盤的 4 種類型: Images:所有鏡像占用的空間,包括拉取下來的鏡像,和本地構建的。 Con...

    requests實現全自動PPT模板

    http://www.1ppt.com/moban/ 可以免費的下載PPT模板,當然如果要人工一個個下,還是挺麻煩的,我們可以利用requests輕松下載 訪問這個主頁,我們可以看到下面的樣式 點每一個PPT模板的圖片,我們可以進入到詳細的信息頁面,翻到下面,我們可以看到對應的下載地址 點擊這個下載的按鈕,我們便可以下載對應的PPT壓縮包 那我們就開始做吧 首先,查看網頁的源代碼,我們可以看到每一...

    Linux C系統編程-線程互斥鎖(四)

    互斥鎖 互斥鎖也是屬于線程之間處理同步互斥方式,有上鎖/解鎖兩種狀態。 互斥鎖函數接口 1)初始化互斥鎖 pthread_mutex_init() man 3 pthread_mutex_init (找不到的情況下首先 sudo apt-get install glibc-doc sudo apt-get install manpages-posix-dev) 動態初始化 int pthread_...

    統計學習方法 - 樸素貝葉斯

    引入問題:一機器在良好狀態生產合格產品幾率是 90%,在故障狀態生產合格產品幾率是 30%,機器良好的概率是 75%。若一日第一件產品是合格品,那么此日機器良好的概率是多少。 貝葉斯模型 生成模型與判別模型 判別模型,即要判斷這個東西到底是哪一類,也就是要求y,那就用給定的x去預測。 生成模型,是要生成一個模型,那就是誰根據什么生成了模型,誰就是類別y,根據的內容就是x 以上述例子,判斷一個生產出...

    精品国产乱码久久久久久蜜桃不卡