分類
Uncategorized

Arrays.binarySearch 直接使用於 android resource 的問題

為了方便使用陣列常常會直接在array.xml中宣告2個互相對應的陣列 e.g.,

  <string-array name="type">
    <item>0</item>
    <item>1</item>
    <item>2</item>
    <item>3</item>
    <item>4</item>
    <item>5</item>
  </string-array>
  <string-array name="code">
    <item>A</item>
    <item>C</item>
    <item>B</item>
    <item>E</item>
    <item>F</item>
    <item>G</item>
    <item>D</item>
  </string-array>

若直接在code中使用Arrays.binsearch 會出現 index 錯誤 e.g.,

        String[] targetArray = getResources().getStringArray(R.array.type);
        int index = Arrays.binarySearch(targetArray, 3);
        String[] codeArray = getResources().getStringArray(R.array.code);

原因是因為Arrays.binarySearch只能使用在已經排序(自然排序)過的陣列。
若該陣列還未排序會丟出Exception,說明index有問題。
比較快的解決方式就是自己寫取得對應的元素 e.g.,

    private int findSpecifyIndexInArray(String[] targetArray, String specify){
        for (int i = 0; i < targetArray.length; ++i) {
            if (targetArray[i].equals(specify)) {
                return i;
            }
        }
        return -1;
    }
    String[] targetArray = getResources().getStringArray(R.array.type);
    int index = findSpecifyIndexInArray(targetArray, 3);
    String[] codeArray = getResources().getStringArray(R.array.code);

 

分類
Google Cloud Platform

Google Cloud Source Repositories 如何將本地端已存在的 Repo 推送到遠端

Situation :

本地端已存在 git repo,現在想將本地端的 git repo 推送到遠端的新 repo

Action :

1.先到 GCSR 新增遠端新 repo
到 https://source.cloud.google.com/repos ->點擊新增存放區->點擊建立新的存放區->點擊繼續->輸入存放區名稱->選擇已存在專案或新增專案->點擊建立

2.A建立本地端 repo 與SSH驗證

2.A.1在”請選取將程式碼推送至存放區的選項:”選擇”從本機 Git 存放區推送程式碼”

2.A.2在”選取您偏好的驗證方式:”選擇”SSH驗證”

2.A.3點擊透過Google Cloud 註冊安全殼層金鑰組的連結

2.A.4登入 Google 帳號

2.A.5在註冊安全殼層金鑰的視窗,輸入金鑰名稱和金鑰內容

金鑰名稱沒有特別限制,但請盡量提供具體且明確的名稱,如某某電腦

如果你沒有金鑰請按照這篇產生一組新的金鑰。
如果你已經有金鑰按照這篇把金鑰內容填到註冊安全殼層金鑰中。

完成後在註冊安全殼層金鑰視窗都會填入金鑰名稱和金鑰內容,後續就可以透過SSH驗證進行git操作

2.A.6最後進行 3.從本機 Git 存放區推送程式碼:


2.B建立本地端 repo 與手動產生認證(手動產生認證可能會有問題,可以的話盡量使用2.A的SSH來驗證)
2.B.1在”請選取將程式碼推送至存放區的選項:”選擇”從本機 Git 存放區推送程式碼”

2.B.2在”選取您偏好的驗證方式:”選擇”手動產生的憑證”

2.B.3點擊 1.產生及儲存 Git 憑證->將 Configure Git 頁面中的 Windows Users 程式碼複製並貼到本地端的 git bash

