Open Link with (https://play.google.com/store/apps/details?id=com.tasomaniac.openwith) 앱의 핵심 기능을 테스트삼아서 구현했다. 이 앱은 오픈소스라서 새로 구현할 필요는 없었지만 기능 구현이 끝날 때 까지 오픈소스인줄 몰랐다. 마켓의 등록 정보를 꼼꼼히 읽어봤어야 하는건데.
앱의 기능이 쉽게 이해가 안 될 수도 있다. 예를 들어보면 웹 브라우저로 유튜브 보고 있다가 이것을 유튜브 전용 앱으로 보고 싶다면 공유하기를 눌러 Open Link with를 선택한다. Open Link with에서 다시 실제 실행하고 싶은 유튜브 앱을 선택한다. 그런데 유튜브 웹서비스는 종종 메신저 같은 다른 앱의 인앱 브라우저로도 볼 수 있는데 이때 공유하기를 눌러서 Open Link with를 선택하면 유튜브 앱이 바로 실행되는 것을 볼 수 있다. 번거로운 앱 선택의 절차를 없애주는 앱인 셈이다.
다른 앱에서 공유하기를 할 때 내 앱이 선택 목록에 나오게 하기
android.intent.action.PROCESS_TEXT는 크롬 브라우저 선택영역 플로팅 메뉴에서 내 앱을 노출 시킨다.
<activity android:name=".ShareToAppWithActivity" android:configChanges="keyboardHidden|orientation|screenLayout|screenSize" android:excludeFromRecents="true" android:label="@string/app_name" android:noHistory="true" android:relinquishTaskIdentity="true" android:theme="@style/AppTheme.Transparent" android:windowSoftInputMode="stateAlwaysHidden|adjustResize"> <intent-filter android:label="@string/app_name"> <action android:name="android.intent.action.SEND" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="text/plain" /> </intent-filter> <intent-filter android:label="@string/app_name"> <action android:name="android.intent.action.PROCESS_TEXT" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="text/plain" /> </intent-filter> </activity>
Url을 공유받아 실행할 앱을 선택하는 페이지 구현하기(ShareToAppWithActivity)
class ShareToAppWithActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) loadUri(getIntent()) } override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) loadUri(intent!!) } private fun loadUri(intent: Intent) { var uriString: String? uriString = intent.getStringExtra(Intent.EXTRA_TEXT) // check web browser selection if (TextUtils.isEmpty(uriString)) { uriString = intent.getStringExtra("android.intent.extra.PROCESS_TEXT") } if (TextUtils.isEmpty(uriString)) { finishWithToast() return } try { loadIntentFilters(uriString) } catch (ex: Exception) { showException(ex) } } private fun finishWithToast() { Toast.makeText(this, R.string.invalid_url_msg, Toast.LENGTH_SHORT).show() finish() } private fun loadIntentFilters(uriString: String) { val resolveItemList = IntentResolver(this).queryActivities(uriString) if (!resolveItemList.isEmpty()) { showAppList(resolveItemList, uriString) } else { finishWithToast() } } private fun showException(ex: Exception) { AlertDialog.Builder(this) .setMessage(ex.message) .setPositiveButton("OK", null) .setOnDismissListener { finish() } .show() } private fun showAppList(resolveItemList: ArrayList<IntentResolver.ResolveItem>, uriString: String) { val host = Uri.parse(uriString).host val appItem = Settings.getAppItem(this, host) if (appItem != null) { for (resolveItem in resolveItemList) { if (resolveItem.getAppId() == appItem.appId) { launchActivity(appItem.appId, uriString) return } } try { startActivity(packageManager.getLaunchIntentForPackage(appItem.appId)) finish() return } catch (ex: Exception) { // empty } } val items = Array(resolveItemList.size, { i -> resolveItemList[i].getAppName() }) var selectedIndex = 0 AlertDialog.Builder(this) .setSingleChoiceItems(items, selectedIndex, { d, index -> selectedIndex = index }) .setNegativeButton(R.string.just_once, { d, w -> launchActivity(resolveItemList[selectedIndex].getAppId(), uriString) }) .setPositiveButton(R.string.always, { d, w -> Settings.setAppItem(this@ShareToAppWithActivity, host, resolveItemList[selectedIndex]) launchActivity(resolveItemList[selectedIndex].getAppId(), uriString) }) .setOnDismissListener { finish() } .show() } private fun launchActivity(appId: String, uriString: String) { val intent = Intent() .setAction(Intent.ACTION_VIEW) .setData(Uri.parse(uriString)) .setPackage(appId) try { startActivity(intent) finish() } catch (ex: Exception) { showException(ex) } } }
Url을 브라우징할 수 있는 앱 목록을 얻기(IntentResolver)
class IntentResolver { private val packageManager: PackageManager constructor(activity: Activity) { packageManager = activity.packageManager } fun queryActivities(uriString: String): ArrayList<ResolveItem> { val url = convertUri(uriString) // query app activities val queryIntent = Intent() .setAction(Intent.ACTION_VIEW) .setData(Uri.parse(url)) val resolveItemList = ArrayList<ResolveItem>() val queryFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PackageManager.MATCH_ALL else PackageManager.MATCH_DEFAULT_ONLY val checkSet = HashSet<String>() val list = packageManager.queryIntentActivities(queryIntent, queryFlag) if (!list.isEmpty()) { for (resolveInfo in list) { val packageName = resolveInfo.activityInfo.applicationInfo.packageName if (checkSet.contains(packageName)) { continue } checkSet.add(packageName) resolveItemList.add(ResolveItem(resolveInfo)) } } // query web browser val webBrowserIntent = Intent() .setAction(Intent.ACTION_VIEW) .addCategory(Intent.CATEGORY_BROWSABLE) .setData(Uri.parse("http:")) val list2 = packageManager.queryIntentActivities(webBrowserIntent, 0) if (!list2.isEmpty()) { for (resolveInfo in list2) { val packageName = resolveInfo.activityInfo.applicationInfo.packageName if (checkSet.contains(packageName)) { continue } checkSet.add(packageName) resolveItemList.add(ResolveItem(resolveInfo)) } } return resolveItemList } private fun convertUri(uriString: String): String { var url = uriString; if (!url.contains("://")) { url = "http://$url" } else { var position = url.indexOf("http://") if (position < 0) { position = url.indexOf("https://") } if (position > 0) { url = url.substring(position) } } val position = url.indexOf('\n') if (position > 0) { url = url.substring(0, position) } return url.trim() } inner class ResolveItem(private val resolveInfo: ResolveInfo) { fun getAppName(): String { var appName = resolveInfo.loadLabel(packageManager) if (appName.isNullOrEmpty()) { appName = resolveInfo.activityInfo.applicationInfo.loadLabel(packageManager) } return appName.toString() } fun getAppId() = resolveInfo.activityInfo.applicationInfo.packageName } }
선택한 앱을 Url의 호스트와 함께 저장하기(Settings)
object Settings { data class AppItem(val host: String, val appName: String, val appId: String, val timestamp: Long) private val appItems = HashMap<String, AppItem>() private const val KEY_HOST = "host" private const val KEY_APP_NAME = "appName" private const val KEY_APP_ID = "appId" private const val KEY_TIMESTAMP = "timestamp" fun getAppItem(context: Context, host: String): AppItem? { if (appItems.contains(host)) { return appItems.get(host) } val sharedPreferences = getSharedPreferences(context, host) val appItem = AppItem(sharedPreferences.getString(KEY_HOST, ""), sharedPreferences.getString(KEY_APP_NAME, ""), sharedPreferences.getString(KEY_APP_ID, ""), sharedPreferences.getLong(KEY_TIMESTAMP, 0L)) if (appItem.host.isNotEmpty() && appItem.appId.isNotEmpty() && appItem.timestamp > 0) { appItems.put(host, appItem) return appItem } return null } private fun getSharedPreferences(context: Context, host: String) = context.getSharedPreferences(host, Context.MODE_PRIVATE) fun setAppItem(context: Context, host: String, resolveItem: IntentResolver.ResolveItem) { val editor = getSharedPreferences(context, host).edit() editor.putString(KEY_HOST, host) editor.putString(KEY_APP_NAME, resolveItem.getAppName()) editor.putString(KEY_APP_ID, resolveItem.getAppId()) editor.putLong(KEY_TIMESTAMP, System.currentTimeMillis()) editor.commit() appItems.remove(host) } fun getAppList(context: Context): ArrayList<AppItem> { val appList = ArrayList<AppItem>() val xmlFilesDir = File(context.filesDir.parent, "shared_prefs") val files = xmlFilesDir.listFiles(); if (files != null) { for (file in files) { val name = file.name val host = name.substring(0, name.length - 4) getAppItem(context, host)?.let { appList.add(it) } } } appList.sortBy { it.timestamp } return appList } fun deleteAppItem(context: Context, appItem: AppItem) { getSharedPreferences(context, appItem.host).edit().clear().commit() appItems.remove(appItem.host) } }
앱 저장목록을 보여주고 삭제하기(MainActivity)
class MainActivity : AppCompatActivity() { private lateinit var listView: ListView private lateinit var appList: ArrayList<Settings.AppItem> private lateinit var adapter: ListAdapter private var firstResumed = true override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) listView = findViewById(R.id.list_view) appList = Settings.getAppList(this) adapter = ListAdapter() listView.adapter = adapter listView.setOnItemClickListener { adapterView, view, index, Id -> AlertDialog.Builder(this) .setMessage(getString(R.string.delete_confirm, appList[index].appName)) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.delete, { d, i -> Settings.deleteAppItem(this@MainActivity, appList[index]) appList.removeAt(index) adapter.notifyDataSetChanged() }) .show() } } override fun onResume() { super.onResume() if (firstResumed) { firstResumed = false } else { appList = Settings.getAppList(this) adapter.notifyDataSetChanged() } } inner class ListAdapter : BaseAdapter() { override fun getView(index: Int, convertView: View?, parent: ViewGroup?): View { var baseView = convertView if (baseView == null) { baseView = layoutInflater.inflate(R.layout.list_item, parent, false) } baseView?.findViewById<TextView>(R.id.host)?.text = appList[index].host baseView?.findViewById<TextView>(R.id.app_name)?.text = appList[index].appName return baseView!! } override fun getItem(index: Int): Any { return appList[index] } override fun getItemId(index: Int): Long { return index.toLong() } override fun getCount(): Int { return appList.size } } }
전체 소스 코드: OpenAppWith_src.zip