原创-Android Studio篇章2

Android Studio

1 持久化技术

Android系统中主要提供了 3种方式用于简单地实现数据持久化功能,即文件 存储、SharedPreference存储以及数据库存储

1.1 文件存储

文件存储是Android中最基本的一种数据存储方式,它不对存储的内容进行任何的格式化处 理,所有数据都是原封不动地保存到文件当中的,因而它比较适合用于存储一些简单的文本数据 或二进制数据。

1.1.1 将数据存储到文件中

Context类中提供了一个openFileOutput()方法,可以用于将数据存储到指定的文件中。 这个方法接收两个参数,第一个参数是文件名,在文件创建的时候使用的就是这个名称,注意这 里指定的文件名不可以包含路径,因为所有的文件都是默认存储到/data/data/<package name>/files/目录下的。第二个参数是文件的操作模式,主要有两种模式可选,MODE_PRIVATE 和M0DE_APPENDo其中MODE_PRIVATE是默认的操作模式,表示当指定同样文件名的时候, 所写入的内容将会覆盖原文件中的内容,而MODE_APPEND则表示如果该文件已存在,就往文 件里面追加内容,不存在就创建新文件。其实文件的操作模式本来还有另外两种: MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE,这两种模式表示允许其他的应 用程序对我们程徉中的文件进行读写操作,不过由于这两种模式过于危险,很容易引起应用的安 全性漏洞,已在Android 4.2版本中被废弃。
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/edit"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Type something here">

    </EditText>

</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity
package com.yxj.filepersistencetest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.widget.EditText;
import android.widget.Toast;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
// 首先我们在onCreate()方法中获取了 EditText的实例,然后重写了 onDestroyO 方法,这样就可以保证在活动销毁之前一定会调用这个方法。在onDestroy()方法中我们获取 了 EditText中输入的内容,并调用save()方法把输入的内容存储到文件中,文件命名为data
public class MainActivity extends AppCompatActivity {
    
    
    private EditText edit;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        edit = (EditText) findViewById(R.id.edit);
        String inputText = load();
        if (!TextUtils.isEmpty(inputText)) {
    
    
            edit.setText(inputText);
            edit.setSelection(inputText.length());
            Toast.makeText(this,"Restoring succeeded",Toast.LENGTH_SHORT).show();
        }

    }

    @Override
    protected void onDestroy() {
    
    
        super.onDestroy();
        String inputText = edit.getText().toString();
        save(inputText);

    }

    public void save(String inputText){
    
    
        FileOutputStream out = null;
        BufferedWriter writer = null;

        try {
    
    
            out = openFileOutput("data", Context.MODE_PRIVATE);
            writer = new BufferedWriter(new OutputStreamWriter(out));
            writer.write(inputText);
            System.out.println("chenggong");
        }catch (IOException e){
    
    
            e.printStackTrace();
        }finally {
    
    

            try {
    
    
                if (writer != null) {
    
    
                    writer.close();
                }
            }catch (IOException e){
    
    
                e.printStackTrace();
            }
        }
    }

    public String load() {
    
    
        FileInputStream in = null;
        BufferedReader reader = null;
        StringBuilder content = new StringBuilder();

        try {
    
    
            in = openFileInput("data");
            reader = new BufferedReader(new InputStreamReader(in));
            String line = "";
            while ((line = reader.readLine()) != null) {
    
    
                content.append(line);
            }
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }finally {
    
    
            if (reader != null) {
    
    
                try {
    
    
                    reader.close();
                } catch (IOException e) {
    
    
                    e.printStackTrace();
                }
            }
        }
        return content.toString();

    }
}

在这里插入图片描述

1.1.2 从文件中读取数据

似于将数据存储到文件中,Context类中还提供了一个0penFilelnput ()方法,用于从 文件中读取数据。这个方法要比openFileOutputO简单一些,它只接收一个参数,即要读取的 文件名,然后系统会自动到/data/data/vpackage name>/files/目录下去加载这个文件,并返回一个 Fileinputstream对象,得到了这个对象之后再通过Java流的方式就可以将数据读取出来了
修改MainActivity中的代码
MainActivity
package com.yxj.filepersistencetest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.widget.EditText;
import android.widget.Toast;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
// 首先我们在onCreate()方法中获取了 EditText的实例,然后重写了 onDestroyO 方法,这样就可以保证在活动销毁之前一定会调用这个方法。在onDestroy()方法中我们获取 了 EditText中输入的内容,并调用save()方法把输入的内容存储到文件中,文件命名为data
// 在onCreate()方法中调用load()方法来读取文件中存储 的文本内容,如果读到的内容不为null,就调用EditText的setText ()方法将内容填充到EditText 里,并调用setSelection ()方法将输入光标移动到文本的末尾位置以便于继续输入,然后弹出 一句还原成功的提示。注意,上述代码在对字符串进行非空判断的时候使用了 TextUtils.isEmpty()方法,这是 一个非常好用的方法,它可以一次性进行两种空值的判断。当传入的字符串等于null或者等于 空字符串的时候,这个方法都会返回true,从而使得我们不需要先单独判断这两种空值再使用 逻辑运算符连接起来了。现在重新运行一下程序,刚才保存的Content字符串肯定会被填充到EditText中,然后编写 一点其他的内容,比如在EditText中输入Hello,接着按下Back键退出程序,再重新启动程序, 这时刚才输入的内容并不会丢失,而是还原到了EditText中

public class MainActivity extends AppCompatActivity {
    
    
    private EditText edit;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        edit = (EditText) findViewById(R.id.edit);
        String inputText = load();
        if (!TextUtils.isEmpty(inputText)) {
    
    
            edit.setText(inputText);
            edit.setSelection(inputText.length());
            Toast.makeText(this,"Restoring succeeded",Toast.LENGTH_SHORT).show();
        }

    }

    @Override
    protected void onDestroy() {
    
    
        super.onDestroy();
        String inputText = edit.getText().toString();
        save(inputText);

    }

    public void save(String inputText){
    
    
        FileOutputStream out = null;
        BufferedWriter writer = null;

        try {
    
    
            out = openFileOutput("data", Context.MODE_PRIVATE);
            writer = new BufferedWriter(new OutputStreamWriter(out));
            writer.write(inputText);
            System.out.println("chenggong");
        }catch (IOException e){
    
    
            e.printStackTrace();
        }finally {
    
    

            try {
    
    
                if (writer != null) {
    
    
                    writer.close();
                }
            }catch (IOException e){
    
    
                e.printStackTrace();
            }
        }
    }

    public String load() {
    
    
        FileInputStream in = null;
        BufferedReader reader = null;
        StringBuilder content = new StringBuilder();

        try {
    
    
            in = openFileInput("data");
            reader = new BufferedReader(new InputStreamReader(in));
            String line = "";
            while ((line = reader.readLine()) != null) {
    
    
                content.append(line);
            }
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }finally {
    
    
            if (reader != null) {
    
    
                try {
    
    
                    reader.close();
                } catch (IOException e) {
    
    
                    e.printStackTrace();
                }
            }
        }
        return content.toString();

    }
}

在这里插入图片描述

1.2 SharedPreferences 存储

同于文件的存储方式,SharedPreferences是使用键值对的方式来存储数据的。也就是说, 当保存一条数据的时候,需要给这条数据提供一个对应的键,这样在读取数据的时候就可以通过 这个键把相应的值取出来。而且SharedPreferences还支持多种不同的数据类型存储,如果存储的 数据类型是整型,那么读取岀来的数据也是整型的;如果存储的数据是一个字符串,那么读取岀 来的数据仍然是字符串。

1.2.1 将数据存储到 SharedPreferences 中

主要提供了 3种方法用于得到SharedPreferences对象。
1.	Context 类中的 getSharedPreferences()方法
此方法接收两个参数,第一个参数用于指定SharedPreferences文件的名称,如果指定的文件 不存在则会创建一个,SharedPreferences 文件都是存放在/data/data/<package name>/shared_prefs/ 目录下的。第二个参数用于指定操作模式,目前只有MODE_PRIVATE这一种模式可选,它是默 认的操作模式,和直接传入0效果是相同的,表示只有当前的应用程序才可以对这个 SharedPreferences文件进行读写。其他几种操作模式均已被废弃,MODE_WORLD_READABLE和 MODE_WORLD_WRITEABLE这两种模式是在 Android 4.2版本中被废弃的,MODE_MULTI_ PROCESS模式是在Android 6.0版本中被废弃的。
2.	Activity 类中的 getPreferences()方法
这个方法和Context中的getSharedPreferences()方法很相似,不过它只接收一个操作模 式参数,因为使用这个方法时会自动将当前活动的类名作为SharedPreferences的文件名。
3.	PreferenceManager 类中的 getDefaultSharedPreferences()方法
这是一个静态方法,它接收一个Context参数,并自动使用当前应用程序的包名作为前缀 来命名SharedPref&ences文件。得到了 SharedPreferences对象之后,就可以开始向Shared- Preferences文件中存储数据了,主要可以分为3步实现。
(1)	调用 SharedPreferences 对象的 edit ()方法来获取一个 SharedPreferences. Editor 对象。
(2)	向SharedPreferences.Editor对象中添加数据,比如添加一个布尔型数据就使用 putBooleanO方法,添加一个字符串则使用putStringO方法,以此类推。
(3)	调用apply ()方法将添加的数据提交,从而完成数据存储操作。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/save_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Save data">
    </Button>

</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity
package com.yxj.sharedpreferencestest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
// 可以看到,这里首先给按钮注册了一个点击事件,然后在点击事件中通过getShared¬Preferences ()方法指定 SharedPreferences 的文件名为 data,并得到了 SharedPreferences. Editor对象。接着向这个对象中添加了3条不同类型的数据,最后调用apply ()方法进行提交, 从而完成了数据存储的操作。
public class MainActivity extends AppCompatActivity {
    
    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button saveData = (Button) findViewById(R.id.save_data);
        saveData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                SharedPreferences.Editor editor = getSharedPreferences("data", MODE_PRIVATE).edit();
                editor.putString("name", "Tom");
                editor.putInt("age", 28);
                editor.putBoolean("married", false);
                editor.apply();
            }
        });

    }
}

在这里插入图片描述

1.2.2 从 SharedPreferences 中读取数据

从SharedPreferences文件中读取数据会更加地简单。SharedPreferences对象中 提供了一系列的get方法,用于对存储的数据进行读取,每种get方法都对应了 Shared¬Preferences .Editor中的一种put方法,比如读取一个布尔型数据就使用getBoolean ()方法, 读取一个字符串就使用getString()方法。这些get方法都接收两个参数,第一个参数是键, 传入存储数据时使用的键就可以得到相应的值了;第二个参数是默认值,即表示当传入的键找不 到对应的值时会以什么样的默认值进行返回。
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/save_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Save data">
    </Button>

    <Button
        android:id="@+id/restore_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Restore data">

    </Button>

</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity
package com.yxj.sharedpreferencestest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {
    
    
    private static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button saveData = (Button) findViewById(R.id.save_data);
        Button restoreData = (Button) findViewById(R.id.restore_data);
        saveData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                SharedPreferences.Editor editor = getSharedPreferences("data", MODE_PRIVATE).edit();
                editor.putString("name", "Tom");
                editor.putInt("age", 28);
                editor.putBoolean("married", false);
                editor.apply();
            }
        });
        restoreData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                SharedPreferences pref = getSharedPreferences("data", MODE_PRIVATE);
                String name = pref.getString("name", "");
                int age = pref.getInt("age", 0);
                boolean married = pref.getBoolean("married", false);
                Log.d(TAG, "name is " + name);
                Log.d(TAG, "age is " + age);
                Log.d(TAG, "married is " + married);

            }
        });

    }
}

在这里插入图片描述

1.2.3 实现记住密码功能

activity_login.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="60dp">
        <TextView
            android:layout_width="90dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:textSize="18sp"
            android:text="Account: ">

        </TextView>

        <EditText
            android:id="@+id/account"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_gravity="center_vertical">

        </EditText>
    </LinearLayout>
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="60dp">

        <TextView
            android:layout_width="90dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:textSize="18sp"
            android:text="Password: ">

        </TextView>

        <EditText
            android:id="@+id/password"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_gravity="center_vertical"
            android:inputType="textPassword">

        </EditText>

    </LinearLayout>

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <CheckBox
            android:id="@+id/remember_pass"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">

        </CheckBox>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="18sp"
            android:text="Remember password">

        </TextView>
    </LinearLayout>
    <Button
        android:id="@+id/login"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:text="Login">

    </Button>
</LinearLayout>

LoginActivity
package com.yxj.broadcastbestpractice;


import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;
// 可以看到,这里首先在onCreateO方法中获取到了 SharedPreferences对象,然后调用 它的getBoolean()方法去获取remember password这个键对应的值。一开始当然不存在对应 的值了,所以会使用默认值false,这样就什么都不会发生。接着在登录成功之后,会调用 CheckBox的isChecked()方法来检查复选框是否被选中,如果被选中了,则表示用户想要记住 密码,这时将remember password设置为true,然后把account和password对应的值都存 入到SharedPreferences文件当中并提交。如果没有被选中,就简单地调用一下clear()方法,将 SharedPreferences文件中的数据全部清除掉。当用户选中了记住密码复选框,并成功登录一次之后,remember_password键对应的值就 是true 了,这个时候如果再重新启动登录界面,就会从SharedPreferences文件中将保存的账号 和密码都读取出来,并填充到文本输入框中,然后把记住密码复选框选中,这样就完成记住密码 的功能了。

public class LoginActivity extends BaseActivity {
    
    
    private SharedPreferences pref;
    private SharedPreferences.Editor editor;
    private EditText accountEdit;
    private EditText passwordEdit;
    private Button login;
    private CheckBox rememberPass;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        pref = PreferenceManager.getDefaultSharedPreferences(this);
        accountEdit = (EditText)findViewById(R.id.account);
        passwordEdit = (EditText)findViewById(R.id.password);
        rememberPass = (CheckBox) findViewById(R.id.remember_pass);
        login = (Button)findViewById(R.id.login);
        boolean isRemember = pref.getBoolean("remember_password", false);
        if (isRemember) {
    
    
            String account = pref.getString("account", "");
            String password = pref.getString("password", "");
            accountEdit.setText(account);
            passwordEdit.setText(password);
            rememberPass.setChecked(true);
        }
        login.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                String account = accountEdit.getText().toString();
                String password = passwordEdit.getText().toString();