2.B.4移動到本地端專案路徑並進行 2. 進行將 Cloud Repository 新增為遠端存放區:(這裡注意最好編輯成1行再貼到 git bash,如 git remote add google https://xxxx)

2.B.5最後進行 3.從本機 Git 存放區推送程式碼:


3.錯誤處理

a.注意若推送時出現以下錯誤

fatal: remote error:
Invalid authentication credentials.
Please generate a new identifier: https://source.developers.google.com/new-password

代表要重新產生憑證,移動到 https://source.developers.google.com/auth/start?scopes=https://www.googleapis.com/auth/cloud-platform,再選擇登入帳號後會出現 Configure Git 頁面,將 Authenticate as yourgoogleaccount to source.developers.google.com 下方顯示的程式碼貼到 git bash(直接複製整段程式碼貼到 git bash 即可)。
Configure Git 頁面主要是用來設定本地端 git 和遠端 repository 的權限,有上下 2 部分的 code,上方的 code 是用在 cmd ,下方的 code 是用在 git bash。

b.若推送時出現以下錯誤

fatal: unable to access ‘https://source.developers.google.com/p/reponame/’: SSL certificate problem: self signed certificate in certificate chain

可以考慮使用以下解法

  1. 試著使用SSH協議代替HTTPS協議。這樣可以避免SSL證書驗證的問題。你需要添加你的公鑰到你的Google Cloud Platform帳戶中。詳細的操作指南可以參考這個連結:https://cloud.google.com/source-repositories/docs/authentication
  2. (快速)如果你想繼續使用HTTPS協議,那麼可以嘗試在Git命令中添加一個參數來忽略SSL證書的驗證。你可以使用以下命令:

    git config --global http.sslVerify false

    這會將Git的全局設置中的http.sslVerify設置為false,這樣Git就不會驗證SSL證書了。但是,這樣做會降低安全性,因為Git無法確定你連接的網站是否可信。

Result :

完成後在 GCSR 的 repo 便可看到推送的內容。

分類
Google Cloud Platform

Google Cloud Source Repositories 試用紀錄

2021/11/19 更新

GCSR的入口不好找,首先進入 Google Cloud Platform,點擊左上方的導覽選單圖示,圖示的下拉選單中有

“持續整合/持續推送軟體更新” 類別的 Source Repositories 就是啦!!!!


GCSR(Google Cloud Source Repositories) 是 Google 推出以 Git 為基礎的程式碼代管服務。
原先我的Side Project是自己建 Git Server 來放置 Source Code,後來因為硬體故障等等
自己維護需要花費不少時間和成本,加上本身也是 Google 的愛好者,最後選擇 GCSR 來試試。
首先談談 GCSR 的缺點,基本上最麻煩的部分為 Google 提供的 Repository 必須依附於Google Cloud Platform(GCP)上的專案。
也就是說你必須先了解基本的 GCP 使用方式才能開始用 GCSR。
第2個麻煩的部分在於 Create, Clone Repository 在官方教學中必須透過 Google Cloud SDK 才能使用(也許可以透過別的方式但還未試出),也就是說你必須先安裝 Google Cloud SDK才能開始使用 GCSR。
第3個麻煩的部分在於免費版的限制,如下

免費方案超量
最多 5 位使用者每位專案使用者每月 $1 美元 (專案使用者達 5 位以上時)
50 GB 儲存空間每 GB 儲存空間 每月 $0.10 美元
50 GB 輸出每 GB 輸出每月 $0.10 美元

若以上 3 個限制您都還可以接受,那就繼續往下看吧。
首先提供官網 GCSR 的說明。若想直接參考使用方式請看 quick start
1.在 Google Cloud Platform 建立專案
建議新增獨立的 GCP 專案來放置 Repository 避免和其他的專案混淆。
2.安裝Google Cloud SDK
安裝 SDK 的目的就是用來新增,複製專案(應該是可以透過其他方式來做到但還未試出)。
注意安裝完成之後還需要進行初始化的動作,參考這裡
3.安裝git
Windows 官方推薦使用 git-scm
Linux就用 command line 安裝吧
4.使用 Google Cloud SDK(gcloud command line) 建立 Repository

gcloud source repos create hello-world

clone repo 到本地端

gcloud source repos clone hello-world

從官網看起來似乎只有 Create 和 Clone 需要使用 gcloud
其它的操作就可以使用一般的 git 指令,如 git add, git commit, git push等等
5. GCSR UI
使用者介面就像 github,gitlab 等等,GCSR也有提供 Repository 的 UI 操作。
以下為 Repository 的介面,存放區即為 Repository

以下是搶先試用版的畫面


Update –2018-09-30

分類
wordpress

同步 WordPress 發文到 Google Blogger

由於之前發文主要在Blogger,目前已改至WordPress。
現在的需求是在WordPress發文後也同步到Blogger上。
搜尋了一下大部份推薦的使用方式是在WordPress上安裝DJ EmailPublish。
從善如流!! 馬上來實驗一下。

1.設定Blogger email 發文

1-1.開啟Blogger設定頁面之後,左邊選擇 “設定”-> “以電子郵件傳送” ->右邊的”使用電子郵件張貼” -> 在預設的空格中填入你想要對應的名稱(我直接填async) -> 填完之後請複製起來,待會會用在DJ EmailPublish的設定上。 -> 最後記得點擊右上方的”儲存設定”

2.到WordPress上搜尋DJ EmailPublish並安裝外掛

略過,就和一般的搜尋安裝外掛相同。

3.設定DJ EmailPublish

3-1.開啟WordPress 控制台 -> 點擊右邊的Settings -> Email Publish -> 左邊 Email Address 填入Step 1-1.你想要對應的名稱 -> Publish Type 選擇 Full Text -> Username 填入WordPress控制台登入的帳號,Password填入WordPress控制台登入的密碼 -> 最後點擊 Update Email Publish Option

4. 完成,你可以嘗試在WordPress上發文,看看Blogger是否有同步。
(本篇就是實驗品!!)

分類
Uncategorized

PhotoView 使用介紹

PhotoView 是一個開源函式庫,主要提供圖片縮放處理,預設已提供兩指縮放,雙擊放大等等基本的手勢功能。
因此若需求相當簡單,可以考慮使用該函式庫來快速完成功能。
GitHub : https://github.com/chrisbanes/PhotoView
相關的相依性以及初始化可以參考官方github。

以下紀錄基本的使用方式:

1. Dependency

1-1.在 project module 的 build.gradle 加入

allprojects {
	repositories {
        maven { url "https://jitpack.io" }
    }
}

1-2.在 module module 的 build.gradle 加入

dependencies {
    implementation 'com.github.chrisbanes:PhotoView:latest.release.here'
}

 

2. declare in xml

<com.github.chrisbanes.photoview.PhotoView
    android:id="@+id/photo_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

注意 com.github.chrisbanes.photoview.PhotoView 命名必須完全相同
 

3. Using in code

3-1. init by resource id

PhotoView photoView = (PhotoView) findViewById(R.id.photo_view);
photoView.setImageResource(R.drawable.image);

3-2. init by bitmap

String photoPath = PHOTO_FILE_PATH + "/" + photoIndex + ".jpg";
Bitmap bitmap = BitmapFactory.decodeFile(photoPath);
PhotoView photoView = (PhotoView) findViewById(R.id.photo_view);
photoView.setImageBitmap(bitmap);

 
4. 雙擊圖片或拖曳圖片即有縮放效果,完全不用加入任何控制的程式碼。

分類
Uncategorized

RecyclerView 基本使用架構

這個基本使用架構主要套用MVP Patterrn,架構中包含1個顯示在V(Activity)的RecyclerView,提供了以下幾個基本功能:
1.RecyclerView可以即時檢視,新增,修改,刪除項目(ItemView)
2.可設定每頁應該包含項目數,切換頁面。
3.透過FinalDB來儲存項目。
4.自定義RecyclerView的ItemView
首先從架構的PackageDiagram來介紹,該架構包含3個package。
主要的package為左上角的basic package,共有6個class,包含MVP組成元件以及RecyclerView的元件。
右上角的database package,共有2個class為提供儲存資料的功能。
下方的common package,只有1個class,提供取得Application context。
Package 相依性如下圖

 
接著從主要的basic package 的 class diagram開始介紹。
MVP構成的元件為RecyclerItemManager(M),RecyclerViewPresenter(P),
RecyclerViewActivity(V),可以看出V和M不直接溝通,而是透過P交互
(雖然在標準的MVP中,V和P是透過interface交互,為了方便介紹架構就不另外為了V和P建立interface)。
RecyclerViewActivity為介面層,只負責初始化UI以及接受使用者輸入並轉發給RecyclerViewPresenter,不包含任何業務邏輯。
RecyclerViewPresenter為邏輯層,主要接受介面層轉發的指令,並再轉發給Model,當Model處理完回傳資料給邏輯層,處理介面邏輯後再回傳給介面層。
RecyclerItemManager為Model,會持有RecyclerItem list,該list是資料的中心,主要處理業務邏輯,不含任何的介面邏輯。
而 RecyclerViewAdapter 和 RecyclerViewHolder 為構成RecyclerView的元件。
RecyclerViewHolder負責管理ItemView(顯示在RecyclerView的項目)。
RecyclerViewAdapter負責建立RecyclerViewHolder以及綁定數據到holder。
最後是RecyclerItem為資料類別,主要是提供數據並顯示在RecyclerView的ItemView。

接著來看看source code。
首先是RecyclerViewActivity,先來看看該Activity使用的介面檔案。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical"
  tools:context=".basic.RecycleViewActivity">
  <LinearLayout
    android:layout_width="match_parent"
    android:orientation="horizontal"
    android:layout_height="wrap_content">
    <Button
      android:id="@+id/recycler_view_btn_add"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="add"/>
  </LinearLayout>
  <android.support.v7.widget.RecyclerView
    android:id="@+id/recycler_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>
</LinearLayout>

相當簡單,只包含1個Button,1個RecyclerView。
接著看看RecyclerViewActivity。

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.OnScrollListener;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import com.codefoxx.recycleviewexample.R;
import java.util.List;
public class RecycleViewActivity extends AppCompatActivity implements OnClickListener {
  private static final String TAG = RecycleViewActivity.class.getSimpleName();
  private RecyclerViewPresenter mRecyclerViewPresenter;
  private RecyclerView mRecyclerView;
  private RecyclerViewAdapter mRecyclerViewAdapter;
  private Button mAdd;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.recycler_view_main_activity);
    mRecyclerViewPresenter = new RecyclerViewPresenter(this);
    initData();
    initUI();
  }
  private void initData() {
    mRecyclerViewPresenter.loadData();
  }
  private void initUI() {
    mAdd = (Button) findViewById(R.id.recycler_view_btn_add);
    mAdd.setOnClickListener(this);
    initRecyclerView();
  }
  private void initRecyclerView() {
    mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
    LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
    linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
    mRecyclerView.setLayoutManager(linearLayoutManager);
    setScrollListenerToRecyclerView();
    mRecyclerViewAdapter = new RecyclerViewAdapter(
        mRecyclerViewPresenter.getItemsByPage(0));
    mRecyclerView.setAdapter(mRecyclerViewAdapter);
  }
  public void recyclerViewClickEvent(View view) {
    int position = mRecyclerView.getChildAdapterPosition(view);
    mRecyclerViewPresenter.showActionDialog(position);
  }
  private void setScrollListenerToRecyclerView() {
    mRecyclerView.addOnScrollListener(new OnScrollListener() {
      @Override
      public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
        recyclerViewScrollToTopAndLoadPreviousPage(newState);
        recyclerViewScrollToBottomAndLoadNextPage(newState);
      }
    });
  }
  private void recyclerViewScrollToBottomAndLoadNextPage(int newState) {
    boolean isScrollToBottomEdge = !mRecyclerView.canScrollVertically(1);
    if (newState == RecyclerView.SCROLL_STATE_IDLE && isScrollToBottomEdge) {
      mRecyclerViewPresenter.loadNextPage();
    }
  }
  private void recyclerViewScrollToTopAndLoadPreviousPage(int newState) {
    boolean isScrollToTopEdge = !mRecyclerView.canScrollVertically(-1);
    if (newState == RecyclerView.SCROLL_STATE_IDLE && isScrollToTopEdge) {
      mRecyclerViewPresenter.loadPreviousPage();
    }
  }
  public void reloadRecyclerView(int page) {
    List<RecyclerItem> itemsOnPage = mRecyclerViewPresenter.getItemsByPage(page);
    mRecyclerViewAdapter = new RecyclerViewAdapter(itemsOnPage);
    mRecyclerView.setAdapter(mRecyclerViewAdapter);
  }
  @Override
  public void onClick(View v) {
    int uiID = v.getId();
    switch (uiID) {
      case R.id.recycler_view_btn_add:
        mRecyclerViewPresenter.addNewItem();
        break;
    }
  }
}

