android 通話自動錄音服務(wù)

需求:
①:通話自動錄音;
②:無界面,只是一個service;
③:錄音自動壓縮上傳;
④:當(dāng)用戶清理后臺的時候,要求service不可以被殺死;
⑤:穩(wěn)定性:1、無網(wǎng)絡(luò)的情況下;2、上傳失??;3、服務(wù)報錯。
解決方案:
①:通話自動錄音
啟動一個service,監(jiān)聽用戶手機通話狀態(tài),當(dāng)檢測到用戶處于通話狀態(tài)下,立即開始錄音,通話結(jié)束后,停止錄音,并保存文件。
此功能的前提條件:
1、錄音權(quán)限、讀寫存儲空間的權(quán)限、讀取通話狀態(tài)的權(quán)限;
2、Service不可以被停止,否則無法錄音。
3、開機啟動(不可以讓用戶每次開機都主動去打開服務(wù))
②:無界面,只是一個service
方案①
普通的service,監(jiān)聽開機廣播,當(dāng)用戶開機的時候,啟動service。但是,發(fā)現(xiàn)service并沒有啟動。想要啟動一個service,必須要有一個activity,即使你不打開這個activity。
在真正做項目的時候,PM會提出各種你不能理解的需求,比如說本系統(tǒng),PM要求本應(yīng)用只是一個錄音服務(wù),不可以有任何界面,也不可以在手機桌面上出現(xiàn)應(yīng)用圖標(biāo)。因此,方案①不可行。
方案②
Android手機在設(shè)置里面都一個輔助功能(個別手機也叫:無障礙),利用這個我們可以實現(xiàn)一些強大的功能,前提是用戶開啟我們的輔助功能,搶紅包軟件就是利用輔助功能實現(xiàn)的。

這里寫圖片描述
這里寫圖片描述

③:錄音自動壓縮上傳
我們只需要在上傳之前對文件進行壓縮處理,然后再上傳即可。
④:當(dāng)用戶清理后臺的時候,要求service不可以被殺死
不會被殺死的服務(wù),或許只有系統(tǒng)服務(wù)吧。當(dāng)然類似于QQ、微信他們做的這種全家桶也可以做到。大公司是可以和廠商合作的,他們的應(yīng)用可以不那么容易被殺死。當(dāng)然也不提倡這樣做,這樣就是垃圾軟件,破壞了Android開發(fā)的美好環(huán)境。
其實,如果可以把服務(wù)設(shè)置成系統(tǒng)服務(wù),那么只要用戶不主動在輔助功能頁面關(guān)掉服務(wù),后臺是清理不掉改服務(wù)的。本人在小米手機上測試過,設(shè)置成系統(tǒng)級別的服務(wù)后,當(dāng)清理后臺的時候,即使服務(wù)被殺死,也會非??斓闹匦聠?。(感興趣的同學(xué)可以試一下)
⑤:穩(wěn)定性:1、無網(wǎng)絡(luò)的情況下;2、上傳失?。?、服務(wù)報錯
思路:
當(dāng)無網(wǎng)絡(luò)的情況下,把錄音文件的地址保存下來(保存的方式有很多:Sqlite、Sharedpreferences等等),上傳失敗也一樣,失敗的原因可能有很多種:網(wǎng)絡(luò)斷開、接口報錯等,當(dāng)網(wǎng)絡(luò)恢復(fù)的時候,可以重新上傳,這樣就不會丟失錄音文件。
代碼很簡單,注釋很詳細:
項目的結(jié)構(gòu):
這里寫圖片描述

<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- 要存儲文件或者創(chuàng)建文件夾的話還需要以下兩個權(quán)限 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET"/>
<!--允許讀取網(wǎng)絡(luò)狀態(tài)-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!--允許讀取wifi網(wǎng)絡(luò)狀態(tài)-->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<service
    android:name=".service.RecorderService"
    android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
    <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService" />
    </intent-filter>
    <meta-data
        android:name="android.accessibilityservice"
        android:resource="@xml/accessible_service_config" />