                if (account.equals("admin") && password.equals("123456")){
    
    
                    editor = pref.edit();
                    if (rememberPass.isChecked()){
    
    
                        editor.putBoolean("remember_password",true);
                        editor.putString("account",account);
                        editor.putString("password",password);
                    }else {
    
    
                        editor.clear();
                    }
                    editor.apply();
                    Intent intent = new Intent(LoginActivity.this, MainActivity.class);
                    startActivity(intent);
                    finish();
                }else {
    
    
                    Toast.makeText(LoginActivity.this,"account or password is invalid",Toast.LENGTH_SHORT).show();
                }


            }
        });
    }
}

在这里插入图片描述

1.3 SQLite数据库存储

1.3.1 创建数据库

SQLiteOpenHelper是一个抽象类,这意味着如果我们想要使用它的话,就需 要创建一个自己的帮助类去继承它。SQLiteOpenHelper中有两个抽象方法,分别是onCreateO 和onUpgrade(),我们必须在自己的帮助类里面重写这两个方法,然后分别在这两个方法中去 实现创建、升级数据库的逻辑。
SQLiteOpenHelper中还有两个非常重要的实例方法:getReadableDatabase()和get- WritableDatabase()o这两个方法都可以创建或打开一个现有的数据库(如果数据库已存在则 直接打开,否则创建一个新的数据库),并返回一个可对数据库进行读写操作的对象。不同的是, 当数据库不可写入的时候(如磁盘空间已满),getReadableDatabase()方法返回的对象将以只 读的方式去打开数据库,而getWritableDatabase()方法则将出现异常。
SQLiteOpenHelper中有两个构造方法可供重写,一般使用参数少一点的那个构造方法即可。 这个构造方法中接收4个参数,第一个参数是Context,这个没什么好说的,必须要有它才能对 数据库进行操作。第二个参数是数据库名,创建数据库时使用的就是这里指定的名称。第三个参 数允许我们在查询数据的时候返回一个自定义的Cursor, 一般都是传入null。第四个参数表示 当前数据库的版本号,可用于对数据库进行升级操作。构建出SQLiteOpenHelper的实例之后, 再调用它的getReadableDatabase()或getWritableDatabase()方法就能够创建数据库了, 数据库文件会存放在/data/data/<packagename>/databases/目录下。此时,重写的onCreate()方法 也会得到执行,所以通常会在这里去处理一些创建表的逻辑。

MyDatabaseHelper
package com.yxj.databasetest;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.widget.Toast;

import androidx.annotation.Nullable;

public class MyDatabaseHelper extends SQLiteOpenHelper {
    
    
    public static final String CREATE_BOOK = "create table Book (" +
            "id integer primary key autoincrement," +
            "author text," +
            "price real," +
            "pages integer," +
            "name text)";
    private Context mcontext;
    public MyDatabaseHelper(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
    
    
        super(context, name, factory, version);
        mcontext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
    
    
        db.execSQL(CREATE_BOOK);
        Toast.makeText(mcontext,"Create succeeded",Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    
    
    }
}


MainActivity
package com.yxj.databasetest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {
    
    
    private MyDatabaseHelper dphelper;
    @Override
    // 这里我们在onCreateO方法中构建了一个MyDatabaseHelper对象,并且通过构造函数的 参数将数据库名指定为BookStore.db,版本号指定为1,然后在Create database按钮的点击事件 里调用了 getWritableDatabase()方法。这样当第一次点击Create database按钮时,就会检测到 当前程序中并没有BookStore.db这个数据库,于是会创建该数据库并调用MyDatabaseHelper中的 onCreate()方法,这样Book表也就得到了创建,然后会弹出一个Toast提示创建成功。再次点 击Create database按钮时,会发现此时已经存在BookStore.db数据库了,因此不会再创建一次。
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dphelper = new MyDatabaseHelper(this,"BookStore.db", null,1);
        Button createDatabase = (Button) findViewById(R.id.create_database);
        createDatabase.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                dphelper.getWritableDatabase();
            }
        });


    }
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/create_database"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Create database">

    </Button>

</androidx.constraintlayout.widget.ConstraintLayout>

在这里插入图片描述
在这里插入图片描述

打开数据库
只需要键入sqlite3,后面加上数据库名 即可
查表
键入.table命令

在这里插入图片描述

查看建表语句
.schema

在这里插入图片描述

1.3.2 升级数据库

MyDatabaseHelper
package com.yxj.databasetest;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.widget.Toast;

import androidx.annotation.Nullable;

public class MyDatabaseHelper extends SQLiteOpenHelper {
    
    
    public static final String CREATE_BOOK = "create table Book (" +
            "id integer primary key autoincrement," +
            "author text," +
            "price real," +
            "pages integer," +
            "name text)";

    public static final String CREATE_CATEGORY = "create table Category(" +
            "id integer primary key autoincrement," +
            "category_name text," +
            "category_code integer)";
    private Context mcontext;
    public MyDatabaseHelper(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
    
    
        super(context, name, factory, version);
        mcontext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
    
    
        db.execSQL(CREATE_BOOK);
        db.execSQL(CREATE_CATEGORY);
        Toast.makeText(mcontext,"Create succeeded",Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    
    
        db.execSQL("drop table if exists Book");
        db.execSQL("drop table if exists Category");
        onCreate(db);
    }
}

MainActivity
package com.yxj.databasetest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {
    
    
    private MyDatabaseHelper dphelper;
    @Override
    // 我们传入的是1,现在只要传入一 个比1大的数,就可以让onllpgrade()方法得到执行了
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dphelper = new MyDatabaseHelper(this,"BookStore.db", null,4);
        Button createDatabase = (Button) findViewById(R.id.create_database);
        createDatabase.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                dphelper.getWritableDatabase();
            }
        });


    }
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/create_database"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Create database">

    </Button>

</androidx.constraintlayout.widget.ConstraintLayout>

在这里插入图片描述

1.3.3 添加数据

insert ()方法,这个方法就是专门用于添加数据的。它接收3个参数,第一个参数是表名, 我们希望向哪张表里添加数据,这里就传入该表的名字。第二个参数用于在未指定添加数据的情 况下给某些可为空的列自动赋值NULL, 一般我们用不到这个功能,直接传入null即可。第三个 参数是一个ContentValues对象,它提供了一系列的put()方法重载,用于向ContentValues 中添加数据,只需要将表中的每个列名以及相应的待添加数据传入即可。
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <Button
            android:id="@+id/create_database"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Create database">

        </Button>

        <Button
            android:id="@+id/add_data"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Add data">

        </Button>
    </LinearLayout>


</androidx.constraintlayout.widget.ConstraintLayout>
MyDatabaseHelper
package com.yxj.databasetest;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.widget.Toast;

import androidx.annotation.Nullable;

public class MyDatabaseHelper extends SQLiteOpenHelper {
    
    
    public static final String CREATE_BOOK = "create table Book (" +
            "id integer primary key autoincrement," +
            "author text," +
            "price real," +
            "pages integer," +
            "name text)";

    public static final String CREATE_CATEGORY = "create table Category(" +
            "id integer primary key autoincrement," +
            "category_name text," +
            "category_code integer)";
    private Context mcontext;
    public MyDatabaseHelper(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
    
    
        super(context, name, factory, version);
        mcontext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
    
    
        db.execSQL(CREATE_BOOK);
        db.execSQL(CREATE_CATEGORY);
        Toast.makeText(mcontext,"Create succeeded",Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    
    
        db.execSQL("drop table if exists Book");
        db.execSQL("drop table if exists Category");
        onCreate(db);
    }
}

MainActivity

package com.yxj.databasetest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.ContentValues;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {
    
    
    private MyDatabaseHelper dphelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dphelper = new MyDatabaseHelper(this,"BookStore.db", null,2);
        Button createDatabase = (Button) findViewById(R.id.create_database);
        Button addData = (Button) findViewById(R.id.add_data);
        createDatabase.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                dphelper.getWritableDatabase();
            }
        });

        addData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                SQLiteDatabase db = dphelper.getWritableDatabase();
                // insert ()方法,这个方法就是专门用于添加数据的。它接收3个参数,第一个参数是表名, 我们希望向哪张表里添加数据,这里就传入该表的名字。第二个参数用于在未指定添加数据的情 况下给某些可为空的列自动赋值NULL, 一般我们用不到这个功能,直接传入null即可。第三个 参数是一个ContentValues对象,它提供了一系列的put()方法重载,用于向ContentValues 中添加数据,只需要将表中的每个列名以及相应的待添加数据传入即可。
                ContentValues values = new ContentValues();
                values.put("name", "The Da Vinci Code");
                values.put("author", "Dan Brown");
                values.put("pages", 454);
                values.put("price",3.96);
                db.insert("Book",null,values);
                values.clear();
                values.put("name", "The Lost Symble");
                values.put("author","Dan Brown");
                values.put("pages",510);
                values.put("price",19.95);
                db.insert("Book",null,values);


            }
        });

    }
}

在这里插入图片描述

1.3.4 更新数据

updateO方法,用于对数据进行更新,这个方法接收4 个参数,第一个参数和insert()方法一样,也是表名,在这里指定去更新哪张表里的数据。第 二个参数是Contentvalues对象,要把更新数据在这里组装进去。第三、第四个参数用于约束 更新某一行或某几行中的数据,不指定的话默认就是更新所有行。
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <Button
            android:id="@+id/create_database"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Create database">

        </Button>

        <Button
            android:id="@+id/add_data"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Add data">

        </Button>

        <Button
            android:id="@+id/update_data"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Update data">

        </Button>
    </LinearLayout>


</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity
package com.yxj.databasetest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.ContentValues;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {
    
    
    private MyDatabaseHelper dbhelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dbhelper = new MyDatabaseHelper(this,"BookStore.db", null,2);
        Button createDatabase = (Button) findViewById(R.id.create_database);
        Button addData = (Button) findViewById(R.id.add_data);
        Button updataData = (Button) findViewById(R.id.update_data);
        createDatabase.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                dbhelper.getWritableDatabase();
            }
        });

        addData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                SQLiteDatabase db = dbhelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                values.put("name", "The Da Vinci Code");
                values.put("author", "Dan Brown");
                values.put("pages", 454);
                values.put("price",3.96);
                db.insert("Book",null,values);
                values.clear();
                values.put("name", "The Lost Symble");
                values.put("author","Dan Brown");
                values.put("pages",510);
                values.put("price",19.95);
                db.insert("Book",null,values);


            }
        });
        updataData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                SQLiteDatabase db = dbhelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                values.put("price", 10.99);
                db.update("Book",values,"name=?",new String[] {
    
    "The Da Vinci Code"});

            }
        });
    }
}

在这里插入图片描述

1.3.5 删除数据

SQLiteDatabase中提供了一个delete()方法,专门用于删除数据,这个方法接收3个参数, 第一个参数仍然是表名,这个已经没什么好说的了,第二、第三个参数又是用于约束删除某一行 或某几行的数据,不指定的话默认就是删除所有行。
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <Button
            android:id="@+id/create_database"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Create database">

        </Button>

        <Button
            android:id="@+id/add_data"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Add data">

        </Button>

        <Button
            android:id="@+id/update_data"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Update data">

        </Button>

        <Button
            android:id="@+id/delete_data"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Delete data">

        </Button>
    </LinearLayout>


</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity
package com.yxj.databasetest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.ContentValues;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {
    
    
    private MyDatabaseHelper dbhelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dbhelper = new MyDatabaseHelper(this,"BookStore.db", null,2);
        Button createDatabase = (Button) findViewById(R.id.create_database);
        Button addData = (Button) findViewById(R.id.add_data);
        Button updataData = (Button) findViewById(R.id.update_data);
        Button deleteButton = (Button) findViewById(R.id.delete_data);
        createDatabase.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                dbhelper.getWritableDatabase();
            }
        });

        addData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                SQLiteDatabase db = dbhelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                values.put("name", "The Da Vinci Code");
                values.put("author", "Dan Brown");
                values.put("pages", 454);
                values.put("price",3.96);
                db.insert("Book",null,values);
                values.clear();
                values.put("name", "The Lost Symble");
                values.put("author","Dan Brown");
                values.put("pages",510);
                values.put("price",19.95);
                db.insert("Book",null,values);


            }
        });
        updataData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                SQLiteDatabase db = dbhelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                values.put("price", 10.99);
                db.update("Book",values,"name=?",new String[] {
    
    "The Da Vinci Code"});

            }
        });
        deleteButton.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                SQLiteDatabase db = dbhelper.getWritableDatabase();
                db.delete("Book","pages > ?",new String[]{
    
    "500"});
            }
        });
    }
}

1.3.6 查询数据

第一个参数不用说,当然还是表名,表示我们希望从哪张表中查询数据。第 二个参数用于指定去查询哪几列,如果不指定则默认查询所有列。第三、第四个参数用于约束查 询某一行或某几行的数据,不指定则默认查询所有行的数据。第五个参数用于指定需要去group by 的列,不指定则表示不对查询结果进行group by操作。第六个参数用于对group by之后的数据进 行进一步的过滤,不指定则表示不进行过滤。第七个参数用于指定查询结果的排序方式,不指定 则表示使用默认的排序方式。

在这里插入图片描述

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <Button
            android:id="@+id/create_database"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Create database">

        </Button>

        <Button
            android:id="@+id/add_data"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Add data">

        </Button>

        <Button
            android:id="@+id/update_data"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Update data">

        </Button>

        <Button
            android:id="@+id/delete_data"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Delete data">

        </Button>

        <Button
            android:id="@+id/query_data"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Query data">

        </Button>


    </LinearLayout>


</androidx.constraintlayout.widget.ConstraintLayout>
MyDatabaseHelper
package com.yxj.databasetest;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.widget.Toast;

import androidx.annotation.Nullable;

public class MyDatabaseHelper extends SQLiteOpenHelper {
    
    
    public static final String CREATE_BOOK = "create table Book (" +
            "id integer primary key autoincrement," +
            "author text," +
            "price real," +
            "pages integer," +
            "name text)";