第16行為MVP的Presenter。
第18~19行為構成RecyclerView的基本元件,要使用RecyclerView必須一併使用這2個元件。
第21行為提供加入RecyclerView的項目用。
第27行為Presenter的初始化,把Activity傳入建構式中綁定View。
第28行初始化資料。
第29行初始化UI介面。
第33行為了保持View的簡單,委託Presenter讀取資料。
第37~38行初始化mAdd按鈕。
第39行初始化RecyclerView(重點)。
第43行初始化mRecyclerView變數。
第44~46行建立LinearLayoutManager為垂直並設定給mRecyclerView。
第47行為mRecyclerView設定滾動的監聽器,此監聽器中會偵測mRecyclerView是否已滾動到該頁最上方或最下方,再觸發讀取上一頁或下一頁的動作。
第48~49行初始化mRecyclerViewAdapter。
第50行設定Adapter到RecyclerView。
note:第42行的initRecyclerView為初始化RecyclerView的動作為固定用法。
第53~56行為設定點擊RecyclerView的事件,注意該方法名稱必須對應於RecyclerView的ItemView所使用的layout xml中的onclick名稱。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
...
  android:onClick="recyclerViewClickEvent"
...

第58~67行為設定監聽RecyclerView滾動的事件,在滾動中會判斷是否已到該頁最上方或最下方並觸發讀取上下頁的動作。
第69~74行即為當RecyclerView滾動到最下方並讀取下一頁的動作。
第76~81行即為當RecyclerView滾動到最上方並讀取上一頁的動作。
第83~87行為更新RecyclerView的動作,該動作會在model改變之後被觸發。
第90~97行為Activity的介面點擊事件。
接著是RecyclerViewAdapter

import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.codefoxx.recycleviewexample.R;
import java.util.List;
public class RecyclerViewAdapter extends RecyclerView.Adapter<RecycleViewHolder> {
  private static final String TAG = RecyclerViewAdapter.class.getSimpleName();
  private List<RecyclerItem> mRecycleItems;
  public RecyclerViewAdapter(List<RecyclerItem> source) {
    mRecycleItems = source;
  }
  @Override
  public RecycleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    View itemView = LayoutInflater
        .from(parent.getContext()).inflate(R.layout.recycle_item_view, parent, false);
    RecycleViewHolder viewHolder = new RecycleViewHolder(itemView);
    return viewHolder;
  }
  @Override
  public void onBindViewHolder(RecycleViewHolder holder, int position) {
    String listNumber = mRecycleItems.get(position).getListNumber();
    holder.mListNumber.setText(mRecycleItems.get(position).getListNumber());
    holder.mTime.setText(mRecycleItems.get(position).getTime());
    holder.mDescription.setText(mRecycleItems.get(position).getDescription());
    if (mRecycleItems.get(position).getIsLaunch()) {
      holder.mIsLaunch.setText("已啟用");
    } else {
      holder.mIsLaunch.setText("未啟用");
    }
    if (mRecycleItems.get(position).getIsVibrator()) {
      holder.mIsVibrator.setText("有震動");
    } else {
      holder.mIsLaunch.setText("無震動");
    }
  }
  @Override
  public int getItemCount() {
    return mRecycleItems.size();
  }
}

RecyclerViewAdapter為構成RecyclerView的元件,主要功能為建立ViewHolder以及綁定數據到ViewHolder。
基本的使用方式主要是覆寫3個方法(onCreateVieHolder, onBindViewHolder, getItemCount),並加入相關的實作內容。
第8行注意繼承RecyclerView.Adapter<RecycleViewHolder>
extends RecyclerView.Adapter為固定用法,<RecycleViewHolder>則是自定義類別,ViewHolder也是構成RecyclerView的基本元件。
第11行為adapter持有的數據,也是透過該數據綁定到ViewHolder上。
第18~23行為建立ViewHolder,在該方法中透過LayoutInflater產生需要的itemView並再將itemView傳入viewHolder中,最後回傳viewHolder。
注意第20行的R.layout.recycle_item_view為ItemView使用的介面檔。
note:該方法主要用意在建立viewHolder的介面,目前還未綁定數據到ViewHolder,綁定數據的動作是在onBindViewHolder方法。
第26~41行即為綁定數據到ViewHolder中。
第44~46行為回傳數據的大小。
接著來看看ItemView所使用的介面檔,該介面檔即是描述顯示在RecyclerView中的單個項目。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/basic_recycler_item_baisc"
  android:layout_width="match_parent"
  android:layout_height="110dp"
  android:descendantFocusability="blocksDescendants"
  android:orientation="vertical"
  android:onClick="recyclerViewClickEvent"
  android:weightSum="1">
  <RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="0.6">
    <ImageView
      android:id="@+id/basic_recycler_item_iv_uplayout_upSplitLine"
      android:layout_width="fill_parent"
      android:layout_height="1sp"
      android:layout_alignParentTop="true">
    </ImageView>
    <TextView
      android:id="@+id/basic_recycler_item_tv_uplayout_list_number"
      android:textStyle="bold"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginTop="20dp"
      android:layout_marginLeft="10dp"
      android:layout_alignParentLeft="true"
      android:layout_gravity="center"
      android:text="1"
      android:textSize="20sp"
      android:visibility="invisible"/>
    <TextView
      android:id="@+id/basic_recycler_item_tv_uplayout_time"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginLeft="10dp"
      android:layout_centerVertical="true"
      android:text="06:00"
      android:textSize="18sp"/>
    <TextView
      android:id="@+id/basic_recycler_item_et_uplayout_is_launch"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_centerInParent="true"
      android:text="已啟動"
      android:textSize="18sp"/>
    <ImageView
      android:id="@+id/basic_recycler_item_et_uplayout_is_launch_icon"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginTop="8dp"
      android:layout_gravity="center_horizontal"
      android:layout_toLeftOf="@+id/basic_recycler_item_et_uplayout_is_launch"
      android:adjustViewBounds="true"
      android:background="#00000000"
      android:maxHeight="50dp"
      android:maxWidth="50dp"
      android:scaleType="fitCenter"/>
    <TextView
      android:id="@+id/basic_recycler_item_et_uplayout_is_vibrator"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginRight="10dp"
      android:layout_alignParentRight="true"
      android:layout_centerInParent="true"
      android:text="有震動"
      android:textSize="18sp"/>
    <ImageView
      android:id="@+id/basic_recycler_item_et_uplayout_is_vibrator_icon"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginTop="8dp"
      android:layout_gravity="center_horizontal"
      android:layout_toLeftOf="@+id/basic_recycler_item_et_uplayout_is_vibrator"
      android:adjustViewBounds="true"
      android:background="#00000000"
      android:maxHeight="50dp"
      android:maxWidth="50dp"
      android:scaleType="fitCenter"
      />
  </RelativeLayout>
  <RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="0.4">
    <ImageView
      android:id="@+id/basic_recycler_item_iv_downlayout_upSplitLine"
      android:layout_width="fill_parent"
      android:layout_height="1sp"
      android:layout_alignParentTop="true">
    </ImageView>
    <TextView
      android:id="@+id/basic_recycler_item_iv_downlayout_description_hint"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_alignParentTop="true"
      android:text="描述:"/>
    <TextView
      android:id="@+id/basic_recycler_item_iv_downlayout_description"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_toRightOf="@+id/basic_recycler_item_iv_downlayout_description_hint"
      android:text="這是描述。"/>
    <ImageView
      android:id="@+id/basic_recycler_item_iv_downlayout_downSplitLine"
      android:layout_width="fill_parent"
      android:layout_height="1sp"
      android:layout_alignParentBottom="true"></ImageView>
  </RelativeLayout>
