[android] 影片邊播放邊下載須知

想邊播放邊下載,第一件事要確認影片檔案的moov atom檔案是否在影片的開頭
參考:http://blog.sina.com.cn/s/blog_6c762bb301016swe.html
1.先下載isoViewer來查看影片資訊
https://code.google.com/archive/p/mp4parser/downloads
選擇isoviewer-1.0-RC-35.jar下載
2.使用cmd開啟
在cmd下輸入指令 java -jar isoviewer-1.0-RC-35.jar
3.確認是否moov檔在前面

1458727193845

像這樣就是沒有在前面,需要一些動作來做到移動

4.下載qt-faststart
網路收尋或者從參考網站下載

5.接著在cmd下指令
指令 原檔名 輸出檔名
qt-faststart xxx.mp4 xxx1.mp4

6.再用isoviewer確認一下

1458727840417

[Android]抓取相簿圖片並且裁切

1.如果只需要抓取相簿圖片,不裁切的方法

onActivityResult加入
(android版本低於KITKAT使用selectImage(),以上的用getPath(), 網路查到的,功能可以正常執行,但未查明實際程序如何跑的)

            if (requestCode == IMAGE_TAKE && data != null) {
                userIconUri = data.getData();
                LogUtils.d(TAG, userIconUri.toString());
                userIconPath = selectImage(getActivity(), userIconUri);
//                userIconPath = getPath(getActivity(), userIconUri);
                LogUtils.d(TAG, userIconPath);
                ImageLoader.getInstance().displayImage(userIconPath, userIcon, mOptions);
                isImageChange = true;
            }
                Intent intent=new Intent(Intent.ACTION_GET_CONTENT);//ACTION_OPEN_DOCUMENT
                intent.addCategory(Intent.CATEGORY_OPENABLE);
                intent.setType("image/jpeg");
                if(android.os.Build.VERSION.SDK_INT>=android.os.Build.VERSION_CODES.KITKAT){
                    startActivityForResult(intent, IMAGE_TAKE_KITKAT);
                }else{
                    startActivityForResult(intent, IMAGE_TAKE);
                }