    public static final String CREATE_CATEGORY = "create table Category(" +
            "id integer primary key autoincrement," +
            "category_name text," +
            "category_code integer)";
    private Context mcontext;
    public MyDatabaseHelper(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
    
    
        super(context, name, factory, version);
        mcontext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
    
    
        db.execSQL(CREATE_BOOK);
        db.execSQL(CREATE_CATEGORY);
        Toast.makeText(mcontext,"Create succeeded",Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    
    
        db.execSQL("drop table if exists Book");
        db.execSQL("drop table if exists Category");
        onCreate(db);
    }
}

MainActivity
package com.yxj.databasetest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {
    
    
    private MyDatabaseHelper dbhelper;
    private static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dbhelper = new MyDatabaseHelper(this,"BookStore.db", null,2);
        Button createDatabase = (Button) findViewById(R.id.create_database);
        Button addData = (Button) findViewById(R.id.add_data);
        Button updataData = (Button) findViewById(R.id.update_data);
        Button deleteButton = (Button) findViewById(R.id.delete_data);
        Button queryButton = (Button) findViewById(R.id.query_data);
        createDatabase.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                dbhelper.getWritableDatabase();
            }
        });

        addData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                SQLiteDatabase db = dbhelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                values.put("name", "The Da Vinci Code");
                values.put("author", "Dan Brown");
                values.put("pages", 454);
                values.put("price",3.96);
                db.insert("Book",null,values);
                values.clear();
                values.put("name", "The Lost Symble");
                values.put("author","Dan Brown");
                values.put("pages",510);
                values.put("price",19.95);
                db.insert("Book",null,values);


            }
        });
        updataData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                SQLiteDatabase db = dbhelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                values.put("price", 10.99);
                db.update("Book",values,"name=?",new String[] {
    
    "The Da Vinci Code"});

            }
        });
        deleteButton.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                SQLiteDatabase db = dbhelper.getWritableDatabase();
                db.delete("Book","pages > ?",new String[]{
    
    "500"});
            }
        });
        queryButton.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                SQLiteDatabase db = dbhelper.getWritableDatabase();
                // 第一个参数不用说,当然还是表名,表示我们希望从哪张表中查询数据。第二个参数用于指定去查询哪几列,如果不指定则默认查询所有列。第三、第四个参数用于约束查询某一行或某几行的数据,不指定则默认查询所有行的数据。第五个参数用于指定需要去group by 的列,不指定则表示不对查询结果进行group by操作。第六个参数用于对group by之后的数据进 行进一步的过滤,不指定则表示不进行过滤。第七个参数用于指定查询结果的排序方式,不指定则表示使用默认的排序方式。
                Cursor cursor = db.query("Book", null, null, null, null, null, null);
                if (cursor.moveToFirst()){
    
    
                    do {
    
    
                        String name = cursor.getString(cursor.getColumnIndex("name"));
                        String author = cursor.getString(cursor.getColumnIndex("author"));
                        int pages = cursor.getInt(cursor.getColumnIndex("pages"));
                        double price = cursor.getDouble(cursor.getColumnIndex("price"));
                        Log.d(TAG, "book name is " + name);
                        Log.d(TAG, "book author is " + author);
                        Log.d(TAG, "book pages is " + pages);
                        Log.d(TAG, "book price is " + price);
                    }while (cursor.moveToNext());
                   cursor.close();

                }

            }
        });
    }
}

在这里插入图片描述

1.3.7 使用SQL操作数据库

添加数据的方法如下:
db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)", new String[] { "The Da Vinci Code", "Dan Brown", ”454”, "3.96" });
db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)", new String[] { "The Lost Symbol", "Dan Brown", ”510”, "19.95" });
更新数据的方法如下:
db.execSQL("update Book set price = ? where name = ?", new St ring[] { "10.99", "The Da Vinci Code" });
删除数据的方法如下:
db.execSQL("delete from Book where pages > ?", new St ring[] { "500" ));
查询数据的方法如下:
db.rawQuery("select * from Book", null);
可以看到,除了查询数据的时候调用的是SQLiteDatabase的rawQuery()方法,其他的操作 都是调用的execSQL()方法。以上演示的几种方式,执行结果会和前面几小节中我们学习的 CRUD操作的结果完全相同,选择使用哪一种方式就看你个人的喜好了。

1.4 使用LitePal操作数据库

1.4.1 配置 LitePal

apply plugin: 'com.android.application'

android {
    
    
    compileSdkVersion 30
    buildToolsVersion "30.0.2"

    defaultConfig {
    
    
        applicationId "com.yxj.litepaltest"
        minSdkVersion 14
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
    
    
        release {
    
    
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    
    
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    implementation 'org.litepal.android:core:1.4.1'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

}
这样我们就把LitePal成功引入到当前项目中了,接下来需要配置litepaLxml文件。右击 app/src/main目录—>New—^Directory,创建一个assets目录,然后在assets目录下再新建一个 litepal.xml文件,接着编辑litepal.xml文件中的内容,
litepal.xml
<?xml version="1.0" encoding="utf-8"?>
<litepal>
    <dbname value="BookStore"></dbname>
    <version value="1"></version>

    <list>
        <mapping class="com.yxj.litepaltest.Book"> </mapping>
    </list>
</litepal>
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.yxj.litepaltest">
<!--这里我们将项目的application配置为org.litepal.LitePalApplication,这样才能让LitePal的所有功能都可以正常工作。 -->
    <application
        android:name="org.litepal.LitePalApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <Button
            android:id="@+id/create_database"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Create database">

        </Button>

        <Button
            android:id="@+id/add_data"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Add data">

        </Button>

        <Button
            android:id="@+id/update_data"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Update data">

        </Button>

        <Button
            android:id="@+id/delete_data"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Delete data">

        </Button>

        <Button
            android:id="@+id/query_data"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Query data">

        </Button>


    </LinearLayout>


</androidx.constraintlayout.widget.ConstraintLayout>

1.4.2 创建和升级数据库

1.4.2.1 创建数据库
Book
package com.yxj.litepaltest;

import java.security.Principal;

public class Book {
    
    

    private int id;

    private  String  author;

    private  double price;

    private int pages;

    private  String name;

    public int getId() {
    
    
        return id;
    }

    public void setId(int id) {
    
    
        this.id = id;
    }

    public String getAuthor() {
    
    
        return author;
    }

    public void setAuthor(String author) {
    
    
        this.author = author;
    }

    public double getPrice() {
    
    
        return price;
    }

    public void setPrice(double price) {
    
    
        this.price = price;
    }

    public int getPages() {
    
    
        return pages;
    }

    public void setPages(int pages) {
    
    
        this.pages = pages;
    }

    public String getName() {
    
    
        return name;
    }

    public void setName(String name) {
    
    
        this.name = name;
    }
}

修改litepal.xml中的代码
<?xml version="1.0" encoding="utf-8"?>
<litepal>
    <dbname value="BookStore"></dbname>
    <version value="1"></version>
<!-- 使用<mapping>标签来声明我们要配置的映射模型类,注意一定要使用完整的类名。不 管有多少模型类需要映射,都使用同样的方式配置在<list>标签下即可-->
    <list>
        <mapping class="com.yxj.litepaltest.Book"></mapping>
    </list>
</litepal>
MainActivity
package com.yxj.litepaltest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import org.litepal.LitePal;

public class MainActivity extends AppCompatActivity {
    
    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button createDatabase = (Button) findViewById(R.id.create_database);
        createDatabase.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                LitePal.getDatabase();
            }
        });
    }
}

在这里插入图片描述

可以看到,这里有3张表的建表语句,其中android metadata表仍然不用管,table_ schema表是LitePal内部使用的,我们也可以直接忽视,book表就是根据我们定义的Book类以 及类中的字段来自动生成的了。

我们体验 了使用SQLiteOpenHelper来升级数据库的方式,虽说功能是实现了,但你有没有发现一个问题, 就是升级数据库的时候我们需要先把之前的表drop掉,然后再重新创建才行。这其实是一个非 常严重的问题,因为这样会造成数据丢失,每当升级一次数据库,之前表中的数据就全没了。
。而有了 LitePal,这些就都不再是问题了,使用LitePal来升级数据库非常非常简单, 你完全不用思考任何的逻辑,只需要改你想改的任何内容,然后将版本号加1就行了。
1.4.2.2 升级数据库
比如我们想要向Book表中添加一个press (出版社)列,直接修改Book类中的代码,添加 一个press字段即可

与此同时,我们还想再添加一张Category表,那么只需要新建一个Category类就可以了
Book

package com.yxj.litepaltest;

import java.security.Principal;

public class Book {
    
    

    private int id;

    private  String  author;

    private  double price;

    private int pages;

    private  String name;

    private String press;

    public String getPress() {
    
    
        return press;
    }

    public void setPress(String press) {
    
    
        this.press = press;
    }

    public int getId() {
    
    
        return id;
    }

    public void setId(int id) {
    
    
        this.id = id;
    }

    public String getAuthor() {
    
    
        return author;
    }

    public void setAuthor(String author) {
    
    
        this.author = author;
    }

    public double getPrice() {
    
    
        return price;
    }

    public void setPrice(double price) {
    
    
        this.price = price;
    }

    public int getPages() {
    
    
        return pages;
    }

    public void setPages(int pages) {
    
    
        this.pages = pages;
    }

    public String getName() {
    
    
        return name;
    }

    public void setName(String name) {
    
    
        this.name = name;
    }
}

Category
package com.yxj.litepaltest;

public class Category {
    
    
    private int id;
    private String catogoryName;
    private int categoryCode;

    public int getId() {
    
    
        return id;
    }

    public void setId(int id) {
    
    
        this.id = id;
    }

    public String getCatogoryName() {
    
    
        return catogoryName;
    }

    public void setCatogoryName(String catogoryName) {
    
    
        this.catogoryName = catogoryName;
    }

    public int getCategoryCode() {
    
    
        return categoryCode;
    }

    public void setCategoryCode(int categoryCode) {
    
    
        this.categoryCode = categoryCode;
    }
}

改完了所有我们想改的东西,只需要记得将版本号加1就行了。当然由于这里还添加了一个 新的模型类,因此也需要将它添加到映射模型列表中。修改litepaLxml中的代码
litepal.xml
<?xml version="1.0" encoding="utf-8"?>
<litepal>
<dbname value="BookStore"></dbname>
<version value="2"></version>

<list>
    <mapping class="com.yxj.litepaltest.Book"></mapping>
    <mapping class="com.yxj.litepaltest.Category"></mapping>
</list>

</litepal>

在这里插入图片描述

可以看到,book表中新增了一个press列,category表也创建成功了,当然LitePal还自动帮 我们做了一项非常重要的工作,就是保留之前表中的所有数据,这样就再也不用担心数据丢失的问题了。

1.4.3 使用LitePal添加数据

观察创建和升级数据库的模型类,你会发现它们都是没有继承结构的。没错,因为 LitePal进行表管理操作时不需要模型类有任何的继承结构,但是进行CRUD操作时就不行了, 必须要继承自DataSupport类才行,因此这里我们需要先把继承结构给加上。修改Book类中的 代码,
Book
package com.yxj.litepaltest;

import org.litepal.crud.DataSupport;


public class Book extends DataSupport {
    
    

    private int id;

    private  String  author;

    private  double price;

    private int pages;

    private  String name;

    private String press;

    public String getPress() {
    
    
        return press;
    }

    public void setPress(String press) {
    
    
        this.press = press;
    }

    public int getId() {
    
    
        return id;
    }

    public void setId(int id) {
    
    
        this.id = id;
    }

    public String getAuthor() {
    
    
        return author;
    }

    public void setAuthor(String author) {
    
    
        this.author = author;
    }

    public double getPrice() {
    
    
        return price;
    }

    public void setPrice(double price) {
    
    
        this.price = price;
    }

    public int getPages() {
    
    
        return pages;
    }

    public void setPages(int pages) {
    
    
        this.pages = pages;
    }

    public String getName() {
    
    
        return name;
    }

    public void setName(String name) {
    
    
        this.name = name;
    }
}

MainActivity
package com.yxj.litepaltest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import org.litepal.LitePal;

public class MainActivity extends AppCompatActivity {
    
    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button createDatabase = (Button) findViewById(R.id.create_database);
        Button addData = (Button) findViewById(R.id.add_data);
        createDatabase.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                LitePal.getDatabase();
            }
        });
        addData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
            // 首先是创建出 了一个Book的实例,然后调用Book类中的各种set方法对数据进行设置,最后再调用 book.saveO方法就能完成数据添加操作了。
                Book book = new Book();
                book.setName("The Da Vin Code");
                book.setAuthor("Dan Brown");
                book.setPages(454);
                book.setPrice(3.69);
                book.setPress("Unknow");
                book.save();

            }
        });
    }
}

在这里插入图片描述

1.4.4 使用LitePal更新数据

首先,最简单的一种更新方式就是对已存储的对象重新设值,然后重新调用save()方法即 可。
对于LitePal来说,对象是否已存储就是根据调用model.isSavedO方法的结果来判断的, 返回true就表示已存储,返回false就表示未存储。那么接下来的问题就是,什么情况下会返 回true,什么情况下会返回false呢?
实际上只有在两种情况下model.isSavedO方法才会返回true, 一种情况是已经调用过 model. save()方法去添加数据了,此时model会被认为是已存储的对象。另一种情况是model 对象是通过LitePal提供的查询API查岀来的,由于是从数据库中查到的对象,因此也会被认为 是已存储的对象。
由于查询API我们暂时还没学到,因此只能先通过第一种情况来进行验证。修改MainActivity 中的代码

MainActivity
package com.yxj.litepaltest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import org.litepal.LitePal;

public class MainActivity extends AppCompatActivity {
    
    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button createDatabase = (Button) findViewById(R.id.create_database);
        Button addData = (Button) findViewById(R.id.add_data);
        Button updataData = (Button) findViewById(R.id.update_data);
        createDatabase.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                LitePal.getDatabase();
            }
        });
        addData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                Book book = new Book();
                book.setName("The Da Vin Code");
                book.setAuthor("Dan Brown");
                book.setPages(454);
                book.setPrice(3.69);
                book.setPress("Unknow");
                book.save();

            }
        });
        updataData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                Book book = new Book();
                book.setName("The Lost Symbol");
                book.setAuthor("Dan Brown");
                book.setPages(510);
                book.setPrice(19.95);
                book.setPress("Unknow");
                book.save();
                book.setPrice(10.99);
                book.save();
            }
        });
    }
}
在更新数据按钮的点击事件里面,我们先是通过上一小节中学习的知识添加了一条Book数 据,然后调用setPriceO方法将这本书的价格进行了修改,之后再次调用了 save()方法。此时 LitePal会发现当前的Book对象是已存储的,因此不会再向数据库中去添加一条新数据,而是会 直接更新当前的数据。