</LinearLayout>

這裡唯一需要注意的是第8行

android:onClick="recyclerViewClickEvent"

這裡為了方便介紹直接使用onClick屬性指定回調方法名稱,該方法名稱即為在
RecyclerViewActivity第53行定義的方法,簡單的說就是當點擊R.id.basic_recycler_item_baisc UI元件時會觸發RecyclerViewActivity第53行定義的方法。
接著是RecyclerViewHolder

import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.codefoxx.recycleviewexample.R;
public class RecycleViewHolder extends RecyclerView.ViewHolder {
  private static final String TAG = RecyclerViewAdapter.class.getSimpleName();
  public LinearLayout mBasicLayout;
  public TextView mListNumber;
  public TextView mTime;
  public TextView mIsLaunch;
  public TextView mIsVibrator;
  public TextView mDescription;
  public RecycleViewHolder(View v) {
    super(v);
    mBasicLayout = (LinearLayout) v.findViewById(R.id.basic_recycler_item_baisc);
    mListNumber = (TextView) v.findViewById(R.id.basic_recycler_item_tv_uplayout_list_number);
    mTime = (TextView) v.findViewById(R.id.basic_recycler_item_tv_uplayout_time);
    mIsLaunch = (TextView) v.findViewById(R.id.basic_recycler_item_et_uplayout_is_launch);
    mIsVibrator = (TextView) v.findViewById(R.id.basic_recycler_item_et_uplayout_is_vibrator);
    mDescription = (TextView) v.findViewById(R.id.basic_recycler_item_iv_downlayout_description);
  }
}

ViewHolder相當簡單,主要是管理ItemView,若想保持封裝性也可以將public 欄位改為private並提供相對應的public方法。
以上就是RecyclerView所使用到有關View的元件。
接著介紹MVP的Presenter

import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.content.DialogInterface;
import android.widget.ArrayAdapter;
import android.widget.Toast;
import com.codefoxx.recycleviewexample.R;
import com.codefoxx.recycleviewexample.database.RecyclerItemDBWrapper;
import com.krtc.common.tool.DialogFactory;
import java.util.List;
import java.util.UUID;
public class RecyclerViewPresenter {
  private static final String TAG = RecyclerViewPresenter.class.getSimpleName();
  private RecycleViewActivity mRecycleViewActivity;
  public RecyclerViewPresenter(
      RecycleViewActivity recycleViewActivity) {
    mRecycleViewActivity = recycleViewActivity;
  }
  public void showActionDialog(final int position) {
    AlertDialog.Builder actionDialog = new Builder(mRecycleViewActivity);
    RecyclerItem item = RecyclerItemManager.getInstance()
        .getItem(position);
    actionDialog.setTitle("選擇動作" + "提醒為:" + item.getDescription());
    ArrayAdapter<CharSequence> actionDialogAdapter = ArrayAdapter.createFromResource(
        mRecycleViewActivity,
        R.array.basic_recycler_view_item_action_select, android.R.layout.select_dialog_item);
    actionDialog.setAdapter(actionDialogAdapter, new DialogInterface.OnClickListener() {
      @Override
      public void onClick(DialogInterface dialog, int which) {
        final int REVIEW = 0;
        final int ADD = 1;
        final int DELETE = 2;
        final int MODIFY = 3;
        if (which == REVIEW) {
          reviewItem(position);
        } else if (which == ADD) {
          addNewItem();
        } else if (which == DELETE) {
          deleteItem(position);
        } else if (which == MODIFY) {
          modifyItem(position);
        }
        dialog.dismiss();
      }
    });
    actionDialog.show();
  }
  private void modifyItem(int position) {
    RecyclerItemManager.getInstance().modifyItem(position);
    mRecycleViewActivity.reloadRecyclerView(RecyclerItemManager.getInstance().getCurrentPageIndex());
  }
  private void deleteItem(int position) {
    RecyclerItemManager.getInstance().deleteItem(position);
    mRecycleViewActivity.reloadRecyclerView(RecyclerItemManager.getInstance().getCurrentPageIndex());
  }
  public void addNewItem() {
    RecyclerItemManager.getInstance().addItem(generateNewItem());
    mRecycleViewActivity.reloadRecyclerView(RecyclerItemManager.getInstance().getCurrentPageIndex());
  }
  private RecyclerItem generateNewItem() {
    RecyclerItem itemForAdd = new RecyclerItem();
    itemForAdd.setId(UUID.randomUUID().toString());
    itemForAdd.setTime("00:00");
    itemForAdd.setDescription("new description");
    itemForAdd.setIsLaunch(false);
    itemForAdd.setIsVibrator(false);
    itemForAdd.setListNumber("");
    return itemForAdd;
  }
  private void reviewItem(int position) {
    RecyclerItem recyclerItem = RecyclerItemManager.getInstance()
        .getItem(position);
    Toast.makeText(mRecycleViewActivity, recyclerItem.toString(), Toast.LENGTH_LONG)
        .show();
  }
  public void loadPreviousPage() {
    if (RecyclerItemManager.getInstance().isMorePreviousPage()) {
      showSwitchPreviousPageConfirmDialog();
    } else {
      showNoMorePreviousPageToast();
    }
  }
  private void showSwitchPreviousPageConfirmDialog() {
    DialogFactory
        .createTwoButtonAlertDialog(mRecycleViewActivity, "切換頁面", "是否切換到上一頁?", "否",
            new DialogInterface.OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog, int which) {
              }
            }, "是", new DialogInterface.OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog, int which) {
                int page = RecyclerItemManager.getInstance().decreaseCurrentPageIndex();
                mRecycleViewActivity.reloadRecyclerView(page);
              }
            }).show();
  }
  private void showNoMorePreviousPageToast() {
    Toast.makeText(mRecycleViewActivity, "已經沒有上一頁了", Toast.LENGTH_SHORT).show();
  }
  public void loadNextPage() {
    if (RecyclerItemManager.getInstance().isMoreNextPage()) {
      showSwitchNextPageConfirmDialog();
    } else {
      showNoMoreNextPageToast();
    }
  }
  private void showSwitchNextPageConfirmDialog() {
    DialogFactory
        .createTwoButtonAlertDialog(mRecycleViewActivity, "切換頁面", "是否切換到下一頁?", "否",
            new DialogInterface.OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog, int which) {
              }
            }, "是", new DialogInterface.OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog, int which) {
                int page = RecyclerItemManager.getInstance().increaseCurrentPageIndex();
                mRecycleViewActivity.reloadRecyclerView(page);
              }
            }).show();
  }
  private void showNoMoreNextPageToast() {
    Toast.makeText(mRecycleViewActivity, "已經沒有下一頁了", Toast.LENGTH_SHORT).show();
  }
  public List<RecyclerItem> getItemsByPage(int page) {
    return RecyclerItemManager.getInstance()
        .getItemsByPage(page);
  }
  public void loadData() {
    List<RecyclerItem> itemsForLoad = RecyclerItemDBWrapper.loadBasicRecyclerItem();
    RecyclerItemManager.getInstance().cleanAndAddItems(itemsForLoad);
  }
}