getPath()

    /**
     * Get the value of the data column for this Uri. This is useful for
     * MediaStore Uris, and other file-based ContentProviders.
     *
     * @param context The context.
     * @param uri The Uri to query.
     * @param selection (Optional) Filter used in the query.
     * @param selectionArgs (Optional) Selection arguments used in the query.
     * @return The value of the _data column, which is typically a file path.
     */
    public static String getDataColumn(Context context, Uri uri, String selection,
                                       String[] selectionArgs) {

        Cursor cursor = null;
        final String column = "_data";
        final String[] projection = {
                column
        };

        try {
            cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
                    null);
            if (cursor != null && cursor.moveToFirst()) {
                final int index = cursor.getColumnIndexOrThrow(column);
                return cursor.getString(index);
            }
        } finally {
            if (cursor != null)
                cursor.close();
        }
        return null;
    }


    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is ExternalStorageProvider.
     */
    public static boolean isExternalStorageDocument(Uri uri) {
        return "com.android.externalstorage.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is DownloadsProvider.
     */
    public static boolean isDownloadsDocument(Uri uri) {
        return "com.android.providers.downloads.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is MediaProvider.
     */
    public static boolean isMediaDocument(Uri uri) {
        return "com.android.providers.media.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is Google Photos.
     */
    public static boolean isGooglePhotosUri(Uri uri) {
        return "com.google.android.apps.photos.content".equals(uri.getAuthority());
    }

    public static boolean isNewGooglePhotoUri(Uri uri) {
        return "com.google.android.apps.photos.contentprovider".equals(uri.getAuthority());
    }

    public static String getImageUrlWithAuthority(Context context, Uri uri) {
        InputStream is = null;
        if (uri.getAuthority() != null) {
            LogUtils.d(TAG, "getImageUrlWithAuthority");
            try {
                is = context.getContentResolver().openInputStream(uri);
                Bitmap bmp = BitmapFactory.decodeStream(is);
                return writeToTempImageAndGetPathUri(context, bmp).toString();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }finally {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    public static Uri writeToTempImageAndGetPathUri(Context inContext, Bitmap inImage) {
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        inImage.compress(Bitmap.CompressFormat.JPEG, 100, bytes);
        String path = MediaStore.Images.Media.insertImage(inContext.getContentResolver(), inImage, "Title", null);
        return Uri.parse(path);
    }

selectImage()

    public static String selectImage(Context context,Uri data){
        Uri selectedImage = data;
//      Log.e(TAG, selectedImage.toString());
        if(selectedImage!=null){
            String uriStr=selectedImage.toString();
            String path=uriStr.substring(10,uriStr.length());
            if(path.startsWith("com.sec.android.gallery3d")){
                LogUtils.e(TAG, "It's auto backup pic path:"+selectedImage.toString());
                return null;
            }
        }
        String[] filePathColumn = { MediaStore.Images.Media.DATA };
        Cursor cursor = context.getContentResolver().query(selectedImage,filePathColumn, null, null, null);
        cursor.moveToFirst();
        int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
        String picturePath = cursor.getString(columnIndex);
        cursor.close();
        return picturePath;
    }

我原本使用的方法

    private String getPathFromURI(Uri selectedImage) {
        final String[] filePathColumn = { MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME };
        Cursor cursor = getActivity().getContentResolver().query(selectedImage, filePathColumn, null, null, null);
        String filePath = "";
        // some devices (OS versions return an URI of com.android instead of com.google.android
        if (selectedImage.toString().startsWith("content://com.android.gallery3d.provider"))  {
            // use the com.google provider, not the com.android provider.
            selectedImage = Uri.parse(selectedImage.toString().replace("com.android.gallery3d","com.google.android.gallery3d"));
            filePath = selectedImage.getPath();
        } else if (selectedImage.toString().startsWith("content://com.google.android.apps.photos.content/"))  {
            // use the com.google provider, not the com.android provider.
            filePath = selectedImage.getLastPathSegment();
        } else if (selectedImage.toString().startsWith("content://com.google.android.apps.photos.contentprovider"))  {
            // use the com.google provider, not the com.android provider.
            filePath = getImageUrlWithAuthority(getActivity(), selectedImage);
        } else {
            filePath = Uri.decode(userIconUri.toString());
        }
        return filePath;
    }

    public static String getImageUrlWithAuthority(Context context, Uri uri) {
        InputStream is = null;
        if (uri.getAuthority() != null) {
            LogUtils.d(TAG, "getImageUrlWithAuthority");
            try {
                is = context.getContentResolver().openInputStream(uri);
                Bitmap bmp = BitmapFactory.decodeStream(is);
                return writeToTempImageAndGetPathUri(context, bmp).toString();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }finally {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    public static Uri writeToTempImageAndGetPathUri(Context inContext, Bitmap inImage) {
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        inImage.compress(Bitmap.CompressFormat.JPEG, 100, bytes);
        String path = MediaStore.Images.Media.insertImage(inContext.getContentResolver(), inImage, "Title", null);
        return Uri.parse(path);
    }

上面的程式執行完,會發現圖片可能會有翻轉角度不正常的,還需要另外判斷角度的數據
(這是網路上查到的資料,還未執行過,等以後要使用再來了解程序如何執行的)

    public static int getExifOrientation(String filepath) {// YOUR MEDIA PATH AS STRING
        int degree = 0;
        ExifInterface exif = null;
        try {
            exif = new ExifInterface(filepath);
        } catch (IOException ex) {
            ex.printStackTrace();
        }
        if (exif != null) {
            int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1);
            if (orientation != -1) {
                switch (orientation) {
                    case ExifInterface.ORIENTATION_ROTATE_90:
                        degree = 90;
                        break;
                    case ExifInterface.ORIENTATION_ROTATE_180:
                        degree = 180;
                        break;
                    case ExifInterface.ORIENTATION_ROTATE_270:
                        degree = 270;
                        break;
                }

            }
        }
        return degree;
    }

2.抓取相簿圖片,並裁切的方法
先執行Intent.ACTION_GET_CONTENT去圖庫抓圖,返回時再Intent com.android.camera.action.CROP 裁切圖片

                Intent photoPickerIntent = new Intent(Intent.ACTION_GET_CONTENT);
                photoPickerIntent.setType("image/*");
                startActivityForResult(photoPickerIntent, RESULT_LOAD_IMAGE);

onActivityResult()

            if (requestCode == RESULT_LOAD_IMAGE) {
                final String CROP_ACTION = "com.android.camera.action.CROP";
                Intent photoCropIntent = new Intent(CROP_ACTION);
                photoCropIntent.putExtra("crop", "true");
                photoCropIntent.putExtra("aspectX", 1);
                photoCropIntent.putExtra("aspectY", 1);
                photoCropIntent.putExtra("outputX", 300);
                photoCropIntent.putExtra("outputY", 300);
                photoCropIntent.putExtra("scale", true);
                photoCropIntent.putExtra("return-data", false);
                photoCropIntent.putExtra(MediaStore.EXTRA_OUTPUT, getTempUri(getActivity()));
                photoCropIntent.putExtra("outputFormat",
                        Bitmap.CompressFormat.JPEG.toString());
                photoCropIntent.setDataAndType(data.getData(), "image/*");

                startActivityForResult(photoCropIntent, RESULT_CROP_IMAGE);
            }

            if (requestCode == RESULT_CROP_IMAGE) {
                String uriString = null;
                if (data.getAction() != null) {
                    LogUtils.d(TAG, "action");
                    uriString = data.getAction();
                } else if (data.getData() != null) {
                    LogUtils.d(TAG, "data");
                    uriString = data.getDataString();
                }
                LogUtils.d(TAG, "userIconPath:" + uriString);
                if (uriString.startsWith("/")) {
                    ImageLoader.getInstance().displayImage("file://" + uriString, userIcon, mOptions);
                } else {
                    ImageLoader.getInstance().displayImage(uriString, userIcon, mOptions);
                }
            }

建立一個Uri給裁切的圖片使用(Uri如同一個檔案位置)

    private static Uri getTempUri(Context context) {
        return context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new ContentValues());
    }

參考網址:
http://my.oschina.net/ryanhoo/blog/86853
http://my.oschina.net/ryanhoo/blog/86842
http://androidbiancheng.blogspot.tw/2011/01/extraoutput.html
http://www.eoeandroid.com/home.php?mod=space&uid=1289462&do=blog&id=49568
http://stackoverflow.com/questions/23236392/failure-delivering-resultinfo

[Android]使用attrs新增物件的屬性並對應selector

功能描述:android的原生物件擁有自己的屬性,例如在layout.xml中設定一個TextView,便有text、enable之類的屬性,若自己想要設定android沒有提供的屬性,就需要在attrs裡面設定。

res/values/attrs.xml

  <declare-styleable name="CalendarPickerView">
    <attr name="android:background"/>
    <attr name="dividerColor" format="color"/>
    <attr name="dayBackground" format="reference"/>
    <attr name="dayTextColor" format="color"/>
    <attr name="titleTextColor" format="color"/>
    <attr name="displayHeader" format="boolean"/>
    <attr name="headerTextColor" format="color"/>
  </declare-styleable>
  <declare-styleable name="calendar_cell">
    <attr name="state_is_can_reserve" format="boolean" />
  </declare-styleable>

format的類別很多color、reference、boolean…
這邊要講的是boolean的控制對應selector

建立一個CustomeView 繼承 TextView

public class CustomeView extends TextView {
    private static final int[] STATE_CAN_RESERVE = {
          R.attr.state_is_can_reserve
      };
    private boolean isReserve = false;
    public void setReserve(boolean isReserve) {
        this.isReserve = isReserve;
        refreshDrawableState();
    }
    @Override protected int[] onCreateDrawableState(int extraSpace) {
        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
        if (isReserve) {
            mergeDrawableStates(drawableState, STATE_CAN_RESERVE);
        }
    }
}

super.onCreateDrawableState(extraSpace + 1)
extraSpace是指android的屬性數量,因為有新增一項自定義的,所以加1
mergeDrawableStates(drawableState, STATE_CAN_RESERVE)將自定義的屬性merge進原本的
refreshDrawableState()在setReserve時就會做狀態及畫面的refresh
這樣便完成屬性的設定

再來是selector

    public CalendarPickerView(Context context, AttributeSet attrs) {
        super(context, attrs);

        Resources res = context.getResources();
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CalendarPickerView);
        final int bg = a.getColor(R.styleable.CalendarPickerView_android_background,
        res.getColor(R.color.calendar_bg));
        dividerColor = a.getColor(R.styleable.CalendarPickerView_dividerColor,
        res.getColor(R.color.calendar_divider));
        dayBackgroundResId = a.getResourceId(R.styleable.CalendarPickerView_dayBackground,
        R.drawable.calendar_bg_selector);
        dayTextColorResId = a.getResourceId(R.styleable.CalendarPickerView_dayTextColor,
        R.color.calendar_text_selector);
        titleTextColor = a.getColor(R.styleable.CalendarPickerView_titleTextColor,
        res.getColor(R.color.calendar_text_active));
        displayHeader = a.getBoolean(R.styleable.CalendarPickerView_displayHeader, true);
        headerTextColor = a.getColor(R.styleable.CalendarPickerView_headerTextColor,
        res.getColor(R.color.calendar_text_active));
    a.recycle();
    }

context.objainStyledAttributes(attrs, R.styleable.CalendarPickerView)
將attrs用TypeArray帶入程式,R.styleable.CalendarPickerView和attrs中的name要相同
dayTextColorResId = a.getResourceId(R.styleable.CalendarPickerView_dayTextColor,
R.color.calendar_text_selector);
將calendar_text_selector帶入

再來是selector

<selector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item android:state_selected="true" android:color="@color/custom_text_selected"/>
    <item android:state_pressed="true" android:color="@color/custom_text_selected"/>
    <item app:state_current_month="false" android:color="@color/custom_text_inactive"/>
    <item app:state_today="true" android:color="@color/custom_text_selected"/>
    <item app:state_selectable="false" android:color="@color/custom_text_inactive" />
    <item
        app:state_current_month="true"
        app:state_is_can_reserve="true"
        android:color="@color/custom_background_today" />
    <item
        app:state_current_month="true"
        app:state_is_can_reserve="false"
        android:color="@color/custom_text_inactive" />
    <item android:color="@color/custom_background_today"/>
</selector>

此時,須先加入一行
xmlns:app="http://schemas.android.com/apk/res-auto"
這樣才能找到自定義的屬性
item 中的判斷很像if else 的判斷,如上,當兩個都true時才設定color

[Android]PinnedSectionListView應用

1.宣告 PinnedSectionListView

private PinnedSectionListView mPinnedListView;

2.onCreat的時候,建立 PinnedSectionListView物件,再設定Adapter

mPinnedListView = (PinnedSectionListView) findViewById(R.id.pinned_listview);
mPinnedListView.setAdapter(new SimpleAdapter(this, R.layout.listitem_productlist_content_pinned, getPinnedProductList()));

3.要放入Adapter的物件,要設定兩種狀態

public class Product {
	public static final int ITEM = 0;
	public static final int SECTION = 1;
}

4.建立一個Adapter class繼承ArrayAdapter並實作PinnedSectionListAdapter
因為View有兩準狀態ITEM或SECTION,所以getViewTypeCount()要return 2
isItemViewTypePinned()設定返回要釘住的是哪一個狀態的view

	class SimpleAdapter extends ArrayAdapter<Product> implements PinnedSectionListAdapter {

		int mItemResource;
		List<Product> products;
		public SimpleAdapter(Context context, int resource,	List<Product> products) {
			super(context, resource, products);
			mItemResource = resource;
			this.products = products;
		}

		class ViewHolder{
			TextView name;
			TextView amount;
			TextView price;
			TextView tital;
			LinearLayout contentTital;
			LinearLayout content;
			TextView nameTital;
			TextView amountTital;
			TextView priceTital;
		}
		@Override
		public View getView(int position, View convertView, ViewGroup parent) {
			ViewHolder holder;
			if (convertView == null) {
				LayoutInflater inflater = LayoutInflater.from(mContext);
				convertView = inflater.inflate(mItemResource, null);
				holder = new ViewHolder();
				holder.name = (TextView) convertView
						.findViewById(R.id.product_listitem_name_cont);
				holder.amount = (TextView) convertView
						.findViewById(R.id.product_listitem_amount_cont);
				holder.price = (TextView) convertView
						.findViewById(R.id.product_listitem_price_cont);
				holder.tital = (TextView) convertView
						.findViewById(R.id.additional_tital);
				holder.contentTital = (LinearLayout) convertView
						.findViewById(R.id.listview_tital);
				holder.content = (LinearLayout) convertView
						.findViewById(R.id.additional_content);
				holder.nameTital = (TextView) convertView
						.findViewById(R.id.product_listitem_name_content);
				holder.amountTital = (TextView) convertView
						.findViewById(R.id.product_listitem_amount_content);
				holder.priceTital = (TextView) convertView
						.findViewById(R.id.product_listitem_price_content);
				convertView.setTag(holder);
			} else {
				holder = (ViewHolder) convertView.getTag();
			}
			convertView.setOnClickListener(new Item_Click(position));
			if (products.get(position).getListType() == Product.SECTION) {
				holder.tital.setVisibility(View.VISIBLE);
				holder.contentTital.setVisibility(View.VISIBLE);
				holder.content.setVisibility(View.GONE);
				holder.tital.setText(products.get(position).getName());
				if (products.get(position).isAdditionalBuySelected()){
					holder.priceTital.setText(mContext.getResources().getString(R.string.additiional_price_tital));
				} else {
					holder.priceTital.setText(mContext.getResources().getString(R.string.price_tital));					
				}
			} else {
				holder.tital.setVisibility(View.GONE);
				holder.contentTital.setVisibility(View.GONE);
				holder.name.setText(products.get(position).getName());
				if (products.get(position).isAdditionalBuySelected())
					holder.amount.setText((products.get(position).getAdditionalBuySelectedIndex() + 1) + "");
				else
					holder.amount.setText("1");
				holder.price.setText(String.format(mContext
						.getString(R.string.currency_s),
						(Long.valueOf(products.get(position)
								.getPromotePrice()) * (products.get(position).getAdditionalBuySelectedIndex() + 1))));
			}
			
			return convertView;
		}
		
        @Override public int getItemViewType(int position) {
            return getItem(position).getListType();
        }

        @Override 
        public int getViewTypeCount() {
            return 2;
        }
		
        @Override
	public boolean isItemViewTypePinned(int viewType) {
		// TODO Auto-generated method stub
		return viewType == Product.SECTION;
	}
}

PinnedSectionListView下載

原始參考範例

[Grails]限制網站可以接受的Domain name

現在很多服務會建立domain name導向你的ip
會讓你的網站被他的網址綁架

不知道這樣做的實際用途,考量到安全問題,可以在Grails Filter加上檢查機制
只要不是你接受的Domain Name,自動導向你要的網站

參考程式碼

//此範例可以將網站導向正確網址(利用人家的page ranking)
// 也可以送出403 error
// ((HttpServletResponse) response).sendError(403);
if (!(request.getServerName().equals("你可以接受的網址")) {
  response.setHeader("Location", "你想要導向的網址");
  response.status = 301;
  response.flushBuffer();
  return false;
} else {
  //正常呼叫
}

Android在待機狀態下顯示Activity

問題描述:要做到像Line一樣,待機時跳出通知畫面,需要做一些設定才有辦法達到效果,因為在非待機模式下直接Intent就可顯示,但是在待機鎖屏的情況下Intent是無法顯示的

解決辦法:

在需要顯示的Activity的onCreate()中加入下列程式

	this.getWindow().setFlags(
			WindowManager.LayoutParams.FLAG_FULLSCREEN
					| WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
					| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
					| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON,
			WindowManager.LayoutParams.FLAG_FULLSCREEN
					| WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
					| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
					| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
					| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
	requestWindowFeature(Window.FEATURE_NO_TITLE);
	setContentView(R.layout.activity_alarm);

參考網址:http://blog.csdn.net/lovekiss2012/article/details/8541548

Android : Swipe View With Tabs

分成三個步驟


1.在onCreate時候建立ActionBar,ActionBar.addTab增加Tab的頁面,並且在FragmentActivity實作TabListener


2.建立一個class繼承FragmentStatePagerAdapter用來狀載Fragment頁面


3.實作TabListener後要orverride OnTabSelected

public class PaymentInfoActivity extends FragmentActivity implements
  TabListener {

 CollectionPagerAdapter mPagerAdapter;
 ViewPager mViewPager;

 @Override 
protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_payment_info);

 final ActionBar mActionBar = getActionBar();
 mActionBar.setHomeButtonEnabled(true);
 mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
 mViewPager = (ViewPager) findViewById(R.id.pager);
 if (mViewPager != null) {
         mPagerAdapter = new CollectionPagerAdapter(
  getSupportFragmentManager());
  mViewPager.setAdapter(mPagerAdapter);
  mViewPager.setOnPageChangeListener(new ViewPager
                                        .SimpleOnPageChangeListener() {

     @Override
     public void onPageSelected(int position) {
  mActionBar.setSelectedNavigationItem(position);
     }
  });
 }
 TextView view1 = (TextView) getLayoutInflater().inflate(
   R.layout.view_custom_tab, null);
 view1.setText(mPagerAdapter.getPageTitle(0));
 mActionBar.addTab(mActionBar.newTab().setCustomView(view1)
   .setTabListener(this));
 TextView view2 = (TextView) getLayoutInflater().inflate(
   R.layout.view_custom_tab, null);
 view2.setText(mPagerAdapter.getPageTitle(1));
 mActionBar.addTab(mActionBar.newTab().setCustomView(view2)
   .setTabListener(this));
    }
}
  
 class CollectionPagerAdapter extends FragmentStatePagerAdapter {

  public CollectionPagerAdapter(FragmentManager fm) {
   super(fm);
  }

  @Override
  public Fragment getItem(int position) {
   switch (position) {
   case 0:
    return new PaymentPickupFragment();
   case 1:
    return new PaymentSearchFragment();
   }
   return null;
  }

   String[] title = new String[] {getResources().getString(R.string.web_payment),
     getResources().getString(R.string.search_payment) };

  @Override
  public CharSequence getPageTitle(int position) {

   return title[position];
  }

  @Override
  public int getCount() {
   // TODO Auto-generated method stub
   return 2;
  }

 }
 @Override
 public void onTabSelected(Tab tab, FragmentTransaction ft) {
  mViewPager.setCurrentItem(tab.getPosition());
 }


FragmentActivity的xml

     
<RelativeLayout 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.support.v4.view.ViewPager
    android:id="@+id/pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
</RelativeLayout>

最後要記得建立多個Fragment頁面加入Tab

Ubuntu 設定 Apache + Tomcat + SSL 憑證

因為開發的網站牽涉到金流問題,因此必須使用https安全通道來傳送資料,原本主機上就有Apache Web Server走80 port,以及Apacha Tomcat走8080 port,但是當一遇到要走https後狀況就變複雜了,因為在Apache Web Server是443 port,而Tomcat是8443 port,原本使用ProxyPass將http 80 port轉到Tomcat 8080 port的方法似乎不能同理用在https上。解決方式是必須透過Apache VirtualHost的SSL設定,並使用ajp的方式轉換到Tomcat上。

1.安裝 OpenSSL
sudo apt-get install openssl

2.啟用SSL
sudo a2enmod ssl
(如果要停用 sudo a2dismod ssl)

3.安裝憑證
這一步驟先教大家產生自己的私人憑證,壞處是當你用https連線時,瀏覽器會出現警告訊息,這是因為憑證沒有經過第三方公司認證過,最好的作法就是去購買一個已經認證過的憑證或是使用StartSSL的免費憑證服務(https://www.startssl.com/),這部份可以參考(http://blog.mowd.tw/index.php?pl=950)
3.1.建立憑證目錄
sudo mkdir /etc/apache2/ssl
3.2.產生憑證
openssl req -new -x509 -days 365 -nodes -out /etc/apache2/ssl/apache.pem -keyout /etc/apache2/ssl/apache.key
(-days 後面的參數365是憑證的有效日期,可以依需要自行調整,-out 和 –keyout 是憑證和金鑰產出後要存放的路徑。)

4.修改虛擬主機設定
這一段是原本80 port透過ajp轉到tomcat的設定
<VirtualHost *:80>
ProxyPreserveHost On
ProxyPass / ajp://localhost:8009/
ProxyPassReverse / ajp://localhost:8009/
ServerName 127.0.0.1
</VirtualHost>


這一段則是將https轉到tomcat的https且不用轉到tomcat:8443的port
<VirtualHost *:443>
SSLEngine on
#使用自己產生的憑證
SSLCertificateFile /etc/apache2/ssl/apache.pem
SSLCertificateKeyFile /etc/apache2/ssl/apache.key
#以下設定是使用StartSSL產生的檔案用
#SSLCertificateFile /etc/apache2/ssl/ssl.crt
#SSLCertificateKeyFile /etc/apache2/ssl/ssl.key
#SSLCertificateChainFile /etc/apache2/ssl/sub.class1.server.ca.pem
#SSLCACertificateFile /etc/apache2/ssl/ca.pem

ProxyPass / ajp://localhost:8009/
ProxyPassReverse / ajp://localhost:8009/
ServerName 127.0.0.1

</VirtualHost>

如此一來就可以同時使用http與https透過apache來轉址到tomcat上
5.重新啟動apache

[Grails]在Tomcat環境下使用ssl憑證配置

為了網站的訊息傳輸安全性,尤其是當涉及金流、線上刷卡的服務時,一般網站會使用加密的HTTP(也就是HTTPS)來傳輸資料,如此一來就可以防止中間人竊取資料,這篇文章簡單的介紹一下在Grails+Tomcat+Linux環境下,如何建立設定HTTPS的環境。

主要步驟如下:
1.產生憑證金鑰(.keystore)
2.複製金鑰到Tomcat目錄
3.設定Tomcat的server.xml
4.修改Grails應用程式的web.xml設定

以下詳細描述各步驟

1.產生憑證金鑰(.keystore)

因為是要給Tomcat使用的金鑰,所以這裡就使用Java內建的keytool工具
在命令列下輸入以下指令keytool -genkey”
然後跟著指示依序輸入以下資料:
輸入 keystore 密碼:
重新輸入新密碼:
您的名字與姓氏為何?
  [Unknown]:
您的編制單位名稱為何?
  [Unknown]:  
您的組織名稱為何?
  [Unknown]:
您所在的城市或地區名稱為何?
  [Unknown]: 
您所在的州及省份名稱為何?
  [Unknown]:  
該單位的二字國碼為何
  [Unknown]:
CN=Test, OU=Unknown , O=Unknown , L=Hsinchu, ST=Taiwan, C=Unknown 正確嗎?
  [否]:  Y

輸入 <mykey> 的主密碼
        (RETURN 如果和 keystore 密碼相同):
重新輸入新密碼:

其中比較重要的是密碼部分,這個密碼會在後面Tomcat設定時用到

2.複製金鑰到Tomcat目錄

完成上述步驟後,會產生.keystore檔案,將該檔案複製到tomcat/webapps目錄下

3.設定Tomcat的server.xml

開啟tomcatconfigserver.xml,將以下這段程式碼打開,並且加上keystoreFile與keystorePass兩個屬性後儲存即可,需要重新啟動tomcat才可以讓設定生效

<Connector port="8443″ protocol="HTTP/1.1″ SSLEnabled="true"
 maxThreads="150″  scheme="https" secure="true"
 clientAuth="false" sslProtocol="TLS" keystoreFile="webapps/.keystore"
 keystorePass="[密碼]" />

4.修改Grails應用程式的web.xml設定

要在Grails專案裡面設定web.xml,需先要執行grails install-templates指令,將web.xml取出後,手動設定安全連線的部份,執行指令後會在/src/templates/war/目錄下產生web.xml檔案,開啟該檔案後,新增以下設定資料:

<security-constraint>
<web-resource-collection>
<http-method>GET</http-method>
<http-method>POST</http-method>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>

之後產生war檔後deploy到tomcat就算完成了。

可以輸入以下網址驗證https是否成功
https://localhost:8443/PROJECT 或是輸入 http://localhost:8080/PROJECT

不過因為憑證是自己建立,所以第一次進到網站的使用者,都會出現以下畫面,必須要按同意繼續,才可以瀏覽網站,為了解決這問題可以透過購買憑證的方式來解決,這部份等試出在在與大家分享。

有興趣者,可以先考慮以下文章

[筆記] SSL 憑證購買記
添加 GoDaddy SSL 证书到你的网站