</service>
/**
 * 電話自動錄音輔助服務(wù)(去電、來電自動錄音并上傳)。
 * Created by wang.ao in 2017/2/24.
 */

public class RecorderService extends AccessibilityService {
    private static final String TAG = "RecorderService";
    private static final String TAG1 = "手機通話狀態(tài)";
    /**
     * 音頻錄制
     */
    private MediaRecorder recorder;
    private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    /**
     * 監(jiān)聽撥號廣播,以便獲取用戶撥出的電話號碼
     */
    private OutCallReceiver outCallReceiver;
    private IntentFilter intentFilter;
    /**
     * 網(wǎng)絡(luò)狀態(tài)改變廣播,當(dāng)網(wǎng)絡(luò)暢通的狀態(tài)下,把用戶未上傳的錄音文件都上傳掉
     */
    private NetworkConnectChangedReceiver networkConnectChangedReceiver;
    private IntentFilter intentFilter2;
    /**
     * 當(dāng)前通話對象的電話號碼
     */
    private String currentCallNum = "";
    /**
     * 區(qū)分來電和去電
     */
    private int previousStats = 0;
    /**
     * 當(dāng)前正在錄制的文件
     */
    private String currentFile = "";
    /**
     * 保存未上傳的錄音文件
     */
    private SharedPreferences unUploadFile;
    private String dirPath = "";
    private boolean isRecording = false;

    @Override
    protected void onServiceConnected() {
        Log.i(TAG, "onServiceConnected");
        Toast.makeText(getApplicationContext(), "自動錄音服務(wù)已啟動", Toast.LENGTH_LONG).show();
    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        // TODO Auto-generated method stub
        Log.i(TAG, "eventType " + event.getEventType());
    }

    @Override
    public void onInterrupt() {
        // TODO Auto-generated method stub
        Log.i(TAG, "onServiceConnected");
    }

    @Override
    public boolean onUnbind(Intent intent) {
        return super.onUnbind(intent);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        TelephonyManager tm = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
        // 監(jiān)聽電話狀態(tài)
        tm.listen(new MyListener(), PhoneStateListener.LISTEN_CALL_STATE);
        outCallReceiver = new OutCallReceiver();
        intentFilter = new IntentFilter();
        //設(shè)置撥號廣播過濾
        intentFilter.addAction("android.intent.action.NEW_OUTGOING_CALL");
        registerReceiver(outCallReceiver, intentFilter);
        //注冊撥號廣播接收器
        networkConnectChangedReceiver = new NetworkConnectChangedReceiver();
        intentFilter2 = new IntentFilter();
        //設(shè)置網(wǎng)絡(luò)狀態(tài)改變廣播過濾
        intentFilter2.addAction("android.net.conn.CONNECTIVITY_CHANGE");
        intentFilter2.addAction("android.net.wifi.WIFI_STATE_CHANGED");
        intentFilter2.addAction("android.net.wifi.STATE_CHANGE");
        //注冊網(wǎng)絡(luò)狀態(tài)改變廣播接收器
        registerReceiver(networkConnectChangedReceiver, intentFilter2);
        unUploadFile = getSharedPreferences("un_upload_file", 0);
        unUploadFile.edit().putString("description", "未上傳的錄音文件存放路徑").commit();
        dirPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/com.ct.phonerecorder/";
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Toast.makeText(getApplicationContext(), "進程被關(guān)閉,無法繼續(xù)錄音,請打開錄音服務(wù)", Toast.LENGTH_LONG).show();
        if (outCallReceiver != null) {
            unregisterReceiver(outCallReceiver);
        }
        if (networkConnectChangedReceiver != null) {
            unregisterReceiver(networkConnectChangedReceiver);
        }
    }