RecyclerViewPresenter主要接受RecyclerViewActivity轉發的指令,處理需要的邏輯之後再將該指令轉發給Model層,因此會持有對View和對Model的引用。
第15行持有對View的引用(mRecycleViewActivity)。
第17~20行藉由建構式傳入View,和mRecycleViewActivity連結。
第22~52行顯示動作對話框,當使用者點擊RecyclerView上的itemview時,該對話框顯示可操作動作。目前提供檢視,新增,刪除,修改,4個功能。
第54~57行為修改itemview動作。
第59~62行為刪除itemview動作。
第64~67行為新增itemview動作。
note:從以上動作內容可以看出一致性為首先委託RecyclerViewManager進行對model的修改,再通知View更新頁面。
第69~78行為新增itemview使用,這裡為了方便介紹所以新增類似的itemview,實際應用上可以提供使用者輸入新增的相關內容。
第80~85行為檢視itemview動作。
第87~93行為當RecyclerView滾動到該頁的最上方時讀取上一頁的內容。
第95~110行為顯示對話框提醒使用者是否切換到上一頁,使用者選擇否,不進行任何動作,使用者選擇是,就進行讀取上一頁的動作。
第112~114行為提醒使用者該頁已為第一頁。
第116~121行為當RecyclerView滾動到該頁的最下方時讀取下一頁的內容(相對於第87~93行的loadPreviousPage)。
第124~139行為顯示對話框提醒使用者是否切換到下一頁,使用者選擇否,不進行任何動作,使用者選擇是,就進行讀取下一頁的動作。
第141~143行為提醒使用者該頁已為最後頁。
第145~148行為委託RecyclerItemManager取得指定頁數的內容(ItemView)。
第150~153行為啟動RecyclerViewActivity時讀取儲存在資料庫的資料,注意這裡也是為了方便介紹所以直接在UI Thread操作資料庫,標準作法應該要在background thread進行。
接著介紹MVP的Model,RecyclerItemManager
RecyclerItemManager會持有一個RecyclerItem的List,該list的來源為資料庫的內容(RecyclerItemDBWrapper),並提供給RecyclerViewActivity來顯示。

import android.util.Log;
import .database.RecyclerItemDBWrapper;
import java.util.ArrayList;
import java.util.List;
public class RecyclerItemManager {
  private static final String TAG = RecyclerItemManager.class.getSimpleName();
  //Must keep this value big than real item, 1000 is big enough
  private static final int ITEM_PER_PAGE = 20;
  private static final RecyclerItemManager sInstance = new RecyclerItemManager();
  private List<RecyclerItem> mAllResults = new ArrayList<>();
  private List<PageHolder> mAllPageHolders = new ArrayList<>();
  private int mCurrentPageIndex;
  private RecyclerItemManager() {
  }
  public static RecyclerItemManager getInstance() {
    return sInstance;
  }
  public int getCurrentPageIndex() {
    return mCurrentPageIndex;
  }
  public void cleanAndAddItems(List<RecyclerItem> source) {
    mAllResults.clear();
    for (RecyclerItem item : source) {
      mAllResults.add(item);
    }
    mAllPageHolders.clear();
    computePages();
  }
  private void computePages() {
    if (mAllResults.size() < ITEM_PER_PAGE) {
      int totalPage = 1;
      List<RecyclerItem> subList = mAllResults;
      mAllPageHolders.add(new PageHolder(totalPage, subList));
    } else {
      int totalPage = computeTotalPage();
      computeItemsOfPage(totalPage);
    }
    showAllPageHolders();
  }
  private void showAllPageHolders() {
    for (PageHolder onePage : mAllPageHolders) {
      Log.d(TAG, "---");
      Log.d(TAG, "PageHolder.getPage():" + onePage.getPage());
      for (RecyclerItem one : onePage.getItems()) {
        Log.d(TAG, "item.getTime():" + one.getTime());
        Log.d(TAG, "item.getDescription():" + one.getDescription());
      }
      Log.d(TAG, "---");
    }
  }
  private int computeTotalPage() {
    int pageCount = mAllResults.size() / ITEM_PER_PAGE;
    int itemsOfEndPage = mAllResults.size() % ITEM_PER_PAGE;
    int totalPage = 0;
    if (itemsOfEndPage == 0) {
      totalPage = pageCount;
    } else {
      totalPage = pageCount + 1;
    }
    return totalPage;
  }
  private void computeItemsOfPage(int totalPage) {
    for (int page = 0; page < totalPage; ++page) {
      int lastPage = totalPage - 1;
      if (page == lastPage) {
        int endElement = mAllResults.size();
        List<RecyclerItem> subList = mAllResults.subList(page * ITEM_PER_PAGE, endElement);
        mAllPageHolders.add(new PageHolder(page, subList));
      } else {
        List<RecyclerItem> subList = mAllResults
            .subList(page * ITEM_PER_PAGE, (page * ITEM_PER_PAGE) + (ITEM_PER_PAGE));
        mAllPageHolders.add(new PageHolder(page, subList));
      }
    }
  }
  public boolean isMorePreviousPage() {
    if (mCurrentPageIndex > 0) {
      return true;
    }
    return false;
  }
  public boolean isMoreNextPage() {
    if (mCurrentPageIndex < mAllPageHolders.size() - 1) {
      return true;
    }
    return false;
  }
  public int decreaseCurrentPageIndex() {
    mCurrentPageIndex--;
    return mCurrentPageIndex;
  }
  public int increaseCurrentPageIndex() {
    mCurrentPageIndex++;
    return mCurrentPageIndex;
  }
  public List<RecyclerItem> getItemsByPage(int page) {
    PageHolder currentPageHolder = mAllPageHolders.get(page);
    return currentPageHolder.getItems();
  }
  public RecyclerItem getItem(int position) {
    PageHolder currentPageHolder = mAllPageHolders.get(mCurrentPageIndex);
    return currentPageHolder.getItems().get(position);
  }
  public void addItem(RecyclerItem item) {
    RecyclerItemDBWrapper.saveBasicRecyclerItem(item);
    cleanAndAddItems(RecyclerItemDBWrapper.loadBasicRecyclerItem());
  }
  public void isCurrentPageIndexValid() {
    if (mCurrentPageIndex == mAllPageHolders.size()) {
      mCurrentPageIndex--;
    }
  }
  public void deleteItem(int position) {
    RecyclerItem itemForDelete = RecyclerItemManager.getInstance().getItem(position);
    RecyclerItemDBWrapper.deleteItem(itemForDelete);
    RecyclerItemManager.getInstance()
        .cleanAndAddItems(RecyclerItemDBWrapper.loadBasicRecyclerItem());
    RecyclerItemManager.getInstance().isCurrentPageIndexValid();
  }
  public void modifyItem(int position) {
    RecyclerItem itemForModify = RecyclerItemManager.getInstance().getItem(position);
    itemForModify.setDescription("alread fix");
    itemForModify.setTime("alread fix");
    RecyclerItemDBWrapper.update(itemForModify);
    RecyclerItemManager.getInstance()
        .cleanAndAddItems(RecyclerItemDBWrapper.loadBasicRecyclerItem());
  }
  private static class PageHolder {
    private int mPage;
    private List<RecyclerItem> mItems;
    public PageHolder(int page, List<RecyclerItem> items) {
      mPage = page;
      mItems = items;
    }
    public int getPage() {
      return mPage;
    }
    public List<RecyclerItem> getItems() {
      return mItems;
    }
  }
}

第11行為設定每頁有多少ItemView的數量。
第12行為Singleton Pattern的寫法(搭配第17~18行,第20~22行)
第13~14行為計算每頁有多少個ItemView用。
Note:第14行的變數較為重要,該List即為每頁的內容 。e.g., 若 list 有3個element,則代表RecyclerView共有3頁,而每個element可以再透過pageHolder.getItems()來取得該頁上的項目。
第15行為表示目前所在頁數。
第17~18行為建構式私有化,目的為不透過建構式讓外界實體化該類別(Singleton Pattern)。
第20~22行為提供外界取得該類別的實體(Singleton Pattern)。
第24~26行取得目前頁數。
第28~36行為重新計算有多少頁,以及每頁的項目。
第38~49行為計算頁數用。基本算法就是藉由ITEM_PER_PAGE來計算每頁的內容。
第51~61行為顯示頁數以及每頁的內容,只是單純用來顯示。
第63~74行計算總頁數。
第76~89行計算每頁的內容。
第91~96為回傳是否還有上一頁。
第98~103為回傳是否還有下一頁。
第105~108行遞減頁數。
第110~113行遞增頁數。
第115~118行取得指定頁數的全部內容。透過mAllPageHolders取得某個element的內容。
第120~123行取得指定頁數的指定內容。透過mAllPageHolders取得某個element的內容再指定某個位置。
第125~128行加入itemview用。首先加入項目到資料庫中再重新刷新頁數及每頁的內容。注意這裡最好也是透過background thread來進行。
第130~134行檢查並更新目前頁數。這個方法比較特別,主要是用於當刪除項目時,若被刪除項目為該頁的最後一項,則目前頁數必須往前移動一頁。
第136~142刪除項目動作。其實作內容為首先從mAllPageHolders取得要刪除項目(第137行),再從資料庫刪除項目(第138行),接著重新刷新list(第139~140行),最後檢查並設定目前頁數是否正確(第141行)。
第144~151行修改項目動作。其實作內容為首先從mAllPageHolders取得要修改的項目(第145行),設定修改內容(第146~147行),再從資料庫更新項目(第148行),接著重新刷新list(第149~150行)。
第153~170行PageHolder為表示某頁及其內容用。mPage為該頁的頁數,mItems為該頁的內容。
最後為RecyclerItem,該類別為基本的資料類別,無邏輯,用來代表RecyclerView上的ItemView。