在这里插入图片描述

可以看到,Book表中新增了一条书的数据,但这本书的价格并不是一开始设置的19.95,而 是10.99,说明我们的更新操作确实生效了。
但是这种更新方式只能对已存储的对象进行操作,限制性比较大,接下来我们学习另外一种 更加灵巧的更新方式。修改MainActivity中的代码
MainActivity
package com.yxj.litepaltest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import org.litepal.LitePal;

public class MainActivity extends AppCompatActivity {
    
    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button createDatabase = (Button) findViewById(R.id.create_database);
        Button addData = (Button) findViewById(R.id.add_data);
        Button updataData = (Button) findViewById(R.id.update_data);
        createDatabase.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                LitePal.getDatabase();
            }
        });
        addData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                Book book = new Book();
                book.setName("The Da Vin Code");
                book.setAuthor("Dan Brown");
                book.setPages(454);
                book.setPrice(3.69);
                book.setPress("Unknow");
                book.save();

            }
        });
        updataData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                Book book = new Book();
                book.setPrice(1.95);
                book.setPress("Anchor");
                book.updateAll("name = ? and author = ?","The Lost Symbol","Dan Brown");
            }
        });
    }
}
可以看到,这里我们首先new出了一个Book的实例,然后直接调用setPrice()和 setPress()方法来设置要更新的数据,最后再调用updateAll()方法去执行更新操作。注意 updateAll()方法中可以指定一个条件约束,和SQLiteDatabase中update()方法的where参数 部分有点类似,但更加简洁,如果不指定条件语句的话,就表示更新所有数据。这里我们指定将 所有书名是The Lost Symbol并且作者是Dan Brown的书价格更新为1.95,出版社更新为Anchor。

在这里插入图片描述

不过,在使用updateAll()方法时,还有一个非常重要的知识点是你需要知晓的,就是当你想把一个字段的值更新成默认值时,是不可以使用上面的方式来set数据的。我们都知道,在 Java中任何一种数据类型的字段都会有默认值,例如int类型的默认值是0, boolean类型的默 认值是false, String类型的默认值是null。那么当new岀一个Book对象时,其实所有字段 都已经被初识化成默认值了,比如说pages字段的值就是0。因此,如果我们想把数据库表中的 pages列更新成0,直接调用book.setPages(0)是不可以的,因为即使不调用这行代码,pages 字段本身也是0, LitePal此时是不会对这个列进行更新的。对于所有想要将为数据更新成默认值 的操作,LitePal统一提供了一个setToDefault ()方法,然后传入相应的列名就可以了实现了。 比如我们可以这样写:
Book book = new Book(); book.setToDefault("pages"); book.updateAll();
这段代码的意思是,将所有书的页数都更新为0,因为updateAll()方法中没有指定约束条 件,因此更新操作对所有数据都生效了。

MainActivity
package com.yxj.litepaltest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import org.litepal.LitePal;

public class MainActivity extends AppCompatActivity {
    
    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button createDatabase = (Button) findViewById(R.id.create_database);
        Button addData = (Button) findViewById(R.id.add_data);
        Button updataData = (Button) findViewById(R.id.update_data);
        createDatabase.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                LitePal.getDatabase();
            }
        });
        addData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                Book book = new Book();
                book.setName("The Da Vin Code");
                book.setAuthor("Dan Brown");
                book.setPages(454);
                book.setPrice(3.69);
                book.setPress("Unknow");
                book.save();

            }
        });
        updataData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                Book book = new Book();
                book.setToDefault("pages");
                book.updateAll("name = ? and author = ?","The Lost Symbol","Dan Brown");
            }
        });
    }
}
没有使用setToDefault()

在这里插入图片描述

使用setToDefault()

在这里插入图片描述

1.4.5 使用LitePal删除数据

使用LitePal删除数据的方式主要有两种,第一种比较简单,就是直接调用已存储对象的 delete()方法就可以了,对于已存储对象的概念,我们在上一小节中已经学习过了。也就是说, 调用过save()方法的对象,或者是通过LitePal提供的查询API查出来的对象,都是可以直接使 用delete()方法来删除数据的。这种方式比较简单,我们就不进行代码演示了,下面直接来看 另外一种删除数据的方式。
MainActivity
package com.yxj.litepaltest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import org.litepal.LitePal;
import org.litepal.crud.DataSupport;

public class MainActivity extends AppCompatActivity {
    
    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button createDatabase = (Button) findViewById(R.id.create_database);
        Button addData = (Button) findViewById(R.id.add_data);
        Button updataData = (Button) findViewById(R.id.update_data);
        Button deleteData = (Button) findViewById(R.id.delete_data);
        createDatabase.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                LitePal.getDatabase();
            }
        });
        addData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                Book book = new Book();
                book.setName("The Da Vin Code");
                book.setAuthor("Dan Brown");
                book.setPages(454);
                book.setPrice(3.69);
                book.setPress("Unknow");
                book.save();

            }
        });
        updataData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                Book book = new Book();
                book.setToDefault("pages");
                book.updateAll("name = ? and author = ?","The Lost Symbol","Dan Brown");
            }
        });
        deleteData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
            // 这里调用了 DataSupport. deleteAll ()方法来删除数据,其中deleteAll ()方法的第一个 参数用于指定删除哪张表中的数据,Book.class就意味着删除Book表中的数据,后面的参数用于指定约束条件。另外,deleteAll()方法如果不指定约束条件,就意味着你要删除表中的所有数据,这一点和updateAll()方法是比较相似的。
                DataSupport.deleteAll(Book.class,"price < ?","15");
            }
        });
    }
}

在这里插入图片描述

1.4.6 使用LitePal查询数据

MainActivity
package com.yxj.litepaltest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import org.litepal.LitePal;
import org.litepal.crud.DataSupport;

import java.util.List;

import javax.sql.DataSource;

public class MainActivity extends AppCompatActivity {
    
    
    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button createDatabase = (Button) findViewById(R.id.create_database);
        Button addData = (Button) findViewById(R.id.add_data);
        Button updataData = (Button) findViewById(R.id.update_data);
        Button deleteData = (Button) findViewById(R.id.delete_data);
        Button queryButton = (Button) findViewById(R.id.query_data);
        createDatabase.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                LitePal.getDatabase();
            }
        });
        addData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                Book book = new Book();
                book.setName("The Da Vin Code");
                book.setAuthor("Dan Brown");
                book.setPages(454);
                book.setPrice(3.69);
                book.setPress("Unknow");
                book.save();

            }
        });
        updataData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                Book book = new Book();
                book.setToDefault("pages");
                book.updateAll("name = ? and author = ?","The Lost Symbol","Dan Brown");
            }
        });
        deleteData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                DataSupport.deleteAll(Book.class,"price < ?","15");
            }
        });
        queryButton.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                List<Book> books = DataSupport.findAll(Book.class);
                for (Book book : books) {
    
    
                    Log.d(TAG, "book name is " + book.getName());
                    Log.d(TAG, "book author is" + book.getAuthor());
                    Log.d(TAG, "book pages is" + book.getPages());
                    Log.d(TAG, "book price is" + book.getPrice());
                    Log.d(TAG, "book press is" + book.getPress());
                }
            }
        });
    }
}

在这里插入图片描述

除了 findAll()方法之外,LitePal还提供了很多其他非常有用的查询API。比如我们想要查询Book表中的第一条数据就可以这样写:
Book firstBook = DataSupport.findFirst(Book.class);
查询Book表中的最后一条数据就可以这样写:
Book lastBook = DataSupport.findLast(Book.class);
我们还可以通过连缀查询来定制更多的查询功能。
select ()方法用于指定查询哪几列的数据,对应了 SQL当中的select关键字。比如只查name和author这两列的数据,就可以这样写:
List<Book> books = DataSupport.select("name", "author").find(Book.class);
where()方法用于指定查询的约束条件,对应了 SQL当中的where关键字。比如只查页 数大于400的数据,就可以这样写:
List<Book> books = DataSupport.where("pages > ?", "400").find(Book.class);
orderO方法用于指定结果的排序方式,对应了 SQL当中的order by关键字。比如将 查询结果按照书价从高到低排序,就可以这样写:
List<Book> books = DataSupport.order("price desc").find(Book.class);
其中desc表示降序排列,asc或者不写表示升序排列。
limit ()方法用于指定查询结果的数量,比如只查表中的前3条数据,就可以这样写:
List<Book> books = DataSupport.limit(3).find(Book.class);
offsetO方法用于指定查询结果的偏移量,比如查询表中的第2条、第3条、第4条数 据,就可以这样写:
List<Book> books = DataSupport.limit(3).offset(1).find(Book.class); 
由于limit(3)查询到的是前3条数据,这里我们再加上offset(1)进行一个位置的偏移, 就能实现查询第2条、第3条、第4条数据的功能了。limit ()和offset ()方法共同对应了 SQL 当中的limit关键字。
当然,你还可以对这5个方法进行任意的连缀组合,来完成一个比较复杂的查询操作:

 queryButton.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
            // 这段代码就表示,查询Book表中第3〜4条满足页数大于400这个条件的name, author 和pages这3列数据,并将查询结果按照页数升序排列。
                List<Book> books = DataSupport.select("name","author","pages").where("pages > ?","400").order("pages").limit(2).offset(2).find(Book.class);
                for (Book book : books) {
    
    
                    Log.d(TAG, "book name is " + book.getName());
                    Log.d(TAG, "book author is" + book.getAuthor());
                    Log.d(TAG, "book pages is" + book.getPages());
                }
            }
        });