    class MyListener extends PhoneStateListener {
        @Override
        public void onCallStateChanged(int state, String incomingNumber) {
            // TODO Auto-generated method stub
            Log.d(TAG1, "空閑狀態(tài)" + incomingNumber);
            switch (state) {
                case TelephonyManager.CALL_STATE_IDLE:
                    Log.d(TAG1, "空閑");
                    if (recorder != null && isRecording) {
                        recorder.stop();// 停止錄音
                        recorder.release();
                        recorder = null;
                        Log.d("電話", "通話結(jié)束,停止錄音");
                        uploadFile(currentFile);
                    }
                    isRecording = false;
                    break;
                case TelephonyManager.CALL_STATE_RINGING:
                    Log.d(TAG1, "來電響鈴" + incomingNumber);
                    // 進行初始化

                    break;
                case TelephonyManager.CALL_STATE_OFFHOOK:
                    Log.d(TAG1, "摘機" + (!incomingNumber.equals("") ? incomingNumber : currentCallNum));
                    initRecord(!incomingNumber.equals("") ? incomingNumber : currentCallNum);
                    // 開始錄音
                    if (recorder != null) {
                        recorder.start();
                        isRecording = true;
                    }
                default:
                    break;
            }
            super.onCallStateChanged(state, incomingNumber);
        }

    }

    /**
     * 當(dāng)錄音結(jié)束后,自動上傳錄音文件。
     * ①網(wǎng)絡(luò)可用:直接上傳;
     * ②網(wǎng)絡(luò)不可用:保存文件路徑,待網(wǎng)絡(luò)可用的時候再進行上傳;
     * ③上傳失敗的文件,也保存文件路徑,或者重新上傳。
     */
    public void uploadFile(String file) {
        ZipUtils.zipFile(dirPath + file, dirPath + file + ".zip");
        if (NetWorkUtils.isNetworkConnected(getApplicationContext())) {
            //上傳文件
//            OkHttpUtils.postFile()
        } else {
            saveUnUploadFIles(dirPath + file + ".zip");
        }
    }

    /**
     * 保存未上傳的錄音文件
     *
     * @param file 未上傳的錄音文件路徑
     */
    private void saveUnUploadFIles(String file) {
        String files = unUploadFile.getString("unUploadFile", "");
        if (files.equals("")) {
            files = file;
        } else {
            StringBuilder sb = new StringBuilder(files);
            files = sb.append(";").append(file).toString();
        }
        unUploadFile.edit().putString("unUploadFile", files).commit();
    }

    /**
     * 上傳因為網(wǎng)絡(luò)或者其他原因,暫未上傳或者上傳失敗的文件,重新上傳
     */
    public void uploadUnUploadedFiles() {
        //獲取當(dāng)前還未上傳的文件,并把這些文件上傳
        String files = unUploadFile.getString("unUploadFile", "");
        unUploadFile.edit().putString("unUploadFile", "").commit();
        if (files.equals("")) {
            return;
        }
        String[] fileArry = files.split(";");
        int len = fileArry.length;
        for (String file : fileArry) {
            upload(file);
        }
    }

    /**
     * 文件上傳
     *
     * @param file 要上傳的文件
     */
    public void upload(final String file) {
        File file1 = new File(file);
        if (file1 == null || !file1.exists()) {
            //文件不存在
            return;
        }
        if (!NetWorkUtils.isNetworkConnected(getApplicationContext())) {
            saveUnUploadFIles(file);
            return;
        }
        Map<String, String> map = new HashMap<String, String>();
        map.put("type", "1");
        final String url = "http://192.168.1.158:8082/uploader";
        OkHttpUtils.post()//
                .addFile("mFile", file1.getName(), file1)//
                .url(url)//
                .params(map).build()//
                .execute(new StringCallback() {

                    @Override
                    public void onResponse(String response, int id) {
                        Log.e(TAG, "成功 response=" + response);
                    }

                    @Override
                    public void onError(Call call, Exception e, int id) {
                        Log.e(TAG, "失敗 response=" + e.toString());
                        saveUnUploadFIles(file);
                    }
                });
    }