import net.tsz.afinal.annotation.sqlite.Id;
public class RecyclerItem {
  @Id
  private String id;
  private String listNumber;
  private String time;
  private boolean isLaunch;
  private boolean isVibrator;
  private String description;
  public String getListNumber() {
    return listNumber;
  }
  public void setListNumber(String listNumber) {
    this.listNumber = listNumber;
  }
  public String getTime() {
    return time;
  }
  public void setTime(String time) {
    this.time = time;
  }
  public boolean getIsLaunch() {
    return isLaunch;
  }
  public void setIsLaunch(boolean isLaunch) {
    this.isLaunch = isLaunch;
  }
  public String getDescription() {
    return description;
  }
  public void setDescription(String description) {
    this.description = description;
  }
  public boolean getIsVibrator() {
    return isVibrator;
  }
  public void setIsVibrator(boolean vibrator) {
    isVibrator = vibrator;
  }
  public String getId() {
    return id;
  }
  public void setId(String id) {
    this.id = id;
  }
  @Override
  public String toString() {
    return "BasicRecyclerItem{" +
        "listNumber='" + listNumber + '\'' +
        ", time='" + time + '\'' +
        ", isLaunch=" + isLaunch +
        ", isVibrator=" + isVibrator +
        ", description='" + description + '\'' +
        '}';
  }
}

比較需要注意的是第5行透過設定@Id為主鍵。
最重要的package介紹完之後,接著介紹其他協作package,這些package主要提供Application Context的取得以及資料庫的操作。
首先是database package,主要提供資料庫操作,使用的ORM為FinalDB,關於FinalDB參考這裡
包含2個類別(FinalDBwrapper,RecyclerItemDBWrapper)。
FinalDBWrapper

import .common.RecyclerViewSingletonApplication;
import net.tsz.afinal.FinalDb;
public class FinalDBWrapper {
    private static final String TAG = FinalDBWrapper.class.getSimpleName();
    public static final FinalDb DB_INSTANCE = FinalDb
        .create(RecyclerViewSingletonApplication.getInstance(), "test.db", true);
}

相當簡單,主要就是第6~7行的DB_INSTANCE,初始化該變數並設定資料庫名稱,debug模式。後續操作資料庫的動作都是透過DB_INSTANCE來執行。
而RecyclerViewSingletonApplication就是繼承Application,用來取得context。
RecyclerItemDBWrapper

import .basic.RecyclerItem;
import java.util.List;
public class RecyclerItemDBWrapper {
  public static void saveBasicRecyclerItem(RecyclerItem item) {
    FinalDBWrapper.DB_INSTANCE.save(item);
  }
  public static List<RecyclerItem> loadBasicRecyclerItem() {
    return FinalDBWrapper.DB_INSTANCE.findAll(RecyclerItem.class);
  }
  public static void deleteItem(RecyclerItem itemForDelete) {
    FinalDBWrapper.DB_INSTANCE.delete(itemForDelete);
  }
  public static void update(RecyclerItem itemForModify) {
    FinalDBWrapper.DB_INSTANCE.update(itemForModify);
  }
}

這個類別也相當簡單,主要提供資料庫的具體操作,具體操作都是透過FinalDBWrapper DB_INSTANCE 進行。
最後就是common package,只有一個類別RecyclerViewSingletonApplication,主要提供 Application context的取得

import android.app.Application;
public class RecyclerViewSingletonApplication extends Application {
  private static final String TAG = RecyclerViewSingletonApplication.class.getSimpleName();
  private static RecyclerViewSingletonApplication sUniqueInstance;
  public static RecyclerViewSingletonApplication getInstance() {
    return sUniqueInstance;
  }
  @Override
  public void onCreate() {
    super.onCreate();
    sUniqueInstance = this;
    sUniqueInstance.initData();
  }
  private void initData() {
  }
}

 
實際操作畫面


分類
Uncategorized

使用AlarmManager建立定時廣播通知並透過BroadcastReceiver接受廣播通知

1.使用AlarmManager建立或取消定時廣播

首先必須先建立定時廣播的時間

  private static Calendar generateAlarmTime() {
    Calendar reminderTime = Calendar.getInstance();
    reminderTime.setTimeInMillis(System.currentTimeMillis());
    long systemTime = System.currentTimeMillis();
    //同步目前時間
    reminderTime.setTimeInMillis(System.currentTimeMillis());
    // 設定時區避免時差
    reminderTime.setTimeZone(TimeZone.getTimeZone("GMT+8"));
    reminderTime.set(Calendar.HOUR_OF_DAY, 10);
    reminderTime.set(Calendar.MINUTE, 30);
    reminderTime.set(Calendar.SECOND, 0);
    reminderTime.set(Calendar.MILLISECOND, 0);
    long selectTime = reminderTime.getTimeInMillis();
    // 如果現在時間大於設定的時間,就從第2天的設定時間開始
    if (systemTime > selectTime) {
      reminderTime.add(Calendar.DAY_OF_MONTH, 1);
    }
    return reminderTime;
  }

第9~12行即為設定定時廣播的時間,時間為早上10點30分。
時間設定之後便可透過AlarmManager來發送定時廣播。

  private static void setAlarmTime(Context context, Calendar alarmTime) {
    Intent intent = new Intent(context, CycleAlarmReceiver.class);
    PendingIntent pendingIntent = PendingIntent
        .getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    AlarmManager alarmManager = (AlarmManager) context.getSystemService(ALARM_SERVICE);
    final int millisecondPerDay = 1000 * 60 * 60 * 24;
    alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, alarmTime.getTimeInMillis(), millisecondPerDay,
        pendingIntent);
  }

這裡需要注意不少地方,第2行CycleAlarmReceiver.class為自定義的Receiver,用途為定時的時間啟動後,該Receiver的onReceiver方法就會被呼叫。
第3~4行需要注意getBroadcaset方法的第2個參數和第4個參數。
第2個參數是用來識別pendingIntent是哪一個,也就是當建立一個定時廣播之後,想要取消掉該廣播或想對該廣播做修改,必須指定是哪一個廣播。以範例來說就是指定0號廣播。
第4個參數表示意義如下
FLAG_CANCEL_CURRENT:如果当前系统中已经存在一个相同的PendingIntent对象,那么就将先将已有的PendingIntent取消,然后重新生成一个PendingIntent对象。
FLAG_NO_CREATE:如果当前系统中不存在相同的PendingIntent对象,系统将不会创建该PendingIntent对象而是直接返回null。
FLAG_ONE_SHOT:该PendingIntent只作用一次。在该PendingIntent对象通过send()方法触发过后,PendingIntent将自动调用cancel()进行销毁,那么如果你再调用send()方法的话,系统将会返回一个SendIntentException。
FLAG_UPDATE_CURRENT:如果系统中有一个和你描述的PendingIntent对等的PendingInent,那么系统将使用该PendingIntent对象,但是会使用新的Intent来更新之前PendingIntent中的Intent对象数据,例如更新Intent中的Extras。
最後是第7行的setRepeat方法,該方法表示這個alarm會重複啟動,而啟動的間隔時間為第3個參數,第3個參數代表每隔一天啟動。
綜合以上2個方法如下

  private static void addReminder(Context context) {
    Calendar alarmTime = generateAlarmTime();
    setAlarmTime(context, alarmTime);
  }