`
在这里插入图片描述

关于LitePal的查询API差不多就介绍到这里,这些API已经足够我们应对绝大多数场景的 查询需求了。当前,如果你实在有一些特殊需求,上述的API都满足不了你的时候,LitePal仍然 支持使用原生的SQL来进行查询:
Cursor c = DataSupport.findBySQL( "select * from Book where pages > ? and price < ?", "400""20");
//调用DataSupport.findBySQL()方法来进行原生查询,其中第一个参数用于指定SQL语句,后面的参数用于指定占位符的值。注意findBySQL()方法返回的是一个Cursor对象,接下来你还需要通过之前所学的老方式将数据一一取出才行。

2 跨程序共享数据——探究内容提供器

2.1 内容提供器简介

内容提供器(Content Provider)主要用于在不同的应用程序之间实现数据共享的功能,它提 供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访数据的安全性。 目前,使用内容提供器是Android实现跨程序共享数据的标准方式。
不同于文件存储和SharedPreferences存储中的两种全局可读写操作模式,内容提供器可以选 择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险。

2.2 运行时权限

2.2.1 Android权限机制详解

当时为了要访问系统的网络状态以及监听开机广播, 于是在AndroidManifest.xml文件中添加了这样两句权限声明:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.broadcasttest">
<uses-permission android:name="android.permission.ACCESSNETWORKSTATE" /> <uses-permission android:name="android.permission.RECEIVE BOOT COMPLETED" />
</manifest>
因为访问系统的网络状态以及监听开机广播涉及了用户设备的安全性,因此必须在 AndroidManifest.xml中加入权限声明,否则我们的程序就会崩溃。
那么现在问题来了,加入了这两句权限声明后,对于用户来说到底有什么影响呢?为什么这 样就可以保护用户设备的安全性了呢?
其实用户主要在以下两个方面得到了保护,一方面,如果用户在低于6.0系统的设备上安装 该程序,会在安装界面给出如图7.1所示的提醒。这样用户就可以清楚地知晓该程序一共申请了 哪些权限,从而决定是否要安装这个程序。 

在这里插入图片描述

另一方面,用户可以随时在应用程序管理界面查看任意一个程序的权限申请情况

在这里插入图片描述

Android 现在将所有的权限归成了两类,一类是普通权限,一类是危险权限。普通权限指的是那些不会直 接威胁到用户的安全和隐私的权限,对于这部分权限申请,系统会自动帮我们进行授权,而不需 要用户再去手动操作了,比如在BroadcastTest项目中申请的两个权限就是普通权限。危险权限则 表示那些可能会触及用户隐私,或者对设备安全性造成影响的权限,如获取设备联系人信息、定 位设备的地理位置等,对于这部分权限申请,必须要由用户手动点击授权才可以,否则程序就无 法使用相应的功能。
下表列出了 Android中所有的危险权限,一共是9组24个权限。

在这里插入图片描述

2.2.2 在程序运行时申请权限

CALL_PHONE这个权限是编写拨打电话功能的时候需要声明的,因为拨打电话会涉及用户手 机的资费问题,因而被列为了危险权限。在Android 6.0系统出现之前,拨打电话功能的实现其 实非常简单,修改activity main.xml布局文件,如下所示:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/make_call"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Make Call">

    </Button>
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity
package com.yxj.runtimepermissontest;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.telecom.Call;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {
    
    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button makeCall = (Button) findViewById(R.id.make_call);
        makeCall.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
              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();
              }
            }
        });
    }

    private void call(){
    
    
        try {
    
    
            Intent intent = new Intent(Intent.ACTION_CALL);
            intent.setData(Uri.parse("tel:10086"));
            startActivity(intent);
        }catch (SecurityException e){
    
    
            e.printStackTrace();
        }
    }

    @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){
    
    
                    call();
                }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按钮,效果如图所示。
由于用户还没有授权过我们拨打电话权限,因此第一次运行会弹出这样一个权限申请的对话 框,用户可以选择同意或者拒绝,比如说这里点击了 DENY,结果如图所示。

在这里插入图片描述
在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

2.3 访问其他程序中的数据

内容提供器的用法一般有两种,一种是使用现有的内容提供器来读取和操作相应程序中的数 据,另一种是创建自己的内容提供器给我们程序的数据提供外部访问接口。

2.3.1 ContentResolver 的基本用法

对于每一个应用程序来说,如果想要访问内容提供器中共享的数据,就一定要借助Content- Resolver 类,可以通过Context中的getContentResolver()方法获取到该类的实例。Content- Resolver 中提供了一系列的方法用于对数据进行CRUD操作,其中insert()方法用于添加数据, update()方法用于更新数据,delete()方法用于删除数据,query()方法用于查询数据。有没 有似曾相识的感觉?没错,SQLiteDatabase中也是使用这几个方法来进行CRUD操作的,只不过 它们在方法参数上稍微有一些区别。
不同于SQLiteDatabase, ContentResolver中的增删改查方法都是不接收表名参数的,而是使 用一个Uri参数代替,这个参数被称为内容URI。内容URI给内容提供器中的数据建立了唯一 标识符,它主要由两部分组成:authority和path.authority是用于对不同的应用程序做区分的, 一般为了避免冲突,都会采用程序包名的方式来进行命名。比如某个程序的包名是com.example. app,那么该程序对应的authority就可以命名为com.example.app. provider. path则是用于对同一应用程序中不同的表做区分的,通常都会添加到authority的后面。比如某个程序的数据库里存在 两张表:table 1和table2,这时就可以将path分别命名为/table 1和/table2,然后把authority和path 进行组合,内容 URI 就变成了 com.example.app.provider/table 1 和 com.example.app.provider/table2.不过,目前还很难辨认出这两个字符串就是两个内容URI,我们还需要在字符串的头部加上协议 声明。因此,内容URI最标准的格式写法如下:
content://com.example.app.provider/tablel content://com.example.app.provider/table2
有没有发现,内容URI可以非常清楚地表达出我们想要访问哪个程序中哪张表里的数据。 也正是因此,ContentResolver中的增删改查方法才都接收对象作为参数,因为如果使用表名 的话,系统将无法得知我们期望访问的是哪个应用程序里的表。 
在得到了内容URI字符串之后,我们还需要将它解析成Uri对象才可以作为参数传入。解析的方法也相当简单,代码如下所示:
Uri uri = Uri.parse("content://com.example.app.provider/tablel")
只需要调用Uri.parse()方法,就可以将内容URI字符串解析成Uri对象了。
现在我们就可以使用这个Uri对象来查询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参数来对想要更新的数据进行约束,以 防止所有的行都会受影响.
最后,可以调用ContentResolver的delete。方法将这条数据删除掉,代码如下所示:
getContentResolver().delete(uri, "column2 = ?", new String[] { "1" });

2.3.2 读取系统联系人

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ListView
            android:id="@+id/contacts_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        </ListView>
    </LinearLayout>



</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity
package com.yxj.contactstest;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;
import android.widget.Toolbar;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    
    

    ArrayAdapter<String> adapter;
    List<String> contactsList = new ArrayList<>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ListView contactsView = (ListView) findViewById(R.id.contacts_view);
        adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,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.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();
            }
        }
    }

    @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:
                break;
        }
    }
}
在onCreate()方法中,我们首先获取了 ListView控件的实例,并给它设置好了适配器,然 后开始调用运行时权限的处理逻辑,因为READ_CONTACTS权限是属于危险权限的。关于运行 时权限的处理流程相信你已经熟练掌握了,这里我们在用户授权之后调用readContacts ()方法 来读取系统联系人信息。
下面重点看一下readContacts()方法,可以看到,这里使用了 ContentResolver的query() 方法来查询系统的联系人数据。不过传入的Uri参数怎么有些奇怪啊?为什么没有调用 Uri.parse()方法去解析一个内容UR[字符串呢?这是因为ContactsContract .CommonData- Kinds.Phone类已经帮我们做好了封装,提供了一个CONTENT_URI常量,而这个常量就是使用 Uri.parse()方法解析出来的结果。接着我们对Cursor对象进行遍历,将联系人姓名和手机号 这些数据逐个取出,联系人姓名这一列对应的常量是ContactsContract.CommonDataKinds. Phone . DISPLAY_NAME,联系人手机号这一列对应的常量是 ContactsContract. CommonData- Kinds.Phone.NUMBER.两个数据都取出之后,将它们进行拼接,并且在中间加上换行符,然后 将拼接后的数据添加到ListView的数据源里,并通知刷新一下ListViewo最后千万不要忘记将 Cursor对象关闭掉。
这样就结束了吗?还差一点点,读取系统联系人的权限千万不能忘记声明。修改 AndroidManifest.xml中的代码,如下所示:

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.yxj.contactstest">

    <uses-permission android:name="android.permission.READ_CONTACTS"></uses-permission>
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
加入了 android.permission.READ_CONTACTS权限,这样我们的程序就可以访问到系统的 联系人数据了。现在才算是大功告成了,

在这里插入图片描述

首先弹出了申请访问联系人权限的对话框,我们点击ALLOW,然后结果如图所示。

在这里插入图片描述

2.4 创建自己的内容提供器

2.4.1 创建内容提供器的步骤

如果想要实现跨程序共享数据的功能,官方推荐的方式就是使用内容提供器,可以通过新建一个类去继承Contentprovider的方式来创建一个自己的内容提供器。 Contentprovider类中有6个抽象方法,我们在使用子类继承它的时候,需要将这6个方法全 部重写。新建MyProvider继承自Contentprovider,代码如下所示
Myprovider
package com.yxj.contactstest;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class Myprovider extends ContentProvider {
    
    
    @Override
    public boolean onCreate() {
    
    
        return false;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
    
    
        return null;
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
    
    
        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
    
    
        return null;
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
    
    
        return 0;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
    
    
        return 0;
    }
}

1 onCreate()
初始化内容提供器的时候调用。通常会在这里完成对数据库的创建和升级等操作,返回true 表示内容提供器初始化成功,返回false则表示失败。注意,只有当存在ContentResolver尝试 访问我们程序中的数据时,内容提供器才会被初始化。
2 query()
从内容提供器中查询数据。使用uri参数来确定查询哪张表,projection参数用于确定查 询哪些列,selection和selectionArgs参数用于约束查询哪些行,sortOrder参数用于对结 果进行排序,查询的结果存放在Cursor对象中返回。
3	insert()
向内容提供器中添加一条数据。使用uri参数来确定要添加到的表,待添加的数据保存在 values参数中。添加完成后,返回一个用于表示这条新记录的URI。
4	update()
更新内容提供器中已有的数据。使用uri参数来确定更新哪一张表中的数据,新数据保存在 values参数中,selection和selectionArgs参数用于约束更新哪些行,受影响的行数将作 为返回值返回。
5	delete()
从内容提供器中删除数据。使用uri参数来确定删除哪一张表中的数据,selection和 selectionArgs参数用于约束删除哪些行,被删除的行数将作为返回值返回。
6	getType()
根据传入的内容URI来返回相应的MIME类型。
可以看到,几乎每一个方法都会带有Uri这个参数,这个参数也正是调用ContentResolver 的增删改查方法时传递过来的。而现在,我们需要对传入的Uri参数进行解析,从中分析出调用调用方期望访问的表和数据。

根据传入的内容URI来返回相应的MIME类型。
可以看到,几乎每一个方法都会带有Uri这个参数,这个参数也正是调用ContentResolver 的增删改查方法时传递过来的。而现在,我们需要对传入的Uri参数进行解析,从中分析出调用 方期望访问的表和数据。
回顾一下,一个标准的内容URI写法是这样的:
content://com.example.app.provider/table1
这就表示调用方期望访问的是com.example.app这个应用的table1表中的数据。除此之外, 我们还可以在这个内容URI的后面加上一个id,如下所示:
content://com.example.app.provider/tablel/1
这就表示调用方期望访问的是com.example.app这个应用的table1表中id为1的数据。
内容URI的格式主要就只有以上两种,以路径结尾就表示期望访问该表中所有的数据,以 id结尾就表示期望访问该表中拥有相应id的数据。我们可以使用通配符的方式来分别匹配这两种格式的内容URI,规则如下。
*:表示匹配任意长度的任意字符。
#:表示匹配任意长度的数字。
所以,一个能够匹配任意表的内容URI格式就可以写成:
content://com.example.app.provider/*
而一个能够匹配tablel表中任意一行数据的内容URI格式就可以写成:
content://com.example.app.provider/tablel/#
接着,我们再借助UriMatcher这个类就可以轻松地实现匹配内容URI的功能。UriMatcher 中提供了一个addURI()方法,这个方法接收3个参数,可以分别把authority, path和一个自 定义代码传进去。这样,当调用UriMatcher的match()方法时,就可以将一个Uri对象传入, 返回值是某个能够匹配这个Uri对象所对应的自定义代码,利用这个代码,我们就可以判断出调用方期望访问的是哪张表中的数据了
Myprovider
package com.yxj.contactstest;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;


public class Myprovider extends ContentProvider {
    
    
    public static final int TABLE1_DIR = 0;
    public static final int TABLE1_ITEM = 1;
    public static final int TABLE2_DIR = 2;
    public static final int TABLE2_ITEM = 3;
    public static UriMatcher uriMatcher;


    static {
    
    
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI("com.example.app.provider","table1",TABLE1_DIR);
        uriMatcher.addURI("com.example.app.provider","table1/#",TABLE1_ITEM);
        uriMatcher.addURI("com.example.app.provider","table2",TABLE2_DIR);
        uriMatcher.addURI("com.example.app.provider","table2/#",TABLE2_ITEM);
    }
    @Override
    public boolean onCreate() {
    
    
        return false;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
    
    
        switch (uriMatcher.match(uri)) {
    
    
            case TABLE1_DIR:
                break;
            case TABLE1_ITEM:
                break;
            case TABLE2_DIR:
                break;
            case TABLE2_ITEM:
                break;
            default:
                break;
        }
        return null;
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
    
    
        switch (uriMatcher.match(uri)) {
    
    
            case TABLE1_DIR:
                return "vnd.android.cursor.dir/vnd.com.example.app.provider.table1";
            case TABLE1_ITEM:
                return "vnd.android.cursor.item/vnd.com.example.app.provider.table1";
            case TABLE2_DIR:
                return "vnd.android.cursor.dir/vnd.com.example.app.provider.table2";
            case TABLE2_ITEM:
                return "vnd.android.cursor.item/vnd.com.example.app.provider.table2";
        }

        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
    
    
        return null;
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
    
    
        return 0;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
    
    
        return 0;
    }
}

MyProvider中新增了 4个整型常量,其中TABLE1_DIR表示访问tablel表中的所 有数据,TABLE1_ITEM表示访问tablel表中的单条数据,TABLE2_DIR表示访问table2表中的所有数据,TABLE2_ITEM表示访问table2表中的单条数据。接着在静态代码块里我们创建了 UriMatcher的实例,并调用addURI()方法,将期望匹配的内容URI格式传递进去,注意这里传 入的路径参数是可以使用通配符的。然后当query()方法被调用的时候,就会通过UriMatcher 的match()方法对传入的Uri对象进行匹配,如果发现UriMatcher中某个内容URI格式成功匹 配了该Uri对象,则会返回相应的自定义代码,然后我们就可以判断出调用方期望访问的到底是 什么数据了。
上述代码只是以query()方法为例做了个示范,其实insert。、update。、delete这 几个方法的实现也是差不多的,它们都会携带Uri这个参数,然后同样利用UriMatcher的match() 方法判断出调用方期望访问的是哪张表,再对该表中的数据进行相应的操作就可以了。
除此之外,还有一个方法你会比较陌生,即getType()方法。它是所有的内容提供器都必须 提供的一个方法,用于获取Uri对象所对应的MIME类型。一个内容URI所对应的MIME字符串主要由3部分组成,Android对这3个部分做了如下格式规定。
必须以vnd开头。
如果内容URI以路径结尾,则后接android.cursor.dir/,如果内容URI以id结尾, 则后接 android. cursor.item/。
最后接上 vnd.<authority>.<path>。
所以,对于 content://com.example.app.provider/tablel 这个内容 URI,它所对应的 MIME类型 就可以写成:
vnd.android.cursor.dir/vnd.com.example.app.provider.tablel
对于 content://com.example.app.provider/tablel/l1这个内容 URI,它所对应的 MIME类型就可 以写成:
vnd.android.cursor.item/vnd.com.example.app.provider.table1

2.4.2 实现跨程序数据共享

简单起见,我们还是在上一章中DatabaseTest项目的基础上继续开发,通过内容提供器来给 它加入外部访问接口。打开DatabaseTest项目,首先将MyDatabaseHelper中使用Toast弹出创建 数据库成功的提示去除掉,因为跨程序访问时我们不能直接使用Toasto然后创建一个内容提供 器,右击 com.example.broadcasttest>New—>Other->Content Provider,
修改DatabaseProvider中的代码
DatabaseProvider
package com.yxj.databasetest;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;

public class DatabaseProvider extends ContentProvider {
    
    

    public static final int BOOK_DIR = 0;
    public static final int BOOK_ITEM = 1;
    public static final int CATEGORY_DIR = 2;
    public static final int CATEGORY_ITEM =3;
    public static final String AUTHORITY = "com.yxj.databasetest.provider";
    private static UriMatcher uriMatcher;
    private MyDatabaseHelper dbHelper;

    static {
    
    
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI(AUTHORITY,"book",BOOK_DIR);
        uriMatcher.addURI(AUTHORITY,"book/#",BOOK_ITEM);
        uriMatcher.addURI(AUTHORITY,"category",CATEGORY_DIR);
        uriMatcher.addURI(AUTHORITY,"catagory/#",CATEGORY_ITEM);
    }
    public DatabaseProvider() {
    
    
    }



    @Override
    public boolean onCreate() {
    
    
        // TODO: Implement this to initialize your content provider on startup.
        dbHelper = new MyDatabaseHelper(getContext(),"BookStore.db",null,2);
        return  true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
    
    
        // TODO: Implement this to handle query requests from clients.
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        Cursor cursor = null;
        switch (uriMatcher.match(uri)) {
    
    
            case BOOK_DIR:
                cursor = db.query("Book", projection, selection, selectionArgs, null, null, sortOrder);
                break;
            case BOOK_ITEM:
                String bookId = uri.getPathSegments().get(1);
                cursor = db.query("Book", projection, "id = ?", new String[]{
    
     bookId }, null, null, sortOrder);
                break;

            case CATEGORY_DIR:
                cursor = db.query("Category", projection, selection, selectionArgs, null, null, sortOrder);
                break;
            case CATEGORY_ITEM:
                String categoryId = uri.getPathSegments().get(1);
                cursor = db.query("Category", projection, "id = ?", new String[]{
    
     categoryId }, null, null, sortOrder);
                break;
            default:
                break;
        }
        return cursor;
    }


    @Override
    public Uri insert(Uri uri, ContentValues values) {
    
    
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        Uri uriReturn = null;
        switch (uriMatcher.match(uri)) {
    
    
            case BOOK_DIR:
            case BOOK_ITEM:
                long newBookId = db.insert("Book", null, values);
                uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + newBookId);
                break;
            case CATEGORY_DIR:
            case CATEGORY_ITEM:
                long newCategoryId = db.insert("Category", null, values);
                uriReturn = Uri.parse("content://" + AUTHORITY + "/category/" + newCategoryId);
                break;
            default:
                break;
        }
        return uriReturn;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {
    
    
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        int updateRows = 0;
        switch (uriMatcher.match(uri)) {
    
    
            case BOOK_DIR:
                updateRows = db.update("Book", values, selection, selectionArgs);
                break;
            case BOOK_ITEM:
                String bookId = uri.getPathSegments().get(1);
                updateRows = db.update("Book", values, "id = ?", new String[]{
    
     bookId });
                break;
            case CATEGORY_DIR:
                updateRows = db.update("Category", values, selection, selectionArgs);
                break;
            case CATEGORY_ITEM:
                String categoryId = uri.getPathSegments().get(1);
                updateRows = db.update("Category", values, "id = ?", new String[]{
    
     categoryId });
                break;

            default:
                break;

        }

        return  updateRows;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
    
    
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        int deleteRows = 0;
        switch (uriMatcher.match(uri)) {
    
    
            case BOOK_DIR:
                deleteRows = db.delete("Book",selection,selectionArgs);
                break;
            case BOOK_ITEM:
                String bookId = uri.getPathSegments().get(1);
                deleteRows = db.delete("Book","id = ?",new String[]{
    
     bookId });
                break;
            case CATEGORY_DIR:
                deleteRows = db.delete("Category",selection,selectionArgs);
                break;
            case CATEGORY_ITEM:
                String categoryId = uri.getPathSegments().get(1);
                deleteRows = db.delete("Category","id = ?",new String[]{
    
     categoryId });
                break;
            default:
                break;
        }
        return deleteRows;
    }

    @Override
    public String getType(Uri uri) {
    
    
        switch (uriMatcher.match(uri)) {
    
    
            case BOOK_DIR:
                return "vnd.android.cursor.dir/vnd.com.yxj.databasetest.provider.book";
            case BOOK_ITEM:
                return "vnd.android.cursor.item/vnd.com.yxj.databasetest.provider.book";
            case CATEGORY_DIR:
                return "vnd.android.cursor.dir/vnd.com.yxj.databasetest.provider.category";
            case CATEGORY_ITEM:
                return "vnd.android.cursor.item/vnd.com.yxj.databasetest.provider.category";
        }
        return  null;
    }

}

首先在类的一开始,同样是定义了 4个常量,分别用于表示访问Book表中 的所有数据、访问Book表中的单条数据、访问Category表中的所有数据和访问Category表中的 单条数据。然后在静态代码块里对UriMatcher进行了初始化操作,将期望匹配的几种URI格式 添加了进去。
接下来就是每个抽象方法的具体实现了,先来看下onCreateO方法,这个方法的代码很短, 就是创建了一个MyDatabaseHelper的实例,然后返回true表示内容提供器初始化成功,这时数 据库就已经完成了创建或升级操作。
接着看一下query ()方法,在这个方法中先获取到了 SQLiteDatabase的实例,然后根据传入 的Uri参数判断出用户想要访问哪张表,再调用SQLiteDatabase的query()进行查询,并将 Cursor对象返回就好了。注意当访问单条数据的时候有一个细节,这里调用了 Uri对象的 getPathSegments()方法,它会将内容URI权限之后的部分以"/"符号进行分割,并把分割后 的结果放入到一个字符串列表中,那这个列表的第0个位置存放的就是路径,第1个位置存放的 就是id 了。得到了 id之后,再通过selection和selectionArgs参数进行约束,就实现了查 询单条数据的功能。
再往后就是insert()方法,同样它也是先获取到了 SQLiteDatabase的实例,然后根据传入 的Uri参数判断出用户想要往哪张表里添加数据,再调用SQLiteDatabase的insert()方法进行 添加就可以了。注意insert()方法要求返回一个能够表示这条新增数据的URI,所以我们还需 要调用Uri.parse()方法来将一个内容URI解析成Uri对象,当然这个内容URI是以新增数据 的id结尾的。
接下来就是update()方法了,相信这个方法中的代码已经完全难不倒你了。也是先获取 SQLiteDatabase的实例,然后根据传入的Uri参数判断出用户想要更新哪张表里的数据,再调用 SQLiteDatabase的update方法进行更新就好了,受影响的行数将作为返回值返回。
下面是deleteO方法,是不是感觉越到后面越轻松了?因为你已经渐入佳境,真正地找到 窍门了。这里仍然是先获取到SQLiteDatabase的实例,然后根据传入的Uri参数判断出用户想要 删除哪张表里的数据,再调用SQLiteDatabase的delete方法进行删除就好了,被删除的行数 将作为返回值返回。
最后是getType()方法,这个方法中的代码完全是按照上一节中介绍的格式规则编写的,相 信已经没有什么解释的必要了。这样我们就将内容提供器中的代码全部编写完了。
另外还有一点需要注意,内容提供器一定要在AndroidManifest.xml文件中注册才可以使用。 
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.yxj.databasetest">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <provider
            android:name=".DatabaseProvider"
            android:authorities="com.yxj.databasetest.provider"
            android:enabled="true"
            android:exported="true">

        </provider>

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
可以看到,<application>标签内出现了一个新的标签<provider>,我们使用它来对 DatabaseProvider这个内容提供器进行注册Oandroid: name属性指定了 DatabaseProvider的类名, android:authorities 属性指定了 DatabaseProvider 的 authority,而 enabled 和 exported 属性 则是根据我们刚才勾选的状态自动生成的,这里表示允许DatabaseProvider被其他应用程序进行 访问。
现在DatabaseTest这个项目就已经拥有了跨程序共享数据的功能了.
创建一个新项目ProviderTest,我们就将通过这个程序去访问DatabaseTest中的数据。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <Button
            android:id="@+id/add_data"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Add To Book">

        </Button>

        <Button
            android:id="@+id/query_data"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Query From Book">

        </Button>

        <Button
            android:id="@+id/update_data"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Update Book">

        </Button>

        <Button
            android:id="@+id/delete_data"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Delete From Book">
        </Button>
    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity
package com.yxj.providertest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {
    
    

    private String newId;
    private static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button addData = (Button) findViewById(R.id.add_data);
        addData.setOnClickListener(new View.OnClickListener() {
    
    
                                       @Override
                                       public void onClick(View v) {
    
    
                                           Uri uri = Uri.parse("content://com.yxj.databasetest.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);

                                       }
                                   });
        Button queryData = (Button) findViewById(R.id.query_data);
        queryData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                Uri uri = Uri.parse("content://com.yxj.databasetest.provider/book");
                Cursor cursor = getContentResolver().query(uri,null,null,null,null);
                if (cursor != null) {
    
    
                    while (cursor.moveToNext()) {
    
    
                        String name = cursor.getString(cursor.getColumnIndex("name"));
                        String author = cursor.getString(cursor.getColumnIndex("author"));
                        int pages = cursor.getInt(cursor.getColumnIndex("pages"));
                        double price = cursor.getDouble(cursor.getColumnIndex("price"));
                        Log.d(TAG, "book name is " + name);
                        Log.d(TAG, "book author is " + author);
                        Log.d(TAG, "book pages is " + pages);
                        Log.d(TAG, "book price is " + price);
                    }
                    cursor.close();
                }
            }
        });
        Button updataData = (Button) findViewById(R.id.update_data);
        updataData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                Uri uri = Uri.parse("content://com.yxj.databasetest.provider/book/" + newId);
                ContentValues values = new ContentValues();
                values.put("name","A Storm of Swords");
                values.put("pages", 1216);
                values.put("price", 24.05);
                getContentResolver().update(uri,values,null,null);
            }
        });
        Button deleteData = (Button) findViewById(R.id.delete_data);
        deleteData.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                Uri uri = Uri.parse("content://com.yxj.databasetest.provider/book/" + newId);
                getContentResolver().delete(uri,null,null);
            }
        });
    }
}
可以看到,我们分别在这4个按钮的点击事件里面处理了增删改查的逻辑。添加数据的时候, 首先调用了 Uri.parse()方法将一个内容URI解析成对象,然后把要添加的数据都存放到 Contentvalues对象中,接着调用ContentResolver的insert()方法执行添加操作就可以了。 注意insert()方法会返回一个Uri对象,这个对象中包含了新增数据的id,我们通过getPath- SegmentsO方法将这个id取出,稍后会用到它。
查询数据的时候,同样是调用了 Uri.parseO方法将一个内容URI解析成Uri对象,然后 调用ContentResolver的query()方法去查询数据,查询的结果当然还是存放在Cursor对象 中的。之后对Cursor进行遍历,从中取出查询结果,并一一打印出来。
更新数据的时候,也是先将内容URI解析成Uri对象,然后把想要更新的数据存放到 Contentvalues对象中,再调用ContentResolver的update()方法执行更新操作就可以了。 注意这里我们为了不想让Book表中的其他行受到影响,在调用Uri.parse()方法时,给内容 URI的尾部增加了一个id,而这个id正是添加数据时所返回的。这就表示我们只希望更新刚刚 添加的那条数据,Book表中的其他行都不会受影响。
删除数据的时候,也是使用同样的方法解析了一个以id结尾的内容URI,然后调用 ContentResolver的deleteO方法执行删除操作就可以了。由于我们在内容URI里指定了一个 id,因此只会删掉拥有相应id的那行数据,Book表中的其他数据都不会受影响。

点击一下Add To Book按钮,此时数据就应该已经添加到DatabaseTest程序的数据库中了, 我们可以通过点击Query From Book按钮来检查一下,打印日志

在这里插入图片描述

然后点击一下Update Book按钮来更新数据,再点击一下Query From Book按钮进行检查, 结果如图所示

在这里插入图片描述

最后点击Delete From Book按钮删除数据,此时再点击Query From Book按钮就查询不到数 据了。由此可以看出,我们的跨程序共享数据功能已经成功实现了!现在不仅是ProviderTest程 序,任何一个程序都可以轻松访问DatabaseTest中的数据,而且我们还丝毫不用担心隐私数据泄 漏的问题。

3 运用手机多媒体

3.1 使用通知

通知(Notification)是Android系统中比较有特色的一个功能,当某个应用程序希望向用户发出一些提示信息,而该应用程序又不在前台运行时,就可以借助通知来实现。发出一条通知后,手机最上方的状态栏中会显示一个通知的图标,下拉状态栏后可以看到通知的详细内容。Android 的通知功能获得了大量用户的认可和喜爱,就连iOS系统也在5.0版本之后加入了类似的功能。

3.2 通知的基本用法

通知的用法还是比较灵活 的,既可以在活动里创建,也可以在广播接收器里创建,当然还可以在服务里创建。相比于广播接收器和服务,在活动里创建通知的场景还是比较少的,因为一般只有 当程序进入到后台的时候我们才需要使用通知。
不过,无论是在哪里创建通知,整体的步骤都是相同的,下面我们就来学习一下创建通知的 详细步骤。首先需要一个NotificationManager来对通知进行管理,可以调用Context的getSystem-Service()方法获取到。getSystemServicef)方法接收一个字符串参数用于确定获取系统的哪个服务,这里我们传入 Context. NOTIFICATION_SERVICE 即可。因此,获取 NotificationManager 的实例就可以写成"NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
接下来需要使用一个Builder构造器来创建Notification对象,但问题在于,几乎Android 系统的每一个版本都会对通知这部分功能进行或多或少的修改,API不稳定性问题在通知上面突 显得尤其严重。那么该如何解决这个问题呢?其实解决方案我们之前已经见过好几回了,就是使 用support库中提供的兼容API。support*库中提供了一个NotificationCompat类,使用这个 类的构造器来创建Notification对象,就可以保证我们的程序在所有Android系统版本上都能 正常工作了,代码如下所示:
Notification notification = new NotificationCompat.Builder(context).build();
当然,上述代码只是创建了一个空的Notification对象,并没有什么实际作用,我们可以 在最终的build ()方法之前连缀任意多的设置方法来创建一个丰富的Notification对象,先来 看一些最基本的设置:
Notification notification = new NotificationCompat.Builder(context)
.setContentTitle("This is content title")
.setContentText("This is content text")
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.drawab'le. smallicon)
.setLargeIcon(BitmapFactory.decodeResource(getResources(),
R.drawable.largeicon))
.build();
上述代码中一共调用了 5个设置方法,下面我们来一一解析一下
setContentTitle()方法 用于指定通知的标题内容,下拉系统状态栏就可以看到这部分内容。setContentText ()方法用 于指定通知的正文内容,同样下拉系统状态栏就可以看到这部分内容。setWhen()方法用于指定 通知被创建的时间,以毫秒为单位,当下拉系统状态栏时,这里指定的时间会显示在相应的通知 setSmalllconO方法用于设置通知的小图标,注意只能使用纯alpha图层的图片进行设置, 小图标会显示在系统状态栏上。setLargeicon ()方法用于设置通知的大图标,当下拉系统状态 栏时,就可以看到设置的大图标了。
以上工作都完成之后,只需要调用NotificationManager的notify()方法就可以让通知显示 出来了。notify()方法接收两个参数,第一个参数是id,要保证为每个通知所指定的id都是不同的。第二个参数则是Notification对象,这里直接将我们刚刚创建好的Notification对 象传入即可。因此,显示一个通知就可以写成:
manager.notify(l, notification);
到这里就已经把创建通知的每一个步骤都分析完了,下面就让我们通过一个具体的例子来看 一看通知到底是长什么样的。
新建一个NotificationTest项目,并修改activity main.xml中的代码,如下所示:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/send_notice"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Send notice">

    </Button>

</androidx.constraintlayout.widget.ConstraintLayout>
activity_notification.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".NotificationActivity">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:textSize="24sp"
            android:text="This is notification layout">

        </TextView>
    </RelativeLayout>


</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity

package com.yxj.notificationtest;


import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NotificationCompat;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity  implements View.OnClickListener{
    
    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button sendNotice = (Button) findViewById(R.id.send_notice);
        sendNotice.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
    
    
        switch (v.getId()) {
    
    
            case R.id.send_notice:
                Intent intent = new Intent(this, NotificationActivity.class);
                PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);
                NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
                Notification notification = null;
                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
    
    
                    notification = new Notification.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))
                            .setContentIntent(pi)
                            .setAutoCancel(true)
                            .build();
                }
                manager.notify(1,notification);
                break;

            default:
                break;

        }
    }
}
NotificationActivity
package com.yxj.notificationtest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;

public class NotificationActivity extends AppCompatActivity {
    
    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_notification);
    }
}
pendingintent从名字上看起来就和Intent有些类似,它们之间也确实存在着不少共同点。比 如它们都可以去指明某一个“意图”,都可以用于启动活动、启动服务以及发送广播等。不同的 是,Intent更加倾向于去立即执行某个动作,而Pendingintent更加倾向于在某个合适的时机去执 行某个动作。所以,也可以把Pendingintent简单地理解为延迟执行的Intent。
Pendingintent的用法同样很简单,它主要提供了几个静态方法用于获取Pendingintent的实例, 可以根据需求来选择是使用getActivity()方法、getBroadcast ()方法,还是getService()方法。这几个方法所接收的参数都是相同的,第一个参数依旧是Context,不用多做解释。第二 个参数一般用不到,通常都是传入0即可。第三个参数是一个Intent对象,我们可以通过这个 对象构建出Pendingintent的“意图”。第四个参数用于确定Pendingintent的行为,有FLAG_ONE_ SHOT、FLAG_NO_CREATE. FLAG_CANCEL_CURRENT 和 FLAG_UPDATE_CURRENT 这 4 种值可选,每 种值的具体含义你可以查看文档,通常情况下这个参数传入0就可以了。
对Pendingintent有了一定的了解后,我们再回过头来看一下NotificationCompat. Builder。这个构造器还可以再连缀一个setContentlntent ()方法,接收的参数正是一个 Pendingintent对象。因此,这里就可以通过Pendingintent构建出一个延迟执行的“意图”,当 用户点击这条通知时就会执行相应的逻辑。

这里先是使用Intent表达出我们想要启动NotificationActivity的“意图”,然后将 构建好的Intent对象传入到Pendingintent的getActivity()方法里,以得到Pendingintent的实 例,接着在 NotificationCompat.Builder 中调用 setcontentintent()方法,把它作为参数 传入即可。
现在重新运行一下程序,并点击Send notice按钮,依旧会发出一条通知。然后下拉系统状态 栏,点击一下该通知,就会看到NotificationActivity这个活动的界面了

如果我们没有在代码中对该通知进 行取消,它就会一直显示在系统的状态栏上。解决的方法有两种,一种是在NotificationCompat. Builder中再连缀一个set/AutoCancel()方法,一种是显式地调用NotificationManager的 cancel()方法将它取消,两种方法我们都学习一下。

第一种方法写法如下:
Notification notification = new NotificationCompat.Builder(this)
.setAutoCancel(true)
.build.;
可以看到,setAutoCancel()方法传入true,就表示当点击了这个通知的时候,通知会自 动取消掉。
第二种方法写法如下:
public class NotificationActivity extends AppCompatActivity {
(QOverride
protected void onCreate(Bundle savedlnstanceState) {
super.onCreate(savedlnstanceState); setContentView(R.layout.notificationlayout);
NotificationManager manager = (NotificationManager) getSystemService
(NOTIFICATION_SERVICE); manager.cancel(1);
}
}
这里我们在cancel ()方法中传入了 1,这个1是什么意思呢?还记得在创建通知的时候给 每条通知指定的id吗?当时我们给这条通知设置的id就是1。因此,如果你想取消哪条通知, 在cancel ()方法中传入该通知的id就行了。

在这里插入图片描述
在这里插入图片描述

3.3 通知的进阶技巧

实际上,NotificationCompat.Builder中提 供了非常丰富的API来让我们创建出更加多样的通知效果。当然,每一个API都详细地讲一遍不 太可能,我们只能从中选一些比较常用的API来进行学习。先来看看setSound()方法吧,它可 以在通知发出的时候播放一段音频,这样就能够更好地告知用户有通知到来。setSound ()方法 接收一个参数,所以在指定音频文件的时候还需要先获取到音频文件对应的URI。比如说, 每个手机的/system/media/audio/ringtones目录下都有很多的音频文件,我们可以从中随便选一个 音频文件,那么在代码中就可以这样指定:
Notification notification = new NotificationCompat.Builder(this)
,setSound(Uri.fromFile(new File("/system/media/audio/ringtones/Luna.ogg))) .build();
除了允许播放音频外,我们还可以在通知到来的时候让手机进行振动,使用的是vibrate 这个属性。它是一个长整型的数组,用于设置手机静止和振动的时长,以毫秒为单位。下标为0 的值表示手机静止的时长,下标为1的值表示手机振动的时长,下标为2的值又表示手机静止的 时长,以此类推。所以,如果想要让手机在通知到来的时候立刻振动1秒,然后静止1秒,再振 动1秒,代码就可以写成:
Notification notification = new NotificationCompat.Builder(this)
.setVibrate(new long[] {0, 1000, 1000, 1006 ))
.build();
不过,想要控制手机振动还需要声明权限。因此,我们还得编辑AndroidManifest.xml文件, 加入如下声明:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.notificationtest"
android :versionCode="l,,
android:versionName="1.0" >
<uses-peemission android:name='*android.permission.VIBRATE" />
</manifest>
学会了控制通知的声音和振动,下面我们来看一下如何在通知到来时控制手机LED灯的显示
现在的手机基本上都会前置一个LED灯,当有未接电话或未读短信,而此时手机又处于锁 屏状态时,LED灯就会不停地闪烁,提醒用户去查看。我们可以使用setLights()方法来实现 这种效果,setLights()方法接收3个参数,第一个参数用于指定LED灯的颜色,第二个参数 用于指定LED灯亮起的时长,以毫秒为单位,第三个参数用于指定LED灯暗去的时长,也是以 毫秒为单位。所以,当通知到来时,如果想要实现LED灯以绿色的灯光一闪一闪的效果,就可 以写成:
Notification notification = new NotificationCompat.Builder(this)
.setLights(Color.GREEN, 1000, 1000)
.build();
当然,如果你不想进行那么多繁杂的设置,也可以直接使用通知的默认效果,它会根据当前 手机的环境来决定播放什么铃声,以及如何振动,写法如下:
Notification notification = new NotificationCompat.Builder(this)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.build();
注意,以上所涉及的这些进阶技巧都要在手机上运行才能看得到效果,模拟器是无法表现出 振动以及LED灯闪烁等功能的。

3.4 通知的高级功能

观察NotificationCompat. Builder这个类,你会发现里面还有很多API是我们没有 使用过的。那么下面我们就来学习一些更加强大的API的用法,从而构建岀更加丰富的通知效果。
先来看看setstyle()方法,这个方法允许我们构建出富文本的通知内容。也就是说通知中 不光可以有文字和图标,还可以包含更多的东西。setStyle()方法接收一个NotificationCompat. Style 参数,这个参数就是用来构建具体的富文本信息的,如长文字、图片等。在开始使用setStyle()方法之前,我们先来做一个试验吧,之前的通知内容都比较短,如 果设置成很长的文字会是什么效果呢?比如这样写:
Notification notification = new NotificationCompat.Builder(this)
.setContentText("Learn how to build notifications, send and sync data, and use voice actions. Get the official Android IDE and developer tools to build apps for Android.")
.build();

在这里插入图片描述

以看到,通知内容是无法显示完整的,多余的部分会用省略号来代替。其实这也很正常, 因为通知的内容本来就应该言简意赅,详细内容放到点击后打开的活动当中会更加合适。
但是如果你真的非常需要在通知当中显示一段长文字,Android也是支持的,通过setStyle()方法就可以做到
 .setStyle(new Notification.BigTextStyle().bigText("Learn how to build notifications,send and sync data,and use voice actions." +
                                   "Get the official Android IDE and developer tools to build apps for Android"))

我们在 setStyle()方法中创建了一个 NotificationCompat.BigTextStyle 对象,这个对 象就是用于封装长文字信息的,我们调用它的bigText()方法并将文字内容传入就可以了

在这里插入图片描述

.setStyle(new Notification.BigPictureStyle().bigPicture(BitmapFactory.decodeResource(getResources(),R.mipmap.aa)))

可以看到,这里仍然是调用的setstyle()方法,这次我们在参数中创建了一个 Notif icationCompat. BigPictureStyle对象,这个对象就是用于设置大图片的,然后调用它 的bigPicture()方法并将图片传入。这里我事先准备好了一张图片,通过BitmapFactory的 decodeResource()方法将图片解析成Bitmap对象,再传入到bigPictureO方法中就可以了。

在这里插入图片描述

setPriority()方法接收一个整型参数用于设置这条通知的重要程度,一共有5个常量值可选:PRIORITY, DEFAULT表示默认的重要程度,和不设置效果是一样的;PRIORITY_MIN表示最低的重要程度, 系统可能只会在特定的场景才显示这条通知,比如用户下拉状态栏的时候;PRIORITY_LOW表示 较低的重要程度,系统可能会将这类通知缩小,或改变其显示的顺序,将其排在更重要的通知之 后;PRIORITY_HIGH表示较高的重要程度,系统可能会将这类通知放大,或改变其显示的顺序, 将其排在比较靠前的位置;PRIORITY_MAX表示最高的重要程度,这类通知消息必须要让用户立 刻看到,甚至需要用户做出响应操作
.setPriority(NotificationCompat.PRIORITY_MAX)

在这里插入图片描述

3.5 调用摄像头和相册

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <Button
            android:id="@+id/take_photo"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Take Photo"
            >

        </Button>

        <Button
            android:id="@+id/choose_from_alum"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Choose From Album">

        </Button>

        <ImageView
            android:id="@+id/picture"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            >

        </ImageView>
    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity
package com.yxj.cameraalbumtest;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;

import android.Manifest;
import android.content.ContentUris;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;

public class MainActivity extends AppCompatActivity {
    
    
    public static final int TAKE_PHOTO = 1;
    private ImageView picture;
    private Uri imageUri;
    public static final int CHOOSE_PHOTO = 2;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button takePhoto = (Button) findViewById(R.id.take_photo);
        picture = (ImageView) findViewById(R.id.picture);
        Button chooseFromAlbum = (Button) findViewById(R.id.choose_from_alum);
        takePhoto.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                File outputImage = new File(getExternalCacheDir(), "output_image.jpg");
                try {
    
    
                    if (outputImage.exists()) {
    
    
                        outputImage.delete();
                    }
                    outputImage.createNewFile();
                } catch (IOException e) {
    
    
                    e.printStackTrace();
                }
                if (Build.VERSION.SDK_INT >= 24) {
    
    
                    imageUri = FileProvider.getUriForFile(MainActivity.this, "com.yxj.cameraalbumtest.fileprovider", outputImage);
                } else {
    
    
                    imageUri = Uri.fromFile(outputImage);
                }

                Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
                intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
                startActivityForResult(intent,TAKE_PHOTO);
            }
        });
        chooseFromAlbum.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View v) {
    
    
                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();
                }
            }

        });

    }

    private void openAlbum() {
    
    
        Intent intent = new Intent("android.intent.action.GET_CONTENT");
        intent.setType("image/*");
        startActivityForResult(intent,CHOOSE_PHOTO);
    }

    @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) {
    
    
                    openAlbum();
                } else {
    
    
                    Toast.makeText(this, "You denied the permission",Toast.LENGTH_SHORT).show();
                }
                break;
            default:
                break;
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    
    
        super.onActivityResult(requestCode, resultCode, data);
        switch (requestCode) {
    
    
            case TAKE_PHOTO:
                if (resultCode == RESULT_OK) {
    
    
                    try {
    
    
                        Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri));
                        picture.setImageBitmap(bitmap);
                    } catch (FileNotFoundException e) {
    
    
                        e.printStackTrace();
                    }
                }

                break;
            case CHOOSE_PHOTO:
                if (resultCode == RESULT_OK) {
    
    
                    //判断手机系统版本号 ~
                    if (Build.VERSION.SDK_INT >= 19) {
    
    
                        // 4.4及以上系统使甬这个方法处理图片
                        handleImageOnKitKat(data);
                    } else {
    
    
                        // 4.4以下系统使用这个方法处理图片
                        handlerImageBeforeKitKat(data);
                    }
                }
            default:
                break;
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    private void handleImageOnKitKat(Intent data) {
    
    
        String imagePath = null;
        Uri uri = data.getData();
        if (DocumentsContract.isDocumentUri(this, uri)) {
    
    
            //如果是document类型的Uri,则通过document id处理
            String docId = DocumentsContract.getDocumentId(uri);
            if ("com.android.providers.media.documents".equals(uri.getAuthority())) {
    
    
                String id = docId.split(":")[1]; // 解析出数字格式的 id
                String selection = MediaStore.Images.Media._ID + "=" + id;
                imagePath = getImagePath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection);
            } else if ("com.android.providers.downloads.documents".equals(uri.getAuthority())) {
    
    
                Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(docId));
                imagePath = getImagePath(contentUri, null);
            }


        } else if ("content".equalsIgnoreCase(uri.getScheme())) {
    
    
            //如果是content类型的Uri,则使用普通方式处理
            imagePath = getImagePath(uri, null);
        } else if ("file".equalsIgnoreCase(uri.getScheme())) {
    
    
            //如果是file类型的Uri,直接获取图片路径即可
            imagePath = uri.getPath();
        }
        displayImage((imagePath));  //根据图片路径显示图片
    }

    private void handlerImageBeforeKitKat(Intent data) {
    
    
        Uri uri = data.getData();
        String imagePath = getImagePath(uri, null);
        displayImage(imagePath);
    }


    private String getImagePath(Uri uri, String selection) {
    
    
        String path = null;
        //通过Uri和selection来获取真实的图片路径
        Cursor cursor = getContentResolver().query(uri, null, selection, null, null);
        if (cursor != null) {
    
    
            if (cursor.moveToFirst()) {
    
    
                path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));

            }
            cursor.close();
        }
        return path;
    }

    private void displayImage(String imagePath) {
    
    
        if (imagePath != null) {
    
    
            Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
            picture.setImageBitmap(bitmap);
        } else {
    
    
            Toast.makeText(this,"failed to get image",Toast.LENGTH_SHORT).show();
        }
    }
}
首先这里创建了一个File对象,用于存放摄像头拍下的图片,这里我们把图片命名为output._image.jpg,并将它存放在手机SD卡的应用关联缓存目录下。什么叫作应用关联缓存目录呢?就 是指SD卡中专门用于存放当前应用缓存数据的位置,调用getExternalCacheDir()方法可以得到这个目录,具体的路径是/sdcard/Android/data/<packagename>/cache.那么为什么要使用应用关联缓存目录来存放图片呢?因为从Android6.0系统开始,读写SD卡被列为了危险权限,如果将图片存放在SD卡的任何其他目录,都要进行运行时权限处理才行,而使用应用关联目录则可以 跳过这一步。
接着会进行一个判断,如果运行设备的系统版本低于Android7.0,就调用Uri的f romFile()方法将File对象转换成Uri对象,这个Uri对象标识着output_image.jpg这张图片的本地真实路 径。否则,就调用FileProvider的getUriForFile()方法将File对象转换成一个封装过的Uri 对象。getUriForFile()方法接收3个参数,第一个参数要求传入Context对象,第二个参数 可以是任意唯一的字符串,第三个参数则是我们刚刚创建的File对象。之所以要进行这样一层 转换,是因为从Android 7.0系统开始,直接使用本地真实路径的Uri被认为是不安全的,会抛出 一个FileUriExposedException异常。而FileProvider则是一种特殊的内容提供器,它使用了和内 容提供器类似的机制来对数据进行保护,可以选择性地将封装过的Uri共享给外部,从而提高了 应用的安全性。
接下来构建出了一个Intent对象,并将这个Intent的action指定为android.media. action.IMAGE_CAPTURE,再调用Intent的putExtra()方法指定图片的输出地址,这里填入 刚刚得到的Uri对象,最后调用startActivityForResult()来启动活动。由于我们使用的是一 个隐式Intent,系统会找出能够响应这个Intent的活动去启动,这样照相机程序就会被打开,拍下的照片将会输出到output_image.jpg中。
注意,刚才我们是使用startActivityForResult()来启动活动的,因此拍完照后会有结 果返回到onActivityResult()方法中。如果发现拍照成功,就可以调用BitmapFactory的 decodeStream()方法将output_image.jpg这张照片解析成Bitmap对象,然后把它设置到ImageView 中显示出来。
不过现在还没结束,刚才提到了内容提供器,那么我们自然要在AndroidManifest.xml对内 容提供器进行注册了.

可以看到,在Choose From Album按钮的点击事件里我们先是进行了一个运行时权限处理, 动态申请WRITE_EXTERNAL_STORAGE这个危险权限。为什么需要申请这个权限呢?因为相册中 的照片都是存储在SD卡上的,我们要从SD卡中读取照片就需要申请这个权限。WRITE_ EXTERNAL_STORAGE表示同时授予程序对SD卡读和写的能力。
当用户授权了权限申请之后会调用openAlbum()方法,这里我们先是构建出了一个Intent 对象,并将它的 action 指定为 android. intent. action.GET_C0NTENT. 接着给这个 Intent 对象设置一些必要的参数,然后调用startActivityForResult()方法就可以打开相册程序选 择照片了。注意在调用startActivityForResult()方法的时候,我们给第二个参数传入的值 变成了 CHOOSE_PHOTO,这样当从相册选择完图片回到onActivityResult ()方法时,就会进入 CHOOSE_PHOTO的case来处理图片。接下来的逻辑就比较复杂了,首先为了兼容新老版本的手 机,我们做了一个判断,如果是4.4及以上系统的手机就调用handlelmageOnKitKat ()方法来 处理鹵片,否则就调用handleImageBeforeKitKat()方法来处理图片。之所以要这样做,是因 为Android系统从4.4版本开始,选取相册中的图片不再返回图片真实的Uri 了,而是一个封装 过的Uri,因此如果是4.4版本以上的手机就需要对这个Uri进行解析才行。
那么handlelmageOnKitKat()方法中的逻辑就基本是如何解析这个封装过的Uri 了。这里 有好几种判断情况,如果返回的Uri是document类型的话,那就取岀document id进行处理, 
如果不是的话,那就使用普通的方式处理。另外,如果Uri的authority是media格式的话,document id还需要再进行一次解析,要通过字符串分割的方式取出后半部分才能得到真正的数字ido取 出的id用于构建新的Uri和条件语句,然后把这些值作为参数传入到getlmagePath()方法当中, 就可以获取到图片的真实路径了。拿到图片的路径之后,再调用displaylmage()方法将图片显 示到界面上。
相比于 handlelmageOnKitKat ()方法,handlelmageBeforeKitKat ()方法中的逻辑就要 简单得多了,因为它的Uri是没有封装过的,不需要任何解析,直接将Uri传入到getlmagePath() 方法当中就能获取到图片的真实路径了,最后同样是调用displaylmage()方法来让图片显示到 界面上。
现在将程序重新运行到手机上,然后点击一下Choose From Album按钮,首先会弹岀权限申 请框,如图所示。
点击允许之后就会打开手机相册,如图所示。
然后随意选择一张照片,回到我们程序的界面,选中的照片应该就会显示出来了,如图所示
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.yxj.cameraalbumtest">

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.yxj.cameraalbumtest.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths"
                >

            </meta-data>

        </provider>

    </application>

</manifest>
其中,android:name属性的值是固定的,android:authorities属性的值必须要和刚才 FileProvider.getUriForFile()方法中的第二个参数一致。另外,这里还在<provider>标签 的内部使用<meta-data>来指定UrL的共享路径,并引用了一个@xml/file_paths资源。当然, 这个资源现在还是不存在的,下面我们就来创建它。
右击res目录->New—^Directory,创建一个xml目录,接着右击xml目录一>New—>File,创 建一个file_paths.xml文件。然后修改file_paths.xml文件中的内容,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android"> <external-path name="my_images" path="" />
</paths>
其中,external-path就是用来指定Uri共享的,name属性的值可以随便填,path属性的值表示共享的具体路径。这里设置空值就表示将整个SD卡进行共享,当然你也可以仅共享我 们存放output_image.jpg这张图片的路径。
另外还有一点要注意,在Android4.4系统之前,访问SD卡的应用关联目录也是要声明权限 的,从4.4系统开始不再需要权限声明。那么我们为了能够兼容老版本系统的手机,还需要在 AndroidManifest.xml中声明一下访问SD卡的权限:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.cameraalbumtest">
<uses-permission android:name=laandroid.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>
这样代码就都编写完了,现在将程序运行到手机上,然后点击Take Photo按钮就可以进行拍 照了,如图8.12所示。拍照完成后,点击中间按钮就会回到我们程序的界面。同时,拍摄的照 片也会显示出来了

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.5 播放多媒体文件

3.5.1 播放音频

在这里插入图片描述

首先需要创建岀一 个MediaPlayer对象,然后调用setDataSource()方法来设置音频文件的路径,再调用 prepare()方法使MediaPlayer进入到准备状态,接下来调用start()方法就可以开始播放音 频,调用pause()方法就会暂停播放,调用resetO方法就会停止播放。
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <Button
            android:id="@+id/play"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Play">

        </Button>

        <Button
            android:id="@+id/pause"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Pause">

        </Button>

        <Button
            android:id="@+id/stop"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Stop">

        </Button>
    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity
package com.yxj.playaudiotest;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.content.pm.PackageManager;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import java.io.File;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    
    

    private MediaPlayer mediaPlayer = new MediaPlayer();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button play = (Button) findViewById(R.id.play);
        Button pause = (Button) findViewById(R.id.pause);
        Button stop = (Button) findViewById(R.id.stop);
        play.setOnClickListener(this);
        pause.setOnClickListener(this);
        stop.setOnClickListener(this);
        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 {
    
    
            initMediaPlayer();
        }
    }

    private void initMediaPlayer() {
    
    
        try {
    
    
            File file = new File("/sdcard/Music/" + "music.mp3");
            mediaPlayer.setDataSource(file.getPath());
            mediaPlayer.prepare();
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
    @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) {
    
    
                    initMediaPlayer();
                } else {
    
    
                    Toast.makeText(this,"拒绝权限将无法使用程序",Toast.LENGTH_SHORT).show();
                    finish();
                }
                break;
            default:
                break;
        }
    }

    @Override
    public void onClick(View v) {
    
    
        switch (v.getId()) {
    
    
            case R.id.play:
                if (!mediaPlayer.isPlaying()) {
    
    
                    mediaPlayer.start();
                }
                break;
            case R.id.pause:
                if (mediaPlayer.isPlaying()) {
    
    
                    mediaPlayer.pause();
                }
                break;
            case R.id.stop:
                if (mediaPlayer.isPlaying()) {
    
    
                    mediaPlayer.reset();
                    initMediaPlayer();
                }
                break;

            default:
                break;
        }
    }

    @Override
    protected void onDestroy() {
    
    
        super.onDestroy();
        if (mediaPlayer != null) {
    
    
            mediaPlayer.stop();
            mediaPlayer.release();
        }
    }
}
在类初始化的时候我们就先创建了一个MediaPlayer的实例,然后在onCreate() 方法中进行了运行时权限处理,动态申请WRITE_EXTERMAL_STORAGE权限。这是由于待会我们 会在SD卡中放置一个音频文件,程序为了播放这个音频文件必须拥有访问SD卡的权限才行。 注意,在onRequestPermissionsResult()方法中,如果用户拒绝了权限申请,那么就调用 finish()方法将程序直接关掉,因为如果没有SD卡的访问权限,我们这个程序将什么都干不了。
用户同意授权之后就会调用initMediaPlayer()方法为MediaPlayer对象进行初始化操 作。在initMediaPlayer()方法中,首先是通过创建一个File对象来指定音频文件的路径,从 这里可以看出,我们需要事先在SD卡下放置一个名为music.mp3的音频文件。后面依 次调用了 setDataSource()方法和prepare()方法,为MediaPlayer做好了播放前的准备。
接下来我们看一下各个按钮的点击事件中的代码。当点击Play按钮时会进行判断,如果当 前MediaPlayer没有正在播放音频,则调用start ()方法开始播放。当点击Pause按钮时会判断, 如果当前MediaPlayer正在播放音频,则调用pause()方法暂停播放。当点击Stop按钮时会判断, 如果当前MediaPlayer正在播放音频,则调用reset ()方法将MediaPlayer重置为刚刚创建的状态, 然后重新调用一遍initMediaPlayer()方法。
最后在onDestroy()方法中,我们还需要分别调用stop()方法和release()方法,将与 MediaPlayer相关的资源释放掉。

另外,千万不要忘记在AndroidManifest.xml文件中声明用到的权限
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.yxj.playaudiotest">

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

在这里插入图片描述
在这里插入图片描述

3.5.2 播放视频

方法名                功能描述
setVideoPath()	设置要播放的视频文件的位置
start()	         开始或继续播放视频
pause()	      暂停播放视频
resume()	   将视频重头开始播放
seekTo()	  从指定的位置开始播放视频
isPlaying()	    判断当前是否正在播放视频
getDuration()	获取载入的视频文件的时长

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            >
            <Button
                android:id="@+id/play"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="Play">

            </Button>

            <Button
                android:id="@+id/puase"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="Pause">

            </Button>

            <Button
                android:id="@+id/replay"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="Replay"
                >

            </Button>

        </LinearLayout>

        <VideoView
            android:id="@+id/video_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

        </VideoView>
    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity
package com.yxj.playvideotest;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.app.Notification;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import android.widget.Toolbar;
import android.widget.VideoView;

import java.io.File;

public class MainActivity extends AppCompatActivity  implements View.OnClickListener {
    
    
    private VideoView videoView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
         videoView = (VideoView) findViewById(R.id.video_view);
        Button play = (Button) findViewById(R.id.play);
        Button pause = (Button) findViewById(R.id.puase);
        Button replay = (Button) findViewById(R.id.replay);
        play.setOnClickListener(this);
        pause.setOnClickListener(this);
        replay.setOnClickListener(this);
        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 {
    
    
            initVideoPath();
        }
    }

    private void initVideoPath() {
    
    
        File file = new File("/sdcard/Movies/01.基础班学习目标_.mp4");
        videoView.setVideoPath(file.getPath());
    }

    @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) {
    
    
                    initVideoPath();
                }else {
    
    
                    Toast.makeText(this,"拒绝权限将无法使用服务",Toast.LENGTH_SHORT).show();
                    finish();
                }
                break;

            default:
                break;
        }
    }

    @Override
    public void onClick(View v) {
    
    
        switch (v.getId()) {
    
    
            case R.id.play:
                if (!videoView.isPlaying()) {
    
    
                    videoView.start();
                }
                break;

            case R.id.puase:
                if (videoView.isPlaying()) {
    
    
                    videoView.pause();
                }
                break;

            case R.id.replay:
                if (videoView.isPlaying()) {
    
    
                    videoView.resume();
                }
                break;
        }
    }

    @Override
    protected void onDestroy() {
    
    
        super.onDestroy();
        if (videoView != null) {
    
    
            videoView.suspend();
        }
    }
}
首先在 onCreate()方法中同样进行了一个运行时权限处理,因为视频文件将会放在SD卡上。当用户同 意授权了之后就会调用initVideoPath()方法来设置视频文件的路径,这里我们需要事先在SD 卡的根目录下放置一个名为movie.mp4的视频文件。
下面看一下各个按钮的点击事件中的代码。当点击Play按钮时会进行判断,如果当前并没 有正在播放视频,则调用start()方法开始播放。当点击Pause按钮时会判断,如果当前视频正 在播放,则调用pause()方法暂停播放。当点击Replay按钮时会判断,如果当前视频正在播放, 则调用resume()方法从头播放视频。
最后在0nDestroy()方法中,我们还需要调用一下suspend()方法,将VideoView所占用 的资源释放掉。
另外,仍然始终要记得在AndroidManifest.xml文件中声明用到的权限,如下所示:
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.yxj.playvideotest">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/unique_perfect/article/details/109763529