    /**
     * 初始化錄音機,并給錄音文件重命名
     *
     * @param incomingNumber 通話號碼
     */
    private void initRecord(String incomingNumber) {
        previousStats = TelephonyManager.CALL_STATE_RINGING;
        recorder = new MediaRecorder();
        recorder.setAudioSource(MediaRecorder.AudioSource.MIC);// Microphone
        recorder.setOutputFormat(MediaRecorder.OutputFormat.RAW_AMR);// 設(shè)置輸出3gp格式
        File out = new File(dirPath);
        if (!out.exists()) {
            out.mkdirs();
        }
        recorder.setOutputFile(dirPath
                + getFileName((previousStats == TelephonyManager.CALL_STATE_RINGING ? incomingNumber : currentCallNum))
        );
        recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);// 設(shè)置音頻編碼格式
        try {
            recorder.prepare();// 做好準(zhǔn)備
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    /**
     * 獲取錄音文件的名稱
     *
     * @param incomingNumber 通話號碼
     * @return 獲取錄音文件的名稱
     */
    private String getFileName(String incomingNumber) {
        Date date = new Date(System.currentTimeMillis());
        currentFile = incomingNumber + " " + dateFormat.format(date) + ".mp3";
        return currentFile;
    }

    /**
     * 撥號廣播接收器,并獲取撥號號碼
     */
    public class OutCallReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            Log.d(TAG1, "當(dāng)前手機撥打了電話:" + currentCallNum);
            if (intent.getAction().equals(Intent.ACTION_NEW_OUTGOING_CALL)) {
                currentCallNum = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
                Log.d(TAG1, "當(dāng)前手機撥打了電話:" + currentCallNum);
            } else {
                Log.d(TAG1, "有電話,快接聽電話");
            }
        }
    }

    /**
     * 網(wǎng)絡(luò)狀態(tài)change廣播接收器
     */
    public class NetworkConnectChangedReceiver extends BroadcastReceiver {
        private static final String TAG = "network status";

        @Override
        public void onReceive(Context context, Intent intent) {
            /**
             * 這個監(jiān)聽網(wǎng)絡(luò)連接的設(shè)置,包括wifi和移動數(shù)據(jù)的打開和關(guān)閉。.
             * 最好用的還是這個監(jiān)聽。wifi如果打開,關(guān)閉,以及連接上可用的連接都會接到監(jiān)聽。見log
             * 這個廣播的最大弊端是比上邊兩個廣播的反應(yīng)要慢,如果只是要監(jiān)聽wifi,我覺得還是用上邊兩個配合比較合適
             */
            if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
                ConnectivityManager manager = (ConnectivityManager) context
                        .getSystemService(Context.CONNECTIVITY_SERVICE);
                Log.i(TAG, "CONNECTIVITY_ACTION");

                NetworkInfo activeNetwork = manager.getActiveNetworkInfo();
                if (activeNetwork != null) { // connected to the internet
                    if (activeNetwork.isConnected()) {
                        //當(dāng)前網(wǎng)絡(luò)可用
                        if (activeNetwork.getType() == ConnectivityManager.TYPE_WIFI) {
                            // connected to wifi
                            Log.e(TAG, "當(dāng)前WiFi連接可用 ");
                        } else if (activeNetwork.getType() == ConnectivityManager.TYPE_MOBILE) {
                            // connected to the mobile provider's data plan
                            Log.e(TAG, "當(dāng)前移動網(wǎng)絡(luò)連接可用 ");
                        }
                        uploadUnUploadedFiles();
                    } else {
                        Log.e(TAG, "當(dāng)前沒有網(wǎng)絡(luò)連接,請確保你已經(jīng)打開網(wǎng)絡(luò) ");
                    }

                } else {   // not connected to the internet
                    Log.e(TAG, "當(dāng)前沒有網(wǎng)絡(luò)連接,請確保你已經(jīng)打開網(wǎng)絡(luò) ");
                }


            }
        }
    }

}
/**
 * 獲取網(wǎng)絡(luò)連接狀態(tài)工具類
 * Created by wang.ao in 2017/2/24.
 */

public class NetWorkUtils {
    /**
     * 判斷是否有網(wǎng)絡(luò)連接
     * @param context
     * @return
     */
    public static boolean isNetworkConnected(Context context) {
        if (context != null) {
            ConnectivityManager mConnectivityManager = (ConnectivityManager) context
                    .getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo();
            if (mNetworkInfo != null) {
                return mNetworkInfo.isAvailable();
            }
        }
        return false;
    }


    /**
     * 判斷WIFI網(wǎng)絡(luò)是否可用
     * @param context
     * @return
     */
    public static boolean isWifiConnected(Context context) {
        if (context != null) {
            ConnectivityManager mConnectivityManager = (ConnectivityManager) context
                    .getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo mWiFiNetworkInfo = mConnectivityManager
                    .getNetworkInfo(ConnectivityManager.TYPE_WIFI);
            if (mWiFiNetworkInfo != null) {
                return mWiFiNetworkInfo.isAvailable();
            }
        }
        return false;
    }


    /**
     * 判斷MOBILE網(wǎng)絡(luò)是否可用
     * @param context
     * @return
     */
    public static boolean isMobileConnected(Context context) {
        if (context != null) {
            ConnectivityManager mConnectivityManager = (ConnectivityManager) context
                    .getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo mMobileNetworkInfo = mConnectivityManager
                    .getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
            if (mMobileNetworkInfo != null) {
                return mMobileNetworkInfo.isAvailable();
            }
        }
        return false;
    }


    /**
     * 獲取當(dāng)前網(wǎng)絡(luò)連接的類型信息
     * @param context
     * @return
     */
    public static int getConnectedType(Context context) {
        if (context != null) {
            ConnectivityManager mConnectivityManager = (ConnectivityManager) context
                    .getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo();
            if (mNetworkInfo != null && mNetworkInfo.isAvailable()) {
                return mNetworkInfo.getType();
            }
        }
        return -1;
    }


    /**
     * 獲取當(dāng)前的網(wǎng)絡(luò)狀態(tài) :沒有網(wǎng)絡(luò)0:WIFI網(wǎng)絡(luò)1:3G網(wǎng)絡(luò)2:2G網(wǎng)絡(luò)3
     *
     * @param context
     * @return
     */
    public static int getAPNType(Context context) {
        int netType = 0;
        ConnectivityManager connMgr = (ConnectivityManager) context
                .getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
        if (networkInfo == null) {
            return netType;
        }
        int nType = networkInfo.getType();
        if (nType == ConnectivityManager.TYPE_WIFI) {
            netType = 1;// wifi
        } else if (nType == ConnectivityManager.TYPE_MOBILE) {
            int nSubType = networkInfo.getSubtype();
            TelephonyManager mTelephony = (TelephonyManager) context
                    .getSystemService(Context.TELEPHONY_SERVICE);
            if (nSubType == TelephonyManager.NETWORK_TYPE_UMTS
                    && !mTelephony.isNetworkRoaming()) {
                netType = 2;// 3G
            } else {
                netType = 3;// 2G
            }
        }
        return netType;
    }
}

public class ZipUtils {
    private static final int BUFF_SIZE = 1024;

    /**
     * @param zos           壓縮流
     * @param parentDirName 父目錄
     * @param file          待壓縮文件
     * @param buffer        緩沖區(qū)
     *                      URL:http://www.bianceng.cn/OS/extra/201609/50420.htm
     * @return 只要目錄中有一個文件壓縮失敗,就停止并返回
     */
    private static boolean zipFile(ZipOutputStream zos, String parentDirName, File file, byte[] buffer) {
        String zipFilePath = parentDirName + file.getName();
        if (file.isDirectory()) {
            zipFilePath += File.separator;
            for (File f : file.listFiles()) {
                if (!zipFile(zos, zipFilePath, f, buffer)) {
                    return false;
                }
            }
            return true;
        } else {
            try {
                BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
                ZipEntry zipEntry = new ZipEntry(zipFilePath);
                zipEntry.setSize(file.length());
                zos.putNextEntry(zipEntry);
                while (bis.read(buffer) != -1) {
                    zos.write(buffer);
                }
                bis.close();
                return true;
            } catch (FileNotFoundException ex) {
                ex.printStackTrace();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
            return false;
        }
    }

    /**
     * @param srcPath 待壓縮的文件或目錄
     * @param dstPath 壓縮后的zip文件
     * @return 只要待壓縮的文件有一個壓縮失敗就停止壓縮并返回(等價于windows上直接進行壓縮)
     */
    public static boolean zipFile(String srcPath, String dstPath) {
        File srcFile = new File(srcPath);
        if (!srcFile.exists()) {
            return false;
        }
        byte[] buffer = new byte[BUFF_SIZE];
        try {
            ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(dstPath));
            boolean result = zipFile(zos, "", srcFile, buffer);
            zos.close();
            return result;
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
        return false;
    }

    /**
     * @param srcPath 待解壓的zip文件
     * @param dstPath zip解壓后待存放的目錄
     * @return 只要解壓過程中發(fā)生錯誤,就立即停止并返回(等價于windows上直接進行解壓)
     */
    public static boolean unzipFile(String srcPath, String dstPath) {
        if (TextUtils.isEmpty(srcPath) || TextUtils.isEmpty(dstPath)) {
            return false;
        }
        File srcFile = new File(srcPath);
        if (!srcFile.exists() || !srcFile.getName().toLowerCase(Locale.getDefault()).endsWith("zip")) {
            return false;
        }
        File dstFile = new File(dstPath);
        if (!dstFile.exists() || !dstFile.isDirectory()) {
            dstFile.mkdirs();
        }
        try {
            ZipInputStream zis = new ZipInputStream(new FileInputStream(srcFile));
            BufferedInputStream bis = new BufferedInputStream(zis);
            ZipEntry zipEntry = null;
            byte[] buffer = new byte[BUFF_SIZE];
            if (!dstPath.endsWith(File.separator)) {
                dstPath += File.separator;
            }
            while ((zipEntry = zis.getNextEntry()) != null) {
                String fileName = dstPath + zipEntry.getName();
                File file = new File(fileName);
                File parentDir = file.getParentFile();
                if (!parentDir.exists()) {
                    parentDir.mkdirs();
                }
                FileOutputStream fos = new FileOutputStream(file);
                while (bis.read(buffer) != -1) {
                    fos.write(buffer);
                }
                fos.close();
            }
            bis.close();
            zis.close();
            return true;
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
        return false;
    }
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,939評論 25 709
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,538評論 19 139
  • 我不是詩人 但是卻想為你寫詩 你是詩里最美好的意象 想讓你出現(xiàn)在詩的每一頁 我不是歌者 但是卻想為你歌唱 你是歌中...
    麥麥sky閱讀 204評論 0 2
  • 《東狹西谷》 《贏旦兒哥》 《走出匠鄉(xiāng)》 《墨斗》 《贏旦兒》 《墨斗》 《跨出魯班壑》 《廊橋工匠》 《還愿》 ...
    銀河謎米閱讀 245評論 0 2
  • 也不知道這樣究竟過了多少個夜晚,細想一下,也數(shù)的過來。 喜歡的,不喜歡的,有興趣的,沒興趣的,都是惘然,都是價值連...
    外灘世界閱讀 168評論 0 0

友情鏈接更多精彩內(nèi)容