完整類別如下

public class AlarmTimeManager {
  private static Calendar generateAlarmTime() {
    Calendar reminderTime = Calendar.getInstance();
    reminderTime.setTimeInMillis(System.currentTimeMillis());
    long systemTime = System.currentTimeMillis(); //同步目前時間
    reminderTime.setTimeInMillis(System.currentTimeMillis()); // 設定時區避免時差
    reminderTime.setTimeZone(TimeZone.getTimeZone("GMT+8"));
    reminderTime.set(Calendar.HOUR_OF_DAY, 10);
    reminderTime.set(Calendar.MINUTE, 30);
    reminderTime.set(Calendar.SECOND, 0);
    reminderTime.set(Calendar.MILLISECOND, 0);
    long selectTime = reminderTime.getTimeInMillis(); // 如果現在時間大於設定的時間,就從第2天的設定時間開始
      if (systemTime > selectTime)
      {
         reminderTime.add(Calendar.DAY_OF_MONTH, 1);
      }
      return reminderTime;
    }
  private static void setAlarmTime(Context context, Calendar alarmTime) {
    Intent intent = new Intent(context, CycleAlarmReceiver.class);
    PendingIntent pendingIntent = PendingIntent
        .getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    AlarmManager alarmManager = (AlarmManager) context.getSystemService(ALARM_SERVICE);
    final int millisecondPerDay = 1000 * 60 * 60 * 24;
    alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, alarmTime.getTimeInMillis(), millisecondPerDay,
        pendingIntent);
  }
   public static void addReminder(Context context)
    {
      Calendar alarmTime = generateAlarmTime();
      setAlarmTime(context, alarmTime);
    }
}

 

2.當setRepeat完成後,alarm時間(早上10點30分)一到便會發送通知給CycleAlarmReceiver。

public class CycleAlarmReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent) {
      Toast.makeText(context, "alarm 啟動", Toast.LENGTH_SHORT).show();
  }
}

onReceive被呼叫後便會顯示一個toast
注意CycleAlarmReceiver必須在AndroidManifest.xml註冊。

    <receiver android:name=".alarm.CycleAlarmReceiver">
    </receiver>

android:name必須指定CycleAlarmReceiver的檔案路徑。

分類
Uncategorized

FinalDB 使用紀錄

FinalDB 為 Android orm 框架其中一種,其最大的好處在於能夠一行完成CRUD操作,這也是當初選擇它的原因,但現在年久失修加上一些缺點,不得不另循其他orm框架(Android Room使用紀錄)。
以下紀錄簡單的使用技巧

1.加入專案相依性

1-1.到 https://github.com/yangfuhai/afinal/tree/master/bin 下載jar檔(afinal_0.5_bin.jar)
1-2.將下載的jar檔放到 App module 的 libs 目錄中
1-3.在App module的build.gradle加入

...
dependencies {
    ...
    implementation files('libs/afinal_0.5_bin.jar')
}

 

2.使用方式

2-1.取得DataBase操作實體

FinalDb.create(context, "test.db", true);

回傳的實體即可進行CRUD操作,context為application context實體,test.db為資料庫名稱,true為啟動debug模式,進行CRUD操作會顯示相關訊息。
以下建立FinalDBWrapper來包裝finalDb

public class FinalDBWrapper {
    public static final FinalDb DB_INSTANCE = FinalDb.create(SingletonApplication.getInstance(), "test.db", true);
}

之後只要透過FinalDBWrapper.DB_INSTANCE來操作即可。
2-2.要儲存的類別

import net.tsz.afinal.annotation.sqlite.Id;
import net.tsz.afinal.annotation.sqlite.Table;
@Table(name = "Reminder")
public class Reminder {
  @Id
  private String listNumber;
  private String time;
  private boolean isLaunch;
  private boolean isVibrator;
  private String description;
  public String getListNumber() {
    return listNumber;
  }
  public void setListNumber(String listNumber) {
    this.listNumber = listNumber;
  }
  public String getTime() {
    return time;
  }
  public void setTime(String time) {
    this.time = time;
  }
  public boolean getIsLaunch() {
    return isLaunch;
  }
  public void setIsLaunch(boolean isLaunch) {
    this.isLaunch = isLaunch;
  }
  public String getDescription() {
    return description;
  }
  public void setDescription(String description) {
    this.description = description;
  }
  public boolean getIsVibrator() {
    return isVibrator;
  }
  public void setIsVibrator(boolean vibrator) {
    isVibrator = vibrator;
  }
}

第5行標示資料表名稱。
第8行指定主鍵(不可重複)。
第15~53行為必須有的setter and getter
注意要儲存的類別內無法內嵌另一個自定義類別。e.g.,

public class PatrolReminder {
  ...
  private CustomClass customClass;
}
public class CustomClass{...}

目前似乎還無法這麼做。
2-3. Insert 資料

Reminder reminder = new Reminder();
reminder.setListNumber("0");
FinalDBWrapper.DB_INSTANCE.save(reminder);

第3行插入主鍵為0的reminder,主鍵不可重複。
2-4. Query資料

public static List<Reminder> queryAllReminders() {
    return FinalDBWrapper.DB_INSTANCE.findAll(Reminder.class);
}
public static List<Reminder> queryListNumberEqual20(){
    String searchMessage = " listNumber=\"" + "20" + "\"";
  return  FinalDBWrapper.DB_INSTANCE.findAllByWhere(Reminder.class, searchMessage);
}

第1~3行取得所有已儲存的Reminder
第6~9行取得listNumber 等於20 的 Reminder
2-5. Update資料

  public void update() {
    Reminder reminder = new Reminder();
    reminder.setListNumber("0");
    FinalDBWrapper.DB_INSTANCE.update(reminder);
  }

第3行設定主鍵為0,第4行會以傳入的參數取代已儲存主鍵為0資料。
2-6. Delete資料

  public void delete() {
    Reminder reminder = new Reminder();
    reminder.setListNumber("0");
    FinalDBWrapper.DB_INSTANCE.delete(reminder);
  }

第3行設定主鍵為0,第4行會刪除已儲存主鍵為0資料。

分類
Uncategorized

Room (Persistence Library) 使用紀錄

Room 為 Android 官方推薦的 ORM 框架,該框架封裝了 SQLite 為底層並提供高層介面以方便使用者快速開發數據儲存。
官方連結:https://developer.android.com/topic/libraries/architecture/room
以下紀錄如何安裝及簡單的使用技巧。

1.加入Room 到你的專案中

1-1.在 Project 的 build.gradle 加入

allprojects {
    repositories {
        jcenter()
        google()
    }
}

1-2.在 Module 的 build.gradle 加入

dependencies {
...
    def room_version = "1.1.1"
    implementation "android.arch.persistence.room:runtime:$room_version"
    annotationProcessor "android.arch.persistence.room:compiler:$room_version" // use kapt for Kotlin
...

 

2.建立相關元件

2-1.首先建立要儲存的類別(User)

import android.arch.persistence.room.Embedded;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.PrimaryKey;
@Entity(tableName = "user")
public class User {
  @PrimaryKey
  private int id;
  private int listNumber;
  private boolean active;
  private String name;
  private String description;
  @Embedded
  private Account account;
  public int getId() {
    return id;
  }
  public void setId(int id) {
    this.id = id;
  }
  public boolean getActive() {
    return active;
  }
  public void setActive(boolean active) {
    this.active = active;
  }
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  public String getDescription() {
    return description;
  }
  public void setDescription(String description) {
    this.description = description;
  }
  public int getListNumber() {
    return listNumber;
  }
  public void setListNumber(int listNumber) {
    this.listNumber = listNumber;
  }
  public Account getAccount() {
    return account;
  }
  public void setAccount(Account account) {
    this.account = account;
  }
}

注意以下幾點:
第5行使用 @Entity 註解並指定資料表名稱,也可以不指定資料表名稱,預設會使用類別名稱當作資料表名稱。
第8~9行使用 @PrimaryKey 指定主鍵,主鍵為 id
第15~16行自定義類別 Account 必須使用 @Embedded 註明
第18~64行為一般的 getter and setter 方法
自定義類別 Account 如下

import android.arch.persistence.room.ColumnInfo;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.PrimaryKey;
@Entity(tableName = "account")
public class Account {
  @PrimaryKey
  @ColumnInfo(name = "account_id")
  private int id;
  @ColumnInfo(name = "account_name")
  private String name;
  public int getId() {
    return id;
  }
  public void setId(int id) {
    this.id = id;
  }
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
}

注意以下幾點
和 User 類別類似,唯一需要注意的是第 9 行和第 12 行的 @ColumnInfo() 註解。
當某個類別(Account)為另一個類別(User)的嵌入類別時,若欄位名稱有相同必須透過 @ColumnInfo 指定另一個名稱。
以第 9 行 name = “account_id” 為例,就是告訴 Room,Account 的 id 在資料表中欄位名稱為 account_id,避免和 User 的 id 欄位名稱重覆。

2-2.建立DAO(UserDAO)

import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Delete;
import android.arch.persistence.room.Insert;
import android.arch.persistence.room.OnConflictStrategy;
import android.arch.persistence.room.Query;
import android.arch.persistence.room.Update;
import java.util.List;
@Dao
public interface UserDao {
  @Query("SELECT * FROM user")
  public List<User> getAllUser();
  @Query("SELECT * FROM user WHERE active = :active")
  public List<User> getUserActive(boolean active);
  @Query("SELECT * FROM user WHERE listNumber < :lessNumber")
  public List<User> getUserListNumberLessThan(int lessNumber);
  @Insert(onConflict = OnConflictStrategy.REPLACE)
  public void insertUser(User users);
  @Insert(onConflict = OnConflictStrategy.REPLACE)
  public void insertUsers(List<User> users);
  @Update
  public void updateUsers(User... users);
  @Update
  public void updateUser(User user);
  @Delete
  public void deleteUser(User user);
  @Delete
  public void deleteUsers(List<User> users);
}

UserDAO 即為操作 User 的 Data Access Object,關於 User 的資料庫操作介面全部都會定義在這裡。只需要定義介面就好,Room 會自動實作其內容。
注意以下幾點
第9~10行使用建立 interface 並使用 @Dao 標示
第12~13行使用 @Query 標示該介面為 query 動作並在括號中指定要動作的內容。
Room 提供了4種註解@Query, @Update, @Insert, @Delete 這4種註解分別提供不同的動作,各個動作可以指定的內容請參考 API。
以第12~13行為例,呼叫 getAllUser 就等於 select * from user 代表回傳已儲存的所有 user 內容。
第15~16行代表回傳已儲存的所有 user 內容且 active 欄位必須符合傳入的參數(true or false)。
第18~19行代表回傳已儲存的所有 user 內容且 listnumber 欄位必須小於指定的參數。
第21~22行為插入資料用,而 onConflict = OnConflictStrategy.REPLACE 代表若主鍵發生重複,那就把新的資料取代已儲存的資料。
第24~25行為插入資料用,類似第21~22行。差別在於可以傳入 list。
第27~28行為更新資料用,會根據主鍵以傳入的資料取代已儲存的資料。
第30~31行為更新資料用,類似第27~28行。差別在於可以傳入 list。
第33~34行為刪除資料用,會根據主鍵刪除已儲存的資料。
第36~37行為刪除資料用,類似第33~34行。差別在於可以傳入 list。

2-3.建立一個抽象類別並繼承RoomDatabase

@Database(version = 1, entities = {User.class})
public abstract class RoomDBWrapper extends RoomDatabase {
  private static RoomDBWrapper sUnique = Room
      .databaseBuilder(SingletonApplication.getInstance(), RoomDBWrapper.class, "user.db")
      .allowMainThreadQueries().build();
  public static RoomDBWrapper getInstance() {
    return sUnique;
  }
  public abstract UserDao getUserDao();
}

注意以下幾點
1.該類別主要用途為建立資料庫並管理 DAO,可以考慮套用 Singleton Pattern。
2.必須是抽象類別並繼承 RoomDatabase
第1行使用 @Database 註解並指定 version(版本),entities(要儲存類別,如有多個就必須一一加入)
第4行建立 sUnique 變數,之後都透過該變數來取得 DAO 並進行相關操作。
第5行的 SingletonApplication 是自定義類別,繼承 Application 如下

public class SingletonApplication extends Application
{
    private static SingletonApplication sUniqueInstance;
    public static SingletonApplication getInstance()
    {
        return sUniqueInstance;
    }
...

user.db 為資料庫名稱。
第6行注意 allowMainThreadQueries(),呼叫該方法代表予許操作資料庫的動作進行在 ui thread 上,若沒有呼叫該方法,預設是不能在 ui thread 上操作資料。其實也不建議在 ui thread 上進行資料庫操作,避免 ANR。
第8~10行為提供對外存取 sUnique 的方法。
第12行為要儲存類別的 DAO。

3.使用方式

3-1.Insert 資料

  private void insert() {
    for (int i = 0; i < 10; ++i) {
      User user = new User();
      user.setId(i);
      user.setListNumber(i);
      user.setName("name " + i);
      if (i % 2 == 0) {
        user.setActive(true);
      } else {
        user.setActive(false);
      }
      user.setDescription("empty");
      Account account = new Account();
      account.setId(i);
      account.setName("name " + i);
      user.setAccount(account);
      RoomDBWrapper.getInstance().getUserDao().insertUser(user);
    }
  }

第3~16行為建立要 insert 的資料。
第17行即呼叫 RoomDBWrapper 進行實際 insert 的動作。
insert 方法完成後 user 資料表就會有 10 筆 user 資料。

3-2.Query 資料

  private void queryAllUsers() {
    List<User> allUser = RoomDBWrapper.getInstance().getUserDao().getAllUser();
  }

3-3.Update資料

  private void updateUser5() {
    User user = new User();
    user.setId(5);
    user.setName("updateUser5");
    RoomDBWrapper.getInstance().getUserDao().updateUser(user);
  }
  private void updateUsers() {
    for (int i = 0; i < 10; ++i) {
      User user = new User();
      user.setId(i);
      user.setName("name " + i + 10);
      RoomDBWrapper.getInstance().getUserDao().updateUsers(user);
    }
  }

第3行當 user.setId(5) 之後,並將該 user 傳入第4行 updateUser(user)方法後,就會取代已儲存資料中主鍵(id)為 5 的項目。
第11行 setId 的參數範圍從 0~9,當呼叫第13行 updateUsers(user) 後,就會取代已儲存資料中主鍵(id)範圍從 0~9 的項目。

3-4. Delete 資料

  private void deleteUser2() {
    User user = new User();
    user.setId(2);
    RoomDBWrapper.getInstance().getUserDao().deleteUser(user);
  }

第3行指定 setId(2)
第4行呼叫 deleteUser(user),即代表刪除已儲存資料中,主鍵(id)為 2 的項目。

分類
Uncategorized

android 裝置開機後收到廣播通知(BootUpReceiver)

一般要收到系統的廣播通知都是類似的做法。
1.首先建立要接收系統開機完成廣播通知的Receiver

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
public class BootUpReceiver extends BroadcastReceiver {
  private static final String TAG = BootUpReceiver.class.getSimpleName();
  @Override
  public void onReceive(Context context, Intent intent) {
    Toast.makeText(context, "boot up!!!", Toast.LENGTH_SHORT).show();
  }
}

onReceive方法中即為收到通知後要做的動作。
2.在AndroidManifest.xml新增開機通知權限以及BootUpReceiver的註冊。

...
  <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
...
    <receiver
      android:enabled="true"
      android:exported="true"
      android:name=".alarm.BootUpReceiver">
      <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
      </intent-filter>
    </receiver>
...

要注意android:name必須對應BootUpReceiver的檔案路